package connector_oauth

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

	"github.com/DATA-DOG/go-sqlmock"
)

type authorizationPrimaryError struct{}

func (*authorizationPrimaryError) Error() string { return "authorization primary failure" }

func TestOAuthPessimisticTransactionSanitizesCleanupErrors(t *testing.T) {
	for _, test := range []struct {
		name      string
		bodyErr   error
		commitErr error
	}{
		{name: "transaction body and cleanup", bodyErr: &authorizationPrimaryError{}},
		{name: "commit and cleanup", commitErr: errors.New("commit primary failure")},
		{name: "confirmed commit cleanup"},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
			if err != nil {
				t.Fatal(err)
			}
			defer db.Close()
			mock.ExpectExec(regexp.QuoteMeta("SET SESSION tidb_txn_mode = 'pessimistic'")).WillReturnResult(sqlmock.NewResult(0, 0))
			mock.ExpectBegin()
			if test.bodyErr != nil {
				mock.ExpectRollback()
			} else {
				commit := mock.ExpectCommit()
				if test.commitErr != nil {
					commit.WillReturnError(test.commitErr)
				}
			}
			mock.ExpectExec(regexp.QuoteMeta("SET SESSION tidb_txn_mode = DEFAULT")).WillReturnError(errors.New("cleanup raw driver-secret"))
			err = withPessimisticTx(context.Background(), db, func(*sql.Tx) error { return test.bodyErr })
			primary := test.bodyErr
			if primary == nil {
				primary = test.commitErr
			}
			if primary != nil && !errors.Is(err, primary) {
				t.Fatalf("primary error was not preserved: %v", err)
			}
			if !errors.Is(err, errOAuthRepositoryTransactionCleanup) || strings.Contains(err.Error(), "driver-secret") || strings.Contains(err.Error(), "cleanup raw") {
				t.Fatalf("sanitized cleanup error = %v", err)
			}
			if test.bodyErr != nil {
				var typedPrimary *authorizationPrimaryError
				if !errors.As(err, &typedPrimary) {
					t.Fatalf("typed primary error was not preserved: %v", err)
				}
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestSQLRepositoryReleaseAuthorizationLocksUserAndUsesExactConsumedPredicate(t *testing.T) {
	for _, consumed := range []bool{false, true} {
		t.Run(fmt.Sprintf("consumed=%v", consumed), func(t *testing.T) {
			db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
			if err != nil {
				t.Fatal(err)
			}
			defer db.Close()
			mock.ExpectExec(regexp.QuoteMeta("SET SESSION tidb_txn_mode = 'pessimistic'")).WillReturnResult(sqlmock.NewResult(0, 0))
			mock.ExpectBegin()
			mock.ExpectQuery(regexp.QuoteMeta("SELECT Id FROM User WHERE Id = ? LIMIT 1 FOR UPDATE")).WithArgs("user-1").WillReturnRows(sqlmock.NewRows([]string{"Id"}).AddRow("user-1"))
			predicate := "ConsumedAt IS NULL"
			if consumed {
				predicate = "ConsumedAt IS NOT NULL"
			}
			mock.ExpectExec(`DELETE FROM _System_ConnectedAuthorizationRequest[\s\S]+Id = \? AND UserId = \? AND ConnectorKey = \? AND ExpectedGeneration = \?[\s\S]+`+predicate).
				WithArgs("request-1", "user-1", "provider-a", int64(2)).
				WillReturnResult(sqlmock.NewResult(0, 0))
			mock.ExpectCommit()
			mock.ExpectExec(regexp.QuoteMeta("SET SESSION tidb_txn_mode = DEFAULT")).WillReturnResult(sqlmock.NewResult(0, 0))

			repository, _ := NewSQLRepository(db)
			outcome, err := repository.releaseAuthorization(context.Background(), authorizationRecord{ID: "request-1", UserID: "user-1", ConnectorKey: "provider-a", ExpectedGeneration: 2}, consumed)
			if err != nil || !outcome.deleteCommitConfirmed {
				t.Fatalf("releaseAuthorization: outcome=%+v err=%v", outcome, err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestSQLRepositoryReservesAuthorizationUnderUserRowLock(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	now := time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)
	record := authorizationRecord{ID: "request-1", StateDigest: "digest", UserID: "user-1", ConnectorKey: "provider-a", RedirectURI: "https://callback.invalid/oauth", RequestedScopes: "scope.read", ExpiresAt: now.Add(time.Minute), CreatedDate: now, LastModifiedDate: now}

	mock.ExpectExec(regexp.QuoteMeta("SET SESSION tidb_txn_mode = 'pessimistic'")).WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectBegin()
	mock.ExpectQuery(regexp.QuoteMeta("SELECT Id FROM User WHERE Id = ? LIMIT 1 FOR UPDATE")).WithArgs("user-1").WillReturnRows(sqlmock.NewRows([]string{"Id"}).AddRow("user-1"))
	mock.ExpectQuery(`SELECT Id, StateDigest, UserId, ConnectorKey`).WithArgs("user-1", "provider-a").WillReturnError(sql.ErrNoRows)
	mock.ExpectQuery(`SELECT Id, UserId, ConnectorKey, Generation`).WithArgs("user-1", "provider-a").WillReturnError(sql.ErrNoRows)
	mock.ExpectExec(`INSERT INTO _System_ConnectedAuthorizationRequest`).WithArgs(
		"request-1", "digest", "user-1", "provider-a", int64(1), "https://callback.invalid/oauth",
		"sealed-verifier", "scope.read", record.ExpiresAt, EffectNone, now, now,
	).WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()
	mock.ExpectExec(regexp.QuoteMeta("SET SESSION tidb_txn_mode = DEFAULT")).WillReturnResult(sqlmock.NewResult(0, 0))

	repository, _ := NewSQLRepository(db)
	reservation, err := repository.reserveAuthorization(context.Background(), record, func(generation int64) (string, error) {
		if generation != 1 {
			t.Fatalf("generation = %d", generation)
		}
		return "sealed-verifier", nil
	})
	if err != nil || !reservation.localCommitConfirmed {
		t.Fatalf("reserveAuthorization: outcome=%+v err=%v", reservation, err)
	}
	if reservation.record.ExpectedGeneration != 1 {
		t.Fatalf("ExpectedGeneration = %d", reservation.record.ExpectedGeneration)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
