package coredb

import (
	"context"
	"regexp"
	"testing"

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

type sqlxAdapterPerson struct {
	ID   int    `db:"id"`
	Name string `db:"name"`
}

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

	var _ AdminDB = (*sqlxDB)(nil)
	var _ DB = (*sqlxDB)(nil)
	var _ Tx = (*sqlxTx)(nil)
	var _ Getter = (*sqlxDB)(nil)
	var _ Selector = (*sqlxDB)(nil)
	var _ NamedExecer = (*sqlxDB)(nil)
	var _ Getter = (*sqlxTx)(nil)
	var _ Selector = (*sqlxTx)(nil)
	var _ NamedExecer = (*sqlxTx)(nil)
}

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

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

	wrapped := WrapSQLDB(sqlDB)
	require.Same(t, sqlDB, RawSQLDB(wrapped))
	require.True(t, IsTiDB(wrapped))

	_, ok := wrapped.(Getter)
	require.True(t, ok)
	_, ok = wrapped.(Selector)
	require.True(t, ok)
	_, ok = wrapped.(NamedExecer)
	require.True(t, ok)

	require.NoError(t, mock.ExpectationsWereMet())
}

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

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

	db := newSQLXDB(sqlDB)
	mock.ExpectQuery(regexp.QuoteMeta("SELECT id, name FROM people WHERE id = ?")).
		WithArgs(7).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "Ada"))

	var got sqlxAdapterPerson
	require.NoError(t, db.Get(ctx, &got, "SELECT id, name FROM people WHERE id = $1", 7))
	require.Equal(t, sqlxAdapterPerson{ID: 7, Name: "Ada"}, got)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

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

	db := newSQLXDB(sqlDB)
	mock.ExpectQuery(regexp.QuoteMeta("SELECT id, name FROM people WHERE id IN (?, ?) ORDER BY id")).
		WithArgs(1, 2).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).
			AddRow(1, "Ada").
			AddRow(2, "Grace"))

	var got []sqlxAdapterPerson
	require.NoError(t, db.Select(ctx, &got, "SELECT id, name FROM people WHERE id IN ($1, $2) ORDER BY id", 1, 2))
	require.Equal(t, []sqlxAdapterPerson{
		{ID: 1, Name: "Ada"},
		{ID: 2, Name: "Grace"},
	}, got)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

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

	db := newSQLXDB(sqlDB)
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO people (id, name) VALUES (?, ?)")).
		WithArgs(9, "Linus").
		WillReturnResult(sqlmock.NewResult(0, 2))

	result, err := db.NamedExec(ctx,
		"INSERT INTO people (id, name) VALUES (:id, :name)",
		sqlxAdapterPerson{ID: 9, Name: "Linus"},
	)
	require.NoError(t, err)
	require.Equal(t, int64(2), result.RowsAffected())
	require.NoError(t, mock.ExpectationsWereMet())
}

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

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

	db := newSQLXDB(sqlDB)
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta("SELECT id, name FROM people WHERE id = ?")).
		WithArgs(3).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(3, "Margaret"))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT id, name FROM people ORDER BY id")).
		WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(3, "Margaret"))
	mock.ExpectExec(regexp.QuoteMeta("UPDATE people SET name = ? WHERE id = ?")).
		WithArgs("Maggie", 3).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

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

	getter, ok := tx.(Getter)
	require.True(t, ok)
	var person sqlxAdapterPerson
	require.NoError(t, getter.Get(ctx, &person, "SELECT id, name FROM people WHERE id = $1", 3))
	require.Equal(t, sqlxAdapterPerson{ID: 3, Name: "Margaret"}, person)

	selector, ok := tx.(Selector)
	require.True(t, ok)
	var people []sqlxAdapterPerson
	require.NoError(t, selector.Select(ctx, &people, "SELECT id, name FROM people ORDER BY id"))
	require.Equal(t, []sqlxAdapterPerson{{ID: 3, Name: "Margaret"}}, people)

	namedExecer, ok := tx.(NamedExecer)
	require.True(t, ok)
	result, err := namedExecer.NamedExec(ctx,
		"UPDATE people SET name = :name WHERE id = :id",
		sqlxAdapterPerson{ID: 3, Name: "Maggie"},
	)
	require.NoError(t, err)
	require.Equal(t, int64(1), result.RowsAffected())

	require.NoError(t, tx.Commit(ctx))
	require.NoError(t, mock.ExpectationsWereMet())
}
