package coredb

import (
	"context"
	"database/sql"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"
)

type bootstrapLockReporterDB struct {
	queries []string
	args    [][]any
	dbName  string

	acquireResult *sql.NullInt64
	releaseResult *sql.NullInt64
}

func (db *bootstrapLockReporterDB) Database() string {
	return "tidb"
}

func (db *bootstrapLockReporterDB) QueryRow(_ context.Context, query string, args ...any) Row {
	db.queries = append(db.queries, query)
	copiedArgs := append([]any(nil), args...)
	db.args = append(db.args, copiedArgs)
	result := db.acquireResult
	if query == `SELECT RELEASE_LOCK(?)` {
		result = db.releaseResult
	}
	return bootstrapLockRow{dbName: db.dbName, intResult: result}
}

func (*bootstrapLockReporterDB) Query(context.Context, string, ...any) (Rows, error) {
	return &contractRowsStub{}, nil
}

func (*bootstrapLockReporterDB) Exec(context.Context, string, ...any) (Result, error) {
	return contractResultStub{}, nil
}

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

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

type bootstrapLockRow struct {
	dbName    string
	intResult *sql.NullInt64
}

type unsupportedBootstrapLockTx struct{}

func (*unsupportedBootstrapLockTx) QueryRow(context.Context, string, ...any) Row {
	return contractRowStub{}
}

func (*unsupportedBootstrapLockTx) Query(context.Context, string, ...any) (Rows, error) {
	return &contractRowsStub{}, nil
}

func (*unsupportedBootstrapLockTx) Exec(context.Context, string, ...any) (Result, error) {
	return contractResultStub{}, nil
}

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

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

func (row bootstrapLockRow) Scan(dest ...any) error {
	switch target := dest[0].(type) {
	case *sql.NullString:
		*target = sql.NullString{String: row.dbName, Valid: row.dbName != ""}
	case *sql.NullInt64:
		result := sql.NullInt64{Int64: 1, Valid: true}
		if row.intResult != nil {
			result = *row.intResult
		}
		*target = result
	default:
		return sql.ErrNoRows
	}
	return nil
}

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

	db := &bootstrapLockReporterDB{}
	release, err := AcquireSystemBootstrapLock(context.Background(), db)
	require.NoError(t, err)
	require.NoError(t, release(context.Background()))
	require.Equal(t, []string{
		`SELECT DATABASE()`,
		`SELECT GET_LOCK(?, ?)`,
		`SELECT RELEASE_LOCK(?)`,
	}, db.queries)
	require.Equal(t, []any{"agentteam_system_bootstrap", int(systemBootstrapLockWait / time.Second)}, db.args[1])
	require.Equal(t, []any{"agentteam_system_bootstrap"}, db.args[2])
}

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

	db := &bootstrapLockReporterDB{dbName: "agentteam_tidb_test_123"}
	release, err := AcquireSystemBootstrapLock(context.Background(), db)
	require.NoError(t, err)
	require.NoError(t, release(context.Background()))

	require.Equal(t, []string{
		`SELECT DATABASE()`,
		`SELECT GET_LOCK(?, ?)`,
		`SELECT RELEASE_LOCK(?)`,
	}, db.queries)
	require.Equal(t, []any{"agentteam_system_bootstrap:agentteam_tidb_test_123", int(systemBootstrapLockWait / time.Second)}, db.args[1])
	require.Equal(t, []any{"agentteam_system_bootstrap:agentteam_tidb_test_123"}, db.args[2])
}

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

	dbName := "agentteam_tidb_test_" + strings.Repeat("x", 80)
	db := &bootstrapLockReporterDB{dbName: dbName}
	release, err := AcquireSystemBootstrapLock(context.Background(), db)
	require.NoError(t, err)
	require.NoError(t, release(context.Background()))

	lockName, ok := db.args[1][0].(string)
	require.True(t, ok)
	require.LessOrEqual(t, len(lockName), 64)
	require.NotContains(t, lockName, dbName)
	require.Contains(t, lockName, "agentteam_system_bootstrap:")
}

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

	release, err := AcquireSystemBootstrapLock(context.Background(), &unsupportedBootstrapLockTx{})
	require.Nil(t, release)
	require.EqualError(t, err, "bootstrap lock is not implemented for *coredb.unsupportedBootstrapLockTx")
}

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

	tests := []struct {
		name    string
		result  sql.NullInt64
		wantErr string
	}{
		{
			name:    "timed out",
			result:  sql.NullInt64{Int64: 0, Valid: true},
			wantErr: "acquire tidb bootstrap lock: timed out",
		},
		{
			name:    "null",
			result:  sql.NullInt64{},
			wantErr: "acquire tidb bootstrap lock: returned NULL",
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			db := &bootstrapLockReporterDB{acquireResult: &tt.result}
			release, err := AcquireSystemBootstrapLock(context.Background(), db)
			require.Nil(t, release)
			require.EqualError(t, err, tt.wantErr)
		})
	}
}

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

	tests := []struct {
		name    string
		result  sql.NullInt64
		wantErr string
	}{
		{
			name:    "not owner",
			result:  sql.NullInt64{Int64: 0, Valid: true},
			wantErr: "release tidb bootstrap lock: current session did not release lock (result 0)",
		},
		{
			name:    "null",
			result:  sql.NullInt64{},
			wantErr: "release tidb bootstrap lock: returned NULL",
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			db := &bootstrapLockReporterDB{releaseResult: &tt.result}
			release, err := AcquireSystemBootstrapLock(context.Background(), db)
			require.NoError(t, err)
			require.EqualError(t, release(context.Background()), tt.wantErr)
		})
	}
}

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

	ctx := context.Background()
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	db := WrapSQLDB(sqlDB)
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT DATABASE()`)).
		WillReturnRows(sqlmock.NewRows([]string{"DATABASE()"}).AddRow("agentteam_test"))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT GET_LOCK(?, ?)`)).
		WithArgs("agentteam_system_bootstrap:agentteam_test", int(systemBootstrapLockWait/time.Second)).
		WillReturnRows(sqlmock.NewRows([]string{"GET_LOCK"}).AddRow(1))
	mock.ExpectBegin()
	mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO bootstrap_probe (id) VALUES (?)`)).
		WithArgs("admin").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT RELEASE_LOCK(?)`)).
		WithArgs("agentteam_system_bootstrap:agentteam_test").
		WillReturnRows(sqlmock.NewRows([]string{"RELEASE_LOCK"}).AddRow(1))

	err = WithSystemBootstrapLockTx(ctx, db, func(tx Tx) error {
		_, err := tx.Exec(ctx, `INSERT INTO bootstrap_probe (id) VALUES ($1)`, "admin")
		return err
	})
	require.NoError(t, err)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	db := WrapSQLDB(sqlDB)
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT DATABASE()`)).
		WillReturnRows(sqlmock.NewRows([]string{"DATABASE()"}).AddRow("agentteam_test"))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT GET_LOCK(?, ?)`)).
		WithArgs("agentteam_system_bootstrap:agentteam_test", int(systemBootstrapLockWait/time.Second)).
		WillReturnRows(sqlmock.NewRows([]string{"GET_LOCK"}).AddRow(1))
	mock.ExpectBegin()
	mock.ExpectCommit()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT RELEASE_LOCK(?)`)).
		WithArgs("agentteam_system_bootstrap:agentteam_test").
		WillReturnRows(sqlmock.NewRows([]string{"RELEASE_LOCK"}).AddRow(1))

	err = WithSystemBootstrapLockTx(nil, db, func(Tx) error {
		return nil
	})
	require.NoError(t, err)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	ctx := context.Background()
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	db := WrapSQLDB(sqlDB)
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT DATABASE()`)).
		WillReturnRows(sqlmock.NewRows([]string{"DATABASE()"}).AddRow("agentteam_test"))
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT GET_LOCK(?, ?)`)).
		WithArgs("agentteam_system_bootstrap:agentteam_test", int(systemBootstrapLockWait/time.Second)).
		WillReturnRows(sqlmock.NewRows([]string{"GET_LOCK"}).AddRow(1))
	mock.ExpectBegin()
	mock.ExpectRollback()
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT RELEASE_LOCK(?)`)).
		WithArgs("agentteam_system_bootstrap:agentteam_test").
		WillReturnRows(sqlmock.NewRows([]string{"RELEASE_LOCK"}).AddRow(1))

	require.PanicsWithValue(t, "boom", func() {
		_ = WithSystemBootstrapLockTx(ctx, db, func(Tx) error {
			panic("boom")
		})
	})
	require.NoError(t, mock.ExpectationsWereMet())
}
