package chai_test

import (
	"database/sql"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"

	_ "github.com/chaisql/chai"
	"github.com/chaisql/chai/internal/testutil"
	"github.com/stretchr/testify/require"
)

func ExampleTx() {
	db, err := sql.Open("chai", ":memory:")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	tx, err := db.Begin()
	if err != nil {
		panic(err)
	}
	defer tx.Rollback()

	_, err = tx.Exec("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
	if err != nil {
		panic(err)
	}

	_, err = tx.Exec("INSERT INTO user (id, name, age) VALUES ($1, $2, $3)", 10, "foo", 15)
	if err != nil {
		panic(err)
	}

	r := tx.QueryRow("SELECT id, name, age FROM user WHERE name = $1", "foo")

	var u User
	err = r.Scan(&u.ID, &u.Name, &u.Age)
	if err != nil {
		panic(err)
	}

	fmt.Println(u.ID, u.Name, u.Age)

	err = tx.Commit()
	if err != nil {
		panic(err)
	}

	// Output:
	// 10 foo 15
}

func TestOpen(t *testing.T) {
	dir, err := os.MkdirTemp("", "chai")
	require.NoError(t, err)
	defer os.RemoveAll(dir)

	db, err := sql.Open("chai", filepath.Join(dir, "testdb"))
	require.NoError(t, err)

	_, err = db.Exec(`
		CREATE TABLE tableA (a INTEGER UNIQUE NOT NULL, b DOUBLE PRECISION PRIMARY KEY);
		CREATE TABLE tableB (a TEXT NOT NULL DEFAULT 'hello', PRIMARY KEY (a));
		CREATE TABLE tableC (a INTEGER PRIMARY KEY, b INTEGER);
		CREATE INDEX tableC_a_b_idx ON tableC(a, b);
		CREATE SEQUENCE seqD INCREMENT BY 10 CYCLE MINVALUE 100 NO MAXVALUE START 500;

		INSERT INTO tableB (a) VALUES (1);
		INSERT INTO tableC (a, b) VALUES (1, nextval('seqD'));
	`)
	require.NoError(t, err)

	err = db.Close()
	require.NoError(t, err)

	// ensure tables are loaded properly
	db, err = sql.Open("chai", filepath.Join(dir, "testdb"))
	require.NoError(t, err)
	defer db.Close()

	conn, err := db.Conn(t.Context())
	require.NoError(t, err)
	defer conn.Close()

	rows, err := conn.QueryContext(t.Context(), "SELECT * FROM __chai_catalog")
	require.NoError(t, err)

	want := []string{
		`{"name":"__chai_catalog", "namespace":1, "owner_table_columns":null, "owner_table_name":null, "rowid_sequence_name":null, "sql":"CREATE TABLE __chai_catalog (name TEXT NOT NULL, type TEXT NOT NULL, namespace BIGINT, sql TEXT, rowid_sequence_name TEXT, owner_table_name TEXT, owner_table_columns TEXT, CONSTRAINT __chai_catalog_pk PRIMARY KEY (name))", "type":"table"}`,
		`{"name":"__chai_sequence", "namespace":2, "owner_table_columns":null, "owner_table_name":null, "rowid_sequence_name":null, "sql":"CREATE TABLE __chai_sequence (name TEXT NOT NULL, seq BIGINT, CONSTRAINT __chai_sequence_pk PRIMARY KEY (name))", "type":"table"}`,
		`{"name":"__chai_store_seq", "namespace":null, "owner_table_columns":null, "owner_table_name":"__chai_catalog", "rowid_sequence_name":null, "sql":"CREATE SEQUENCE __chai_store_seq MAXVALUE 9223372036837998591 START WITH 10", "type":"sequence"}`,
		`{"name":"seqD", "namespace":null, "owner_table_columns":null, "owner_table_name":null, "rowid_sequence_name":null, "sql":"CREATE SEQUENCE seqD INCREMENT BY 10 MINVALUE 100 START WITH 500 CYCLE", "type":"sequence"}`,
		`{"name":"tableA", "namespace":10, "owner_table_columns":null, "owner_table_name":null, "rowid_sequence_name":null, "sql":"CREATE TABLE tableA (a INTEGER NOT NULL, b DOUBLE PRECISION NOT NULL, CONSTRAINT tableA_a_unique UNIQUE (a), CONSTRAINT tableA_pk PRIMARY KEY (b))", "type":"table"}`,
		`{"name":"tableA_a_idx", "namespace":11, "owner_table_columns":"a", "owner_table_name":"tableA", "rowid_sequence_name":null, "sql":"CREATE UNIQUE INDEX tableA_a_idx ON tableA (a)", "type":"index"}`,
		`{"name":"tableB", "namespace":12, "owner_table_columns":null, "owner_table_name":null, "rowid_sequence_name":null, "sql":"CREATE TABLE tableB (a TEXT NOT NULL DEFAULT 'hello', CONSTRAINT tableB_pk PRIMARY KEY (a))", "type":"table"}`,
		`{"name":"tableC", "namespace":13, "owner_table_columns":null, "owner_table_name":null, "rowid_sequence_name":null, "sql":"CREATE TABLE tableC (a INTEGER NOT NULL, b INTEGER, CONSTRAINT tableC_pk PRIMARY KEY (a))",  "type":"table"}`,
		`{"name":"tableC_a_b_idx", "namespace":14, "owner_table_columns":null, "owner_table_name":"tableC", "rowid_sequence_name":null, "sql":"CREATE INDEX tableC_a_b_idx ON tableC (a, b)", "type":"index"}`,
	}
	testutil.RequireJSONEq(t, rows, want...)

	rows, err = db.Query("SELECT * FROM tableB")
	require.NoError(t, err)
	testutil.RequireJSONEq(t, rows, `{"a": "1"}`)

	rows, err = db.Query("SELECT * FROM __chai_sequence LIMIT 1")
	require.NoError(t, err)
	testutil.RequireJSONEq(t, rows, `{"name":"__chai_store_seq", "seq":14}`)

	rows, err = db.Query("SELECT * FROM __chai_sequence LIMIT 1 OFFSET 1")
	require.NoError(t, err)
	testutil.RequireJSONEq(t, rows, `{"name": "seqD", "seq": 500}`)
}

func TestQuery(t *testing.T) {
	db, err := sql.Open("chai", ":memory:")
	require.NoError(t, err)
	require.NoError(t, err)

	tx, err := db.Begin()
	require.NoError(t, err)

	_, err = tx.Exec(`
		CREATE TABLE test(a INTEGER PRIMARY KEY, b TEXT NOT NULL);
		INSERT INTO test (a, b) VALUES (1, 'foo'), (2, 'bar')
	`)
	require.NoError(t, err)
	require.NoError(t, tx.Commit())

	t.Run("Should return the first row", func(t *testing.T) {
		var a int
		var b string

		err := db.QueryRow("SELECT * FROM test").Scan(&a, &b)
		require.NoError(t, err)
		require.Equal(t, 1, a)
		require.Equal(t, "foo", b)

		tx, err := db.Begin()
		require.NoError(t, err)
		defer tx.Rollback()

		err = tx.QueryRow("SELECT * FROM test").Scan(&a, &b)
		require.NoError(t, err)
		require.Equal(t, 1, a)
		require.Equal(t, "foo", b)
	})

	t.Run("Should return an error if no row", func(t *testing.T) {
		var a int
		var b string
		err := db.QueryRow("SELECT * FROM test WHERE a > 100").Scan(&a, &b)
		require.ErrorIs(t, err, sql.ErrNoRows)
	})
}

func TestIterateDeepCopy(t *testing.T) {
	db, err := sql.Open("chai", ":memory:")
	require.NoError(t, err)
	defer db.Close()

	_, err = db.Exec(`
	CREATE TABLE foo (
		a integer primary key,
		b text not null
	);

	INSERT INTO foo (a, b) VALUES
		(1, 'sample text 1'),
		(2, 'sample text 2');
	`)
	require.NoError(t, err)

	rows, err := db.Query(`SELECT * FROM foo ORDER BY a DESC`)
	require.NoError(t, err)

	type item struct {
		A int
		B string
	}

	var items []*item
	for rows.Next() {
		var i item
		err := rows.Scan(&i.A, &i.B)
		require.NoError(t, err)

		items = append(items, &i)
	}
	require.NoError(t, err)

	require.Equal(t, len(items), 2)
	require.Equal(t, &item{A: 2, B: "sample text 2"}, items[0])
	require.Equal(t, &item{A: 1, B: "sample text 1"}, items[1])
}

func TestWWConcurrency(t *testing.T) {
	db, err := sql.Open("chai", ":memory:")
	require.NoError(t, err)
	defer db.Close()

	_, err = db.Exec(`
	CREATE TABLE foo (
		a integer primary key,
		b text not null
	);`)
	require.NoError(t, err)

	iterations := 100
	concurrency := 8

	var wg sync.WaitGroup
	wg.Add(concurrency)

	for i := range concurrency {
		go func(id int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				_, err := db.Exec(`INSERT INTO foo (a, b) VALUES ($1, $2)`, id*1000+j, fmt.Sprintf("sample text %d", j))
				require.NoError(t, err)
			}
		}(i)
	}

	wg.Wait()
}

func TestRWConcurrency(t *testing.T) {
	db, err := sql.Open("chai", ":memory:")
	require.NoError(t, err)
	defer db.Close()

	_, err = db.Exec(`
	CREATE TABLE foo (
		a integer primary key,
		b text not null
	);

	-- insert sample data
	INSERT INTO foo(a, b) VALUES (1000000, 'sample text 1000000');
	`)
	require.NoError(t, err)

	readers := 8
	writers := 4
	iterations := 100

	var wg sync.WaitGroup
	wg.Add(readers + writers)

	for range readers {
		go func() {
			defer wg.Done()
			for range iterations {
				var a int
				var b string
				err := db.QueryRow(`SELECT * FROM foo WHERE a = 1000000`).Scan(&a, &b)
				require.NoError(t, err)
				require.Equal(t, 1000000, a)
				require.Equal(t, "sample text 1000000", b)
			}
		}()
	}

	for i := range writers {
		go func(id int) {
			defer wg.Done()
			for j := range iterations {
				_, err := db.Exec(`INSERT INTO foo (a, b) VALUES ($1, $2)`, id*1000+j, fmt.Sprintf("sample text %d", j))
				require.NoError(t, err)
			}
		}(i)
	}

	wg.Wait()
}

func TestOrderByConcurrency(t *testing.T) {
	dir := t.TempDir()
	db, err := sql.Open("chai", dir)
	require.NoError(t, err)
	defer db.Close()

	tx, err := db.Begin()
	require.NoError(t, err)
	_, err = tx.Exec(`
	CREATE TABLE foo (
		a integer primary key,
		b int not null
	);`)
	require.NoError(t, err)

	for i := range 10_000 {
		_, err := tx.Exec(`INSERT INTO foo (a, b) VALUES ($1, $2)`, i, i)
		require.NoError(t, err)
	}
	require.NoError(t, tx.Commit())

	iterations := 10
	concurrency := 8

	var wg sync.WaitGroup
	wg.Add(concurrency)

	for i := range concurrency {
		go func(id int) {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				var desc string
				if j%2 == 0 {
					desc = "DESC"
				} else {
					desc = "ASC"
				}
				func() {
					rows, err := db.Query(`SELECT * FROM foo ORDER BY b ` + desc)
					require.NoError(t, err)
					defer rows.Close()

					var count int
					for rows.Next() {
						var a int
						var b int
						err := rows.Scan(&a, &b)
						require.NoError(t, err)
						if desc == "ASC" {
							require.Equal(t, count, a)
							require.Equal(t, count, b)
						} else {
							require.Equal(t, 9_999-count, a)
							require.Equal(t, 9_999-count, b)
						}
						count++
					}
					require.NoError(t, rows.Err())
					require.Equal(t, 10_000, count)
				}()
			}
		}(i)
	}

	wg.Wait()
}

func BenchmarkOrderBy(b *testing.B) {
	dir := b.TempDir()
	db, err := sql.Open("chai", dir)
	require.NoError(b, err)
	defer db.Close()

	tx, err := db.Begin()
	require.NoError(b, err)
	_, err = tx.Exec(`
	CREATE TABLE foo (
		a integer primary key,
		b int not null
	);`)
	require.NoError(b, err)

	for i := range 10_000 {
		_, err := tx.Exec(`INSERT INTO foo (a, b) VALUES ($1, $2)`, i, i)
		require.NoError(b, err)
	}
	require.NoError(b, tx.Commit())

	b.ResetTimer()
	for b.Loop() {
		rows, err := db.Query(`SELECT * FROM foo ORDER BY b`)
		require.NoError(b, err)

		for rows.Next() {
			var a int
			var b int
			_ = rows.Scan(&a, &b)
		}
	}
}
