package database_test

import (
	"fmt"
	"testing"

	"github.com/chaisql/chai/internal/database"
	errs "github.com/chaisql/chai/internal/errors"
	"github.com/chaisql/chai/internal/query/statement"
	"github.com/chaisql/chai/internal/row"
	"github.com/chaisql/chai/internal/testutil"
	"github.com/chaisql/chai/internal/tree"
	"github.com/chaisql/chai/internal/types"
	"github.com/cockroachdb/errors"
	"github.com/stretchr/testify/require"
)

var errDontCommit = errors.New("don't commit please")

func newTestTable(t testing.TB) (*database.Table, func()) {
	t.Helper()

	_, tx, fn := testutil.NewTestTx(t)

	ti := database.TableInfo{
		TableName: "test",
		PrimaryKey: &database.PrimaryKey{
			Columns: []string{"a"},
			Types:   []types.Type{types.TypeText},
		},
		ColumnConstraints: database.MustNewColumnConstraints(
			&database.ColumnConstraint{
				Position: 0,
				Column:   "a",
				Type:     types.TypeText,
				TypeDef:  types.TypeText.Def(),
			},
			&database.ColumnConstraint{
				Position: 0,
				Column:   "b",
				Type:     types.TypeText,
				TypeDef:  types.TypeText.Def(),
			},
		),
		TableConstraints: database.TableConstraints{
			&database.TableConstraint{
				PrimaryKey: true,
				Columns:    []string{"a"},
			},
		},
	}
	return createTable(t, tx, ti), fn
}

func createTable(t testing.TB, tx *database.Transaction, info database.TableInfo) *database.Table {
	t.Helper()

	stmt := statement.CreateTableStmt{Info: info}

	res, err := stmt.Run(&statement.Context{
		Conn: tx.Connection(),
	})
	require.NoError(t, err)
	res.Close()

	tb, err := tx.Catalog.GetTable(tx, stmt.Info.TableName)
	require.NoError(t, err)

	return tb
}

func newRow() *row.ColumnBuffer {
	return row.NewColumnBuffer().
		Add("a", types.NewTextValue("a")).
		Add("b", types.NewTextValue("b"))
}

// TestTableGetRow verifies GetRow behaviour.
func TestTableGetRow(t *testing.T) {
	t.Run("Should fail if not found", func(t *testing.T) {
		tb, cleanup := newTestTable(t)
		defer cleanup()

		r, err := tb.GetRow(tree.NewKey(types.NewIntegerValue(10)))
		require.True(t, errs.IsNotFoundError(err))
		require.Nil(t, r)
	})

	t.Run("Should return the right row", func(t *testing.T) {
		tb, cleanup := newTestTable(t)
		defer cleanup()

		// create two rows
		row1 := newRow()
		row2 := newRow()
		err := row2.Set("a", types.NewTextValue("c"))
		require.NoError(t, err)

		key, _, err := tb.Insert(row1)
		require.NoError(t, err)
		_, _, err = tb.Insert(row2)
		require.NoError(t, err)

		// fetch row1 and make sure it returns the right one
		res, err := tb.GetRow(key)
		require.NoError(t, err)
		v, err := res.Get("a")
		require.NoError(t, err)
		require.Equal(t, "a", types.AsString(v))
	})
}

// TestTableDelete verifies Delete behaviour.
func TestTableDelete(t *testing.T) {
	t.Run("Should not fail if not found", func(t *testing.T) {
		tb, cleanup := newTestTable(t)
		defer cleanup()
		err := tb.Delete(tree.NewKey(types.NewIntegerValue(10)))
		require.NoError(t, err)
	})
}

// TestTableReplace verifies Replace behaviour.
func TestTableReplace(t *testing.T) {
	t.Run("Should fail if not found", func(t *testing.T) {
		tb, cleanup := newTestTable(t)
		defer cleanup()

		_, err := tb.Replace(tree.NewKey(types.NewIntegerValue(10)), newRow())
		require.True(t, errs.IsNotFoundError(err))
	})

	t.Run("Should replace the right row", func(t *testing.T) {
		tb, cleanup := newTestTable(t)
		defer cleanup()

		// create two different objects
		row1 := newRow()
		row2 := row.NewColumnBuffer().
			Add("a", types.NewTextValue("c")).
			Add("b", types.NewTextValue("d"))

		key1, _, err := tb.Insert(row1)
		require.NoError(t, err)
		key2, _, err := tb.Insert(row2)
		require.NoError(t, err)

		// create a third object
		doc3 := row.NewColumnBuffer().
			Add("a", types.NewTextValue("e")).
			Add("b", types.NewTextValue("f"))

		// replace row1 with doc3
		d3, err := tb.Replace(key1, doc3)
		require.NoError(t, err)

		// make sure it replaced it correctly
		res, err := tb.GetRow(key1)
		require.NoError(t, err)
		f, err := res.Get("a")
		require.NoError(t, err)
		require.Equal(t, "e", f.V().(string))

		testutil.RequireRowEqual(t, d3, res)

		// make sure it didn't also replace the other one
		res, err = tb.GetRow(key2)
		require.NoError(t, err)
		f, err = res.Get("a")
		require.NoError(t, err)
		require.Equal(t, "c", f.V().(string))
	})
}

// TestTableTruncate verifies Truncate behaviour.
func TestTableTruncate(t *testing.T) {
	t.Run("Should succeed if table empty", func(t *testing.T) {
		tb, cleanup := newTestTable(t)
		defer cleanup()

		err := tb.Truncate()
		require.NoError(t, err)
	})
}

// BenchmarkTableInsert benchmarks the Insert method with 1, 10, 1000 and 10000 successive insertions.
func BenchmarkTableInsert(b *testing.B) {
	for size := 1; size <= 10000; size *= 10 {
		b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
			var fb row.ColumnBuffer

			for i := int64(0); i < 10; i++ {
				fb.Add(fmt.Sprintf("name-%d", i), types.NewBigintValue(i))
			}

			b.ResetTimer()
			b.StopTimer()
			for i := 0; i < b.N; i++ {
				tb, cleanup := newTestTable(b)

				b.StartTimer()
				for j := 0; j < size; j++ {
					_, _, _ = tb.Insert(&fb)
				}
				b.StopTimer()
				cleanup()
			}
		})
	}
}

// BenchmarkTableScan benchmarks the Scan method with 1, 10, 1000 and 10000 successive insertions.
func BenchmarkTableScan(b *testing.B) {
	for size := 1; size <= 10000; size *= 10 {
		b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
			tb, cleanup := newTestTable(b)
			defer cleanup()

			var fb row.ColumnBuffer

			for i := int64(0); i < 10; i++ {
				fb.Add(fmt.Sprintf("name-%d", i), types.NewBigintValue(i))
			}

			for i := 0; i < size; i++ {
				_, _, err := tb.Insert(&fb)
				require.NoError(b, err)
			}

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				it, err := tb.Iterator(nil)
				require.NoError(b, err)

				for it.First(); it.Valid(); it.Next() {
				}

				it.Close()

				// _ = tb.IterateOnRange(nil, false, func(*tree.Key, database.Row) error {
				// 	return nil
				// })
			}
			b.StopTimer()
		})
	}
}
