package coredb

import (
	"context"
	"database/sql"
	"errors"
	"regexp"
	"testing"

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

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

	query, args, err := rebindPositionalQuery(
		`SELECT $1 AS first, $2 AS second, $1 AS first_again, $10 AS tenth`,
		[]any{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
	)
	require.NoError(t, err)
	require.Equal(t, `SELECT ? AS first, ? AS second, ? AS first_again, ? AS tenth`, query)
	require.Equal(t, []any{"one", "two", "one", "ten"}, args)
}

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

	_, _, err := rebindPositionalQuery(`SELECT $2`, []any{"only-one"})
	require.EqualError(t, err, "query placeholder $2 has no argument")
}

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

	_, _, err := rebindPositionalQuery(`SELECT $1`, []any{"used", "unused"})
	require.EqualError(t, err, "query has 2 positional arguments but only references $1")
}

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

	ctx := context.Background()
	closeErr := errors.New("driver close failed")
	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	db := &tidbAdminDB{db: sqlDB}
	mock.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM people`)).
		WillReturnRows(sqlmock.NewRows([]string{"id"}).CloseError(closeErr))

	rows, err := db.Query(ctx, `SELECT id FROM people`)
	require.NoError(t, err)
	require.NoError(t, rows.Err())
	rows.Close()
	require.ErrorIs(t, rows.Err(), closeErr)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	tests := []struct {
		name      string
		options   TxOptions
		isolation sql.IsolationLevel
		readOnly  bool
	}{
		{name: "default"},
		{name: "read uncommitted", options: TxOptions{IsoLevel: ReadUncommitted}, isolation: sql.LevelReadUncommitted},
		{name: "read committed", options: TxOptions{IsoLevel: ReadCommitted}, isolation: sql.LevelReadCommitted},
		{name: "repeatable read", options: TxOptions{IsoLevel: RepeatableRead}, isolation: sql.LevelRepeatableRead},
		{name: "serializable", options: TxOptions{IsoLevel: Serializable}, isolation: sql.LevelSerializable},
		{name: "read write", options: TxOptions{AccessMode: ReadWrite}},
		{name: "read only", options: TxOptions{AccessMode: ReadOnly}, readOnly: true},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			got, err := sqlTxOptionsFromContract(tt.options)
			require.NoError(t, err)
			require.Equal(t, tt.isolation, got.Isolation)
			require.Equal(t, tt.readOnly, got.ReadOnly)
		})
	}
}

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

	_, err := sqlTxOptionsFromContract(TxOptions{IsoLevel: TxIsolationLevel("snapshot")})
	require.EqualError(t, err, `unsupported transaction isolation level "snapshot"`)

	_, err = sqlTxOptionsFromContract(TxOptions{AccessMode: TxAccessMode("deferrable")})
	require.EqualError(t, err, `unsupported transaction access mode "deferrable"`)
}
