package provider

import (
	"context"
	"regexp"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

var providerGitHubClaimColumns = []string{
	"provider_app_id", "workspace_id", "claim_state", "claim_token",
}

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

	const (
		appID       = int64(4242)
		workspaceID = "wsp_claim"
		claimToken  = "request_winner"
	)
	now := time.Date(2026, time.July, 22, 12, 0, 0, 0, time.UTC)

	for _, testCase := range []struct {
		name        string
		state       string
		storedToken string
		wantError   bool
	}{
		{name: "exact patching winner", state: providerGitHubAppClaimStatePatching, storedToken: claimToken},
		{name: "legacy setup is not mutation authority", state: providerGitHubAppClaimStateSetup, storedToken: claimToken, wantError: true},
		{name: "active claim is not a setup lease", state: providerGitHubAppClaimStateActive, wantError: true},
		{name: "different patching token cannot finalize", state: providerGitHubAppClaimStatePatching, storedToken: "request_loser", wantError: true},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			mock, tx, cleanup := newProviderGitHubClaimMockTx(t)
			defer cleanup()

			expectDeleteExpiredProviderGitHubSetupClaims(mock, now)
			mock.ExpectQuery(providerGitHubClaimByAppQueryPattern()).
				WithArgs(appID).
				WillReturnRows(sqlmock.NewRows(providerGitHubClaimColumns).
					AddRow(appID, workspaceID, testCase.state, testCase.storedToken))
			if testCase.state == providerGitHubAppClaimStateSetup {
				mock.ExpectExec(providerGitHubDeleteSetupClaimByAppPattern()).
					WithArgs(appID, providerGitHubAppClaimStateSetup).
					WillReturnResult(sqlmock.NewResult(0, 1))
			}

			err := prepareGitHubAppClaimForConfigSaveInTx(
				context.Background(), tx, appID, workspaceID, claimToken, now,
			)
			if testCase.wantError {
				require.ErrorIs(t, err, ErrConflict)
				return
			}
			require.NoError(t, err)
		})
	}
}

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

	const (
		appID       = int64(4242)
		workspaceID = "wsp_winner"
	)
	now := time.Date(2026, time.July, 22, 12, 1, 0, 0, time.UTC)

	for _, testCase := range []struct {
		name                   string
		appClaim               *providerGitHubAppClaim
		workspaceClaim         *providerGitHubAppClaim
		deleteSetupByApp       bool
		deleteSetupByWorkspace bool
	}{
		{
			name: "same App was reserved by another workspace",
			appClaim: &providerGitHubAppClaim{
				appID: 4242, workspaceID: "wsp_old_candidate", state: providerGitHubAppClaimStateSetup, token: "draft_old",
			},
			deleteSetupByApp: true,
		},
		{
			name: "workspace was reserved for another App",
			workspaceClaim: &providerGitHubAppClaim{
				appID: 4343, workspaceID: workspaceID, state: providerGitHubAppClaimStateSetup, token: "draft_old",
			},
			deleteSetupByWorkspace: true,
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			mock, tx, cleanup := newProviderGitHubClaimMockTx(t)
			defer cleanup()

			expectDeleteExpiredProviderGitHubSetupClaims(mock, now)
			appRows := sqlmock.NewRows(providerGitHubClaimColumns)
			if testCase.appClaim != nil {
				appRows.AddRow(
					testCase.appClaim.appID,
					testCase.appClaim.workspaceID,
					testCase.appClaim.state,
					testCase.appClaim.token,
				)
			}
			mock.ExpectQuery(providerGitHubClaimByAppQueryPattern()).
				WithArgs(appID).
				WillReturnRows(appRows)
			if testCase.deleteSetupByApp {
				mock.ExpectExec(providerGitHubDeleteSetupClaimByAppPattern()).
					WithArgs(appID, providerGitHubAppClaimStateSetup).
					WillReturnResult(sqlmock.NewResult(0, 1))
			}

			workspaceRows := sqlmock.NewRows(providerGitHubClaimColumns)
			if testCase.workspaceClaim != nil {
				workspaceRows.AddRow(
					testCase.workspaceClaim.appID,
					testCase.workspaceClaim.workspaceID,
					testCase.workspaceClaim.state,
					testCase.workspaceClaim.token,
				)
			}
			mock.ExpectQuery(providerGitHubClaimByWorkspaceQueryPattern()).
				WithArgs(workspaceID).
				WillReturnRows(workspaceRows)
			if testCase.deleteSetupByWorkspace {
				mock.ExpectExec(providerGitHubDeleteSetupClaimByWorkspacePattern()).
					WithArgs(workspaceID, providerGitHubAppClaimStateSetup).
					WillReturnResult(sqlmock.NewResult(0, 1))
			}
			mock.ExpectExec(providerGitHubInsertActiveClaimPattern()).
				WithArgs(appID, workspaceID, providerGitHubAppClaimStateActive, now, now).
				WillReturnResult(sqlmock.NewResult(0, 1))

			err := prepareGitHubAppClaimForConfigSaveInTx(
				context.Background(), tx, appID, workspaceID, "", now,
			)
			require.NoError(t, err)
		})
	}
}

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

	const (
		appID       = int64(4242)
		workspaceID = "wsp_candidate"
	)
	now := time.Date(2026, time.July, 22, 12, 2, 0, 0, time.UTC)

	for _, testCase := range []struct {
		name           string
		appClaim       *providerGitHubAppClaim
		workspaceClaim *providerGitHubAppClaim
	}{
		{
			name: "same App is patching for another workspace",
			appClaim: &providerGitHubAppClaim{
				appID: appID, workspaceID: "wsp_winner", state: providerGitHubAppClaimStatePatching, token: "request_winner",
			},
		},
		{
			name: "workspace is patching another App",
			workspaceClaim: &providerGitHubAppClaim{
				appID: 4343, workspaceID: workspaceID, state: providerGitHubAppClaimStatePatching, token: "request_winner",
			},
		},
		{
			name: "same App is active for another workspace",
			appClaim: &providerGitHubAppClaim{
				appID: appID, workspaceID: "wsp_winner", state: providerGitHubAppClaimStateActive,
			},
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			mock, tx, cleanup := newProviderGitHubClaimMockTx(t)
			defer cleanup()

			expectDeleteExpiredProviderGitHubSetupClaims(mock, now)
			appRows := sqlmock.NewRows(providerGitHubClaimColumns)
			if testCase.appClaim != nil {
				appRows.AddRow(
					testCase.appClaim.appID,
					testCase.appClaim.workspaceID,
					testCase.appClaim.state,
					testCase.appClaim.token,
				)
			}
			mock.ExpectQuery(providerGitHubClaimByAppQueryPattern()).
				WithArgs(appID).
				WillReturnRows(appRows)
			if testCase.appClaim == nil {
				workspaceRows := sqlmock.NewRows(providerGitHubClaimColumns)
				workspaceRows.AddRow(
					testCase.workspaceClaim.appID,
					testCase.workspaceClaim.workspaceID,
					testCase.workspaceClaim.state,
					testCase.workspaceClaim.token,
				)
				mock.ExpectQuery(providerGitHubClaimByWorkspaceQueryPattern()).
					WithArgs(workspaceID).
					WillReturnRows(workspaceRows)
			}

			err := prepareGitHubAppClaimForConfigSaveInTx(
				context.Background(), tx, appID, workspaceID, "", now,
			)
			require.ErrorIs(t, err, ErrConflict)
		})
	}
}

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

	const (
		appID       = int64(4242)
		workspaceID = "wsp_candidate"
	)
	now := time.Date(2026, time.July, 22, 12, 3, 0, 0, time.UTC)
	mock, tx, cleanup := newProviderGitHubClaimMockTx(t)
	defer cleanup()

	expectDeleteExpiredProviderGitHubSetupClaims(mock, now)
	mock.ExpectQuery(providerGitHubClaimByAppQueryPattern()).
		WithArgs(appID).
		WillReturnRows(sqlmock.NewRows(providerGitHubClaimColumns))
	mock.ExpectQuery(providerGitHubClaimByWorkspaceQueryPattern()).
		WithArgs(workspaceID).
		WillReturnRows(sqlmock.NewRows(providerGitHubClaimColumns))
	mock.ExpectExec(providerGitHubInsertActiveClaimPattern()).
		WithArgs(appID, workspaceID, providerGitHubAppClaimStateActive, now, now).
		WillReturnError(&mysql.MySQLError{Number: 1062, Message: "duplicate claim"})

	err := prepareGitHubAppClaimForConfigSaveInTx(
		context.Background(), tx, appID, workspaceID, "", now,
	)
	require.ErrorIs(t, err, ErrConflict)
}

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

	const (
		appID       = int64(4242)
		workspaceID = "wsp_claim"
		claimToken  = "request_winner"
	)
	now := time.Date(2026, time.July, 22, 12, 4, 0, 0, time.UTC)

	for _, testCase := range []struct {
		name         string
		rowsAffected int64
		wantError    bool
	}{
		{name: "patching claim is promoted", rowsAffected: 1},
		{name: "setup or mismatched claim cannot be promoted", wantError: true},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			mock, tx, cleanup := newProviderGitHubClaimMockTx(t)
			defer cleanup()

			mock.ExpectExec(providerGitHubActivatePatchingClaimPattern()).
				WithArgs(
					providerGitHubAppClaimStateActive,
					now,
					appID,
					workspaceID,
					providerGitHubAppClaimStatePatching,
					claimToken,
				).
				WillReturnResult(sqlmock.NewResult(0, testCase.rowsAffected))

			err := activateGitHubAppClaimAfterConfigSaveInTx(
				context.Background(), tx, appID, workspaceID, claimToken, now,
			)
			if testCase.wantError {
				require.ErrorIs(t, err, ErrConflict)
				return
			}
			require.NoError(t, err)
		})
	}
}

func newProviderGitHubClaimMockTx(t *testing.T) (sqlmock.Sqlmock, coredb.Tx, func()) {
	t.Helper()

	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	mock.ExpectBegin()
	tx, err := coredb.WrapSQLDB(sqlDB).BeginTx(context.Background(), coredb.TxOptions{})
	require.NoError(t, err)

	return mock, tx, func() {
		t.Helper()
		mock.ExpectRollback()
		require.NoError(t, tx.Rollback(context.Background()))
		mock.ExpectClose()
		require.NoError(t, sqlDB.Close())
		require.NoError(t, mock.ExpectationsWereMet())
	}
}

func expectDeleteExpiredProviderGitHubSetupClaims(mock sqlmock.Sqlmock, now time.Time) {
	mock.ExpectExec(providerGitHubDeleteExpiredSetupClaimsPattern()).
		WithArgs(providerGitHubAppClaimStateSetup, now).
		WillReturnResult(sqlmock.NewResult(0, 0))
}

func providerGitHubDeleteExpiredSetupClaimsPattern() string {
	return regexp.QuoteMeta(`
DELETE FROM service_provider_workspace_claims
WHERE claim_state = ?
  AND expires_at IS NOT NULL
  AND expires_at < ?
`)
}

func providerGitHubClaimByAppQueryPattern() string {
	return regexp.QuoteMeta(`
SELECT provider_app_id, workspace_id, claim_state, claim_token
FROM service_provider_workspace_claims
WHERE provider_app_id = ?
LIMIT 1
FOR UPDATE
`)
}

func providerGitHubClaimByWorkspaceQueryPattern() string {
	return regexp.QuoteMeta(`
SELECT provider_app_id, workspace_id, claim_state, claim_token
FROM service_provider_workspace_claims
WHERE workspace_id = ?
LIMIT 1
FOR UPDATE
`)
}

func providerGitHubDeleteSetupClaimByAppPattern() string {
	return regexp.QuoteMeta(`
DELETE FROM service_provider_workspace_claims
WHERE provider_app_id = ?
  AND claim_state = ?
`)
}

func providerGitHubDeleteSetupClaimByWorkspacePattern() string {
	return regexp.QuoteMeta(`
DELETE FROM service_provider_workspace_claims
WHERE workspace_id = ?
  AND claim_state = ?
`)
}

func providerGitHubInsertActiveClaimPattern() string {
	return regexp.QuoteMeta(`
INSERT INTO service_provider_workspace_claims (
    provider_app_id, workspace_id, claim_state, claim_token,
    expires_at, created_at, updated_at
) VALUES (?, ?, ?, '', NULL, ?, ?)
`)
}

func providerGitHubActivatePatchingClaimPattern() string {
	return regexp.QuoteMeta(`
UPDATE service_provider_workspace_claims
SET claim_state = ?,
    claim_token = '',
    expires_at = NULL,
    updated_at = ?
WHERE provider_app_id = ?
  AND workspace_id = ?
	AND claim_state = ?
	AND claim_token = ?
`)
}
