package coredb

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

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

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

	require.True(t, IsNotFound(sql.ErrNoRows))
}

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

	constraint, ok := UniqueViolationConstraint(&mysql.MySQLError{
		Number:  1062,
		Message: "Duplicate entry 'project-one' for key 'workspaces_slug_key'",
	})
	require.True(t, ok)
	require.Equal(t, "workspaces_slug_key", constraint)
}

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

	constraint, ok := UniqueViolationConstraint(&mysql.MySQLError{
		Number:  1062,
		Message: "Duplicate entry 'ws_123-thread-0000002a' for key 'channels.channels_workspace_slug_key'",
	})
	require.True(t, ok)
	require.Equal(t, "channels_workspace_slug_key", constraint)
}

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

	require.False(t, IsUniqueViolation(nil))
	require.False(t, IsUniqueViolation(errors.New("duplicate entry")))
	require.False(t, IsUniqueViolation(&mysql.MySQLError{Number: 1452, Message: "foreign key failed"}))
}

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

	tests := []struct {
		name string
		err  error
	}{
		{name: "nil", err: nil},
		{name: "not mysql", err: errors.New("duplicate entry")},
		{name: "not duplicate", err: &mysql.MySQLError{Number: 1452, Message: "foreign key failed"}},
		{name: "missing marker", err: &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'x'"}},
		{name: "empty key", err: &mysql.MySQLError{Number: 1062, Message: "Duplicate entry 'x' for key ''"}},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			constraint, ok := UniqueViolationConstraint(tt.err)
			require.False(t, ok)
			require.Empty(t, constraint)
		})
	}
}

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

	constraint, ok := UniqueViolationConstraint(&mysql.MySQLError{
		Number:  1062,
		Message: "Duplicate entry 'project-one' for key '`workspaces`.`workspaces_slug_key`'",
	})
	require.True(t, ok)
	require.Equal(t, "workspaces_slug_key", constraint)
}

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

	require.True(t, IsForeignKeyViolation(&mysql.MySQLError{Number: 1451, Message: "parent row"}))
	require.True(t, IsForeignKeyViolation(&mysql.MySQLError{Number: 1452, Message: "child row"}))
	require.False(t, IsForeignKeyViolation(&mysql.MySQLError{Number: 1062, Message: "duplicate entry"}))
}

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

	admin := NewAdmin()
	require.True(t, admin.ClassifyRetryable(&mysql.MySQLError{Number: 1213, Message: "deadlock found"}))
	require.True(t, admin.ClassifyRetryable(&mysql.MySQLError{Number: 1205, Message: "lock wait timeout exceeded"}))
	require.False(t, admin.ClassifyRetryable(&mysql.MySQLError{Number: 1062, Message: "duplicate entry"}))
}

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

	admin := NewAdmin()

	_, err := admin.Open(context.Background(), "", time.Second)
	require.EqualError(t, err, "tidb dsn is required")
}

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

	db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
	require.NoError(t, err)
	mock.ExpectPing()
	mock.ExpectClose()

	var gotDriverName string
	var gotDataSourceName string
	admin := NewAdminWithOptions(WithSQLOpener(func(driverName string, dataSourceName string) (*sql.DB, error) {
		gotDriverName = driverName
		gotDataSourceName = dataSourceName
		return db, nil
	}))

	opened, err := admin.Open(context.Background(), "user:pass@tcp(127.0.0.1:4000)/agentteam", time.Second)
	require.NoError(t, err)
	require.Equal(t, "mysql", gotDriverName)
	require.Contains(t, gotDataSourceName, "parseTime=true")
	require.Contains(t, gotDataSourceName, "time_zone=%27%2B00%3A00%27")

	dataDB, ok := opened.(DB)
	require.True(t, ok)
	require.Same(t, db, RawSQLDB(dataDB))
	opened.Close()
	require.NoError(t, mock.ExpectationsWereMet())
	require.False(t, strings.Contains(gotDataSourceName, "loc=Local"))
}

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

	require.Nil(t, WrapSQLDB(nil))
}

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

	wrapped := &tidbAdminDB{}

	var adminDB AdminDB = wrapped
	var dataDB DB = wrapped

	require.NotNil(t, adminDB)
	require.NotNil(t, dataDB)
	require.EqualError(t, adminDB.Ping(context.Background()), "tidb db is nil")
}
