package testkit

import (
	"context"
	"database/sql"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

func TestTiDBRollbackDBBeginTxCreatesIncrementingSavepoints(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	rootTx := &rollbackFixtureTxStub{}
	db := BeginTiDBRollbackDB(t, ctx, &rollbackFixtureDBStub{tx: rootTx})

	_, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	_, err = db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)

	require.Equal(t, []string{
		"SAVEPOINT agentteam_test_savepoint_1",
		"SAVEPOINT agentteam_test_savepoint_2",
	}, rootTx.commands())
}

func TestTiDBRollbackDBNestedCommitReleasesSavepoint(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	rootTx := &rollbackFixtureTxStub{}
	db := BeginTiDBRollbackDB(t, ctx, &rollbackFixtureDBStub{tx: rootTx})

	tx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	require.NoError(t, tx.Commit(ctx))

	require.Equal(t, []string{
		"SAVEPOINT agentteam_test_savepoint_1",
		"RELEASE SAVEPOINT agentteam_test_savepoint_1",
	}, rootTx.commands())
}

func TestTiDBRollbackDBNestedRollbackRollsBackAndReleasesSavepoint(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	rootTx := &rollbackFixtureTxStub{}
	db := BeginTiDBRollbackDB(t, ctx, &rollbackFixtureDBStub{tx: rootTx})

	tx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	require.NoError(t, tx.Rollback(ctx))

	require.Equal(t, []string{
		"SAVEPOINT agentteam_test_savepoint_1",
		"ROLLBACK TO SAVEPOINT agentteam_test_savepoint_1",
		"RELEASE SAVEPOINT agentteam_test_savepoint_1",
	}, rootTx.commands())
}

func TestTiDBRollbackDBNestedTxDoubleCloseReturnsError(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	rootTx := &rollbackFixtureTxStub{}
	db := BeginTiDBRollbackDB(t, ctx, &rollbackFixtureDBStub{tx: rootTx})

	tx, err := db.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	require.NoError(t, tx.Commit(ctx))

	err = tx.Rollback(ctx)
	require.EqualError(t, err, "tidb savepoint tx already closed")
	require.Equal(t, []string{
		"SAVEPOINT agentteam_test_savepoint_1",
		"RELEASE SAVEPOINT agentteam_test_savepoint_1",
	}, rootTx.commands())
}

func TestTiDBRollbackDBNestedTxRejectsOptions(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	rootTx := &rollbackFixtureTxStub{}
	db := BeginTiDBRollbackDB(t, ctx, &rollbackFixtureDBStub{tx: rootTx})

	tx, err := db.BeginTx(ctx, coredb.TxOptions{AccessMode: coredb.ReadOnly})
	require.Nil(t, tx)
	require.EqualError(t, err, "tidb rollback db nested transactions do not support tx options")
	require.Empty(t, rootTx.commands())
}

func TestTiDBRollbackDBBootstrapLockReleasesAfterPanic(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	rootTx := &rollbackFixtureTxStub{databaseName: "agentteam_test"}
	db := &tiDBRollbackDB{tx: rootTx}

	require.PanicsWithValue(t, "boom", func() {
		_ = db.WithSystemBootstrapLockTx(ctx, func(coredb.Tx) error {
			panic("boom")
		})
	})
	require.Equal(t, []string{
		"SAVEPOINT agentteam_test_savepoint_1",
		"ROLLBACK TO SAVEPOINT agentteam_test_savepoint_1",
		"RELEASE SAVEPOINT agentteam_test_savepoint_1",
	}, rootTx.commands())
	require.Equal(t, []string{
		`SELECT DATABASE()`,
		`SELECT GET_LOCK(?, ?)`,
		`SELECT RELEASE_LOCK(?)`,
	}, rootTx.queries())
	require.Equal(t, []any{"agentteam_system_bootstrap:agentteam_test", int(15)}, rootTx.queryArgs[1])
	require.Equal(t, []any{"agentteam_system_bootstrap:agentteam_test"}, rootTx.queryArgs[2])
}

func TestTiDBCoreDBFixtureRollbackDBUsesIsolatedDatabasePerCall(t *testing.T) {
	t.Parallel()

	fixture := NewTiDBCoreDBFixture(TiDBTestRuntimeUnistore)
	t.Cleanup(func() {
		cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
		defer cancel()
		require.NoError(t, fixture.Cleanup(cleanupCtx))
	})

	ctx := context.Background()
	first := fixture.RollbackDB(t, ctx)
	second := fixture.RollbackDB(t, ctx)

	var firstDatabase string
	var secondDatabase string
	require.NoError(t, first.QueryRow(ctx, `SELECT DATABASE()`).Scan(&firstDatabase))
	require.NoError(t, second.QueryRow(ctx, `SELECT DATABASE()`).Scan(&secondDatabase))
	require.NotEmpty(t, firstDatabase)
	require.NotEmpty(t, secondDatabase)
	require.NotEqual(t, firstDatabase, secondDatabase)
}

func TestTiDBCoreDBFixtureSerializedRollbackDBReusesCleanDatabase(t *testing.T) {
	fixture := NewTiDBCoreDBFixture(TiDBTestRuntimeUnistore)
	t.Cleanup(func() {
		cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
		defer cancel()
		require.NoError(t, fixture.Cleanup(cleanupCtx))
	})

	ctx := context.Background()
	var firstDatabase string
	t.Run("seed rolls back", func(t *testing.T) {
		db := fixture.SerializedRollbackDB(t, ctx)
		require.NoError(t, db.QueryRow(ctx, `SELECT DATABASE()`).Scan(&firstDatabase))
		_, err := db.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at)
VALUES ('usr_serialized_rollback', 'serialized@example.com', 'serialized', 'Serialized', 'hash', NOW(), NOW())
`)
		require.NoError(t, err)
	})

	t.Run("next transaction sees clean state", func(t *testing.T) {
		db := fixture.SerializedRollbackDB(t, ctx)
		var secondDatabase string
		require.NoError(t, db.QueryRow(ctx, `SELECT DATABASE()`).Scan(&secondDatabase))
		require.Equal(t, firstDatabase, secondDatabase)

		var count int
		require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE id = 'usr_serialized_rollback'`).Scan(&count))
		require.Zero(t, count)
	})
}

func TestTiDBCoreDBFixtureSerializedCommittedDBReusesResetDatabase(t *testing.T) {
	fixture := NewTiDBCoreDBFixture(TiDBTestRuntimeUnistore)
	t.Cleanup(func() {
		cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
		defer cancel()
		require.NoError(t, fixture.Cleanup(cleanupCtx))
	})

	ctx := context.Background()
	reset := func(ctx context.Context, db coredb.DB) error {
		_, err := db.Exec(ctx, `DELETE FROM users WHERE id = 'usr_serialized_committed'`)
		return err
	}
	var firstDatabase string
	t.Run("seed commits", func(t *testing.T) {
		db := fixture.SerializedCommittedDB(t, ctx, reset)
		require.NoError(t, db.QueryRow(ctx, `SELECT DATABASE()`).Scan(&firstDatabase))
		_, err := db.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at)
VALUES ('usr_serialized_committed', 'serialized-committed@example.com', 'serialized', 'Serialized', 'hash', NOW(), NOW())
`)
		require.NoError(t, err)
	})

	t.Run("next caller reuses a clean database", func(t *testing.T) {
		db := fixture.SerializedCommittedDB(t, ctx, reset)
		var secondDatabase string
		require.NoError(t, db.QueryRow(ctx, `SELECT DATABASE()`).Scan(&secondDatabase))
		require.Equal(t, firstDatabase, secondDatabase)

		var count int
		require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE id = 'usr_serialized_committed'`).Scan(&count))
		require.Zero(t, count)
	})
}

type rollbackFixtureDBStub struct {
	tx *rollbackFixtureTxStub
}

func (db *rollbackFixtureDBStub) QueryRow(context.Context, string, ...any) coredb.Row {
	return rollbackFixtureRowStub{}
}

func (db *rollbackFixtureDBStub) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return &rollbackFixtureRowsStub{}, nil
}

func (db *rollbackFixtureDBStub) Exec(context.Context, string, ...any) (coredb.Result, error) {
	return rollbackFixtureResultStub{}, nil
}

func (db *rollbackFixtureDBStub) BeginTx(context.Context, coredb.TxOptions) (coredb.Tx, error) {
	if db.tx == nil {
		return nil, fmt.Errorf("rollback fixture tx stub is nil")
	}
	return db.tx, nil
}

type rollbackFixtureTxStub struct {
	execs        []string
	queryRows    []string
	queryArgs    [][]any
	databaseName string
}

func (tx *rollbackFixtureTxStub) QueryRow(_ context.Context, query string, args ...any) coredb.Row {
	tx.queryRows = append(tx.queryRows, query)
	tx.queryArgs = append(tx.queryArgs, append([]any(nil), args...))
	switch query {
	case `SELECT DATABASE()`:
		return rollbackFixtureRowStub{databaseName: tx.databaseName}
	case `SELECT GET_LOCK(?, ?)`, `SELECT RELEASE_LOCK(?)`:
		return rollbackFixtureRowStub{intResult: sql.NullInt64{Int64: 1, Valid: true}, hasIntResult: true}
	default:
		return rollbackFixtureRowStub{}
	}
}

func (tx *rollbackFixtureTxStub) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return &rollbackFixtureRowsStub{}, nil
}

func (tx *rollbackFixtureTxStub) Exec(_ context.Context, query string, args ...any) (coredb.Result, error) {
	if len(args) > 0 {
		return nil, fmt.Errorf("Exec(%q) received unexpected args: %v", query, args)
	}
	tx.execs = append(tx.execs, query)
	return rollbackFixtureResultStub{}, nil
}

func (tx *rollbackFixtureTxStub) Commit(context.Context) error {
	return nil
}

func (tx *rollbackFixtureTxStub) Rollback(context.Context) error {
	return nil
}

func (tx *rollbackFixtureTxStub) commands() []string {
	return append([]string(nil), tx.execs...)
}

func (tx *rollbackFixtureTxStub) queries() []string {
	return append([]string(nil), tx.queryRows...)
}

type rollbackFixtureRowStub struct {
	databaseName string
	intResult    sql.NullInt64
	hasIntResult bool
}

func (row rollbackFixtureRowStub) Scan(dest ...any) error {
	if len(dest) == 0 {
		return nil
	}
	switch target := dest[0].(type) {
	case *sql.NullString:
		*target = sql.NullString{String: row.databaseName, Valid: row.databaseName != ""}
	case *sql.NullInt64:
		if row.hasIntResult {
			*target = row.intResult
		}
	}
	return nil
}

type rollbackFixtureRowsStub struct{}

func (*rollbackFixtureRowsStub) Close() {}

func (*rollbackFixtureRowsStub) Err() error {
	return nil
}

func (*rollbackFixtureRowsStub) Next() bool {
	return false
}

func (*rollbackFixtureRowsStub) Scan(...any) error {
	return nil
}

type rollbackFixtureResultStub struct{}

func (rollbackFixtureResultStub) RowsAffected() int64 {
	return 0
}
