package app

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "github_admin_verify_pending", time.Now().UTC())
	member := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "github_admin_verify_pending_member", time.Now().UTC())
	pending := requestVerifiedGitHubInstallationForTests(t, ctx, store, member.UserID, admin.WorkspaceID, "member proposal")

	lease, err := store.BeginGitHubAppSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, GitHubSetupVerificationInput{
		Mode:   GitHubAppSetupModeAdmin,
		Config: validGitHubAppConfigParams(),
	})
	require.NoError(t, err)
	require.NotEmpty(t, lease.ID)
	require.NoError(t, store.MarkGitHubSetupVerificationProviderMutationStartedForUser(
		ctx, admin.UserID, admin.WorkspaceID, lease.ID,
	))
	view, err := store.FinalizeGitHubSetupVerificationForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, lease.ID)
	require.NoError(t, err)
	require.True(t, view.Registered)

	stillPending, err := store.GetAppInstallationRequest(ctx, admin.WorkspaceID, pending.ID)
	require.NoError(t, err)
	require.Equal(t, AppInstallationRequestStatusPending, stillPending.Status)
	_, err = store.ApproveAppInstallationRequestForUser(ctx, admin.UserID, admin.WorkspaceID, pending.ID, DecideAppInstallationRequestParams{})
	require.ErrorIs(t, err, ErrConflict)
	require.ErrorIs(t, err, ErrAppInstallationApprovalStale)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "github_requester_mutation_disabled", time.Now().UTC())
	memberA := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "github_requester_mutation_a", time.Now().UTC())
	memberB := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "github_requester_mutation_b", time.Now().UTC())
	params := validGitHubAppConfigParams()

	draftA, err := store.SaveVerifiedGitHubSetupProposalForUser(ctx, memberA.UserID, admin.WorkspaceID, params)
	require.NoError(t, err)
	draftB, err := store.SaveVerifiedGitHubSetupProposalForUser(ctx, memberB.UserID, admin.WorkspaceID, params)
	require.NoError(t, err)
	require.NotEqual(t, draftA.ID, draftB.ID)

	_, err = store.BeginGitHubAppSetupVerificationForUser(ctx, memberA.UserID, admin.WorkspaceID, GitHubSetupVerificationInput{
		Mode: GitHubAppSetupModeRequester, Config: params,
	})
	require.ErrorIs(t, err, ErrConflict)
	require.ErrorContains(t, err, "setup proposal")
	_, _, _, err = store.ResumeGitHubSetupVerificationForUser(
		ctx, memberA.UserID, admin.WorkspaceID, GitHubAppSetupModeRequester,
	)
	require.ErrorIs(t, err, ErrConflict)
	_, err = store.FinalizeGitHubSetupVerificationForRequester(
		ctx, memberA.UserID, admin.WorkspaceID, "ghverify_unreachable",
	)
	require.ErrorIs(t, err, ErrConflict)

	var readyCount, verifyingCount, claimCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT
  SUM(CASE WHEN status = 'setup_ready' THEN 1 ELSE 0 END),
  SUM(CASE WHEN status IN ('setup_verifying', 'setup_patching') THEN 1 ELSE 0 END)
FROM service_installation_requests
WHERE workspace_id = $1 AND service_definition_id = $2
`, admin.WorkspaceID, registry.GitHubAppID).Scan(&readyCount, &verifyingCount))
	require.Equal(t, 2, readyCount)
	require.Zero(t, verifyingCount, "requesters cannot enter a pre-approval provider-mutation saga")
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_provider_workspace_claims WHERE workspace_id = $1`, admin.WorkspaceID).Scan(&claimCount))
	require.Zero(t, claimCount)

	// An upgrade may encounter a requester verification row written by an older
	// binary. The mutation boundary itself must reject that legacy row rather
	// than relying only on Begin/Resume being unreachable in the new API.
	_, err = pool.Exec(ctx, `
UPDATE service_installation_requests
SET status = $1,
    request_reason = $2
WHERE id = $3
  AND workspace_id = $4
`, AppInstallationRequestStatusSetupVerifying, GitHubAppSetupModeRequester,
		draftA.ID, admin.WorkspaceID)
	require.NoError(t, err)
	require.ErrorIs(t, store.MarkGitHubSetupVerificationProviderMutationStartedForUser(
		ctx, memberA.UserID, admin.WorkspaceID, draftA.ID,
	), ErrConflict)
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_provider_workspace_claims WHERE workspace_id = $1`, admin.WorkspaceID).Scan(&claimCount))
	require.Zero(t, claimCount, "legacy requester verification rows cannot acquire a provider-mutation claim")
}

func TestGitHubAdminVerificationFinalizesOnlyAgainstPinnedActiveConfigSnapshot(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "github_verify_admin_cas", time.Now().UTC())
	activeParams := validGitHubAppConfigParams()
	_, err := store.FinalizeGitHubAppSetupForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, activeParams)
	require.NoError(t, err)
	active, found, err := store.Provider().LoadGitHubAppConfigForWorkspace(ctx, admin.WorkspaceID)
	require.NoError(t, err)
	require.True(t, found)

	nextParams := activeParams
	nextParams.AppName = "Must Not Commit After CAS Drift"
	lease, err := store.BeginGitHubAppSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, GitHubSetupVerificationInput{
		Mode:                    GitHubAppSetupModeAdmin,
		Config:                  nextParams,
		ExpectedConfigID:        active.ID,
		ExpectedConfigUpdatedAt: active.UpdatedAt,
	})
	require.NoError(t, err)
	require.NoError(t, store.MarkGitHubSetupVerificationProviderMutationStartedForUser(
		ctx, admin.UserID, admin.WorkspaceID, lease.ID,
	))
	_, err = store.Provider().DeleteGitHubAppConfigForUser(ctx, admin.UserID, admin.WorkspaceID)
	require.ErrorIs(t, err, ErrConflict, "provider direct mutation must not preempt a durable patching winner")
	installations, err := store.ListAppInstallationsForUser(ctx, admin.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	_, err = store.UninstallAppInstallationForUser(ctx, admin.UserID, admin.WorkspaceID, installations[0].ID)
	require.ErrorIs(t, err, ErrConflict, "direct uninstall must not preempt a durable patching winner")

	// Bypass the public store to model an unexpected writer. Finalize must still
	// compare the snapshot persisted in the lease rather than trusting Begin.
	_, err = pool.Exec(ctx, `UPDATE service_provider_configs SET updated_at = $1 WHERE workspace_id = $2 AND provider_kind = 'github'`,
		active.UpdatedAt.Add(time.Second), admin.WorkspaceID)
	require.NoError(t, err)
	_, err = store.FinalizeGitHubSetupVerificationForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, lease.ID)
	require.ErrorIs(t, err, ErrConflict)
	unchanged, found, err := store.Provider().LoadGitHubAppConfigForWorkspace(ctx, admin.WorkspaceID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, activeParams.AppName, unchanged.AppName)
	require.NoError(t, store.AbortGitHubSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, lease.ID))
}

func TestGitHubAdminVerificationBeginsOnFreshWorkspaceBeforeAnyDefinitionUse(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "github_verify_admin_fresh", time.Now().UTC())

	lease, err := store.BeginGitHubAppSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, GitHubSetupVerificationInput{
		Mode: GitHubAppSetupModeAdmin, Config: validGitHubAppConfigParams(),
	})
	require.NoError(t, err, "the canonical definition must be seeded before privileged pending settlement")
	require.NotEmpty(t, lease.ID)
	require.NoError(t, store.AbortGitHubSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, lease.ID))
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "github_verify_config_repair", time.Now().UTC())
	params := validGitHubAppConfigParams()
	_, err := store.FinalizeGitHubAppSetupForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, params)
	require.NoError(t, err)
	_, err = store.Provider().DeleteGitHubAppConfigForUser(ctx, admin.UserID, admin.WorkspaceID)
	require.NoError(t, err)

	lease, err := store.BeginGitHubAppSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, GitHubSetupVerificationInput{
		Mode: GitHubAppSetupModeAdmin, RequireActiveInstallation: true, Config: params,
	})
	require.NoError(t, err, "an active installation with missing config is a repair, not a fresh activation")
	require.NoError(t, store.MarkGitHubSetupVerificationProviderMutationStartedForUser(
		ctx, admin.UserID, admin.WorkspaceID, lease.ID,
	))
	view, err := store.FinalizeGitHubSetupVerificationForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, lease.ID)
	require.NoError(t, err)
	require.True(t, view.Registered)

	fresh := seedAppAdminFixture(t, pool, "github_verify_config_repair_fresh", time.Now().UTC())
	_, err = store.BeginGitHubAppSetupVerificationForUser(ctx, fresh.UserID, fresh.WorkspaceID, GitHubSetupVerificationInput{
		Mode: GitHubAppSetupModeAdmin, RequireActiveInstallation: true, Config: params,
	})
	require.ErrorIs(t, err, ErrConflict)
	var leaseCount, claimCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_installation_requests WHERE workspace_id = $1 AND status IN ('setup_verifying','setup_patching')`, fresh.WorkspaceID).Scan(&leaseCount))
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_provider_workspace_claims WHERE workspace_id = $1`, fresh.WorkspaceID).Scan(&claimCount))
	require.Zero(t, leaseCount)
	require.Zero(t, claimCount)
}

func TestGitHubAdminVerificationRejectsDifferentAppIdentityBeforeCreatingLease(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	admin := seedAppAdminFixture(t, pool, "github_verify_admin_identity_guard", time.Now().UTC())
	activeParams := validGitHubAppConfigParams()
	_, err := store.FinalizeGitHubAppSetupForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, activeParams)
	require.NoError(t, err)
	active, found, err := store.Provider().LoadGitHubAppConfigForWorkspace(ctx, admin.WorkspaceID)
	require.NoError(t, err)
	require.True(t, found)

	replacement := activeParams
	replacement.AppID++
	replacement.AppSlug = "different-github-app"
	_, err = store.BeginGitHubAppSetupVerificationForUser(ctx, admin.UserID, admin.WorkspaceID, GitHubSetupVerificationInput{
		Mode:                    GitHubAppSetupModeAdmin,
		Config:                  replacement,
		ExpectedConfigID:        active.ID,
		ExpectedConfigUpdatedAt: active.UpdatedAt,
	})
	require.ErrorIs(t, err, ErrConflict)

	var leaseCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installation_requests
WHERE workspace_id = $1
  AND service_definition_id = $2
  AND status = 'setup_verifying'
`, admin.WorkspaceID, registry.GitHubAppID).Scan(&leaseCount))
	require.Zero(t, leaseCount, "a different GitHub App must fail before a verification lease or external PATCH can begin")
	unchanged, found, err := store.Provider().LoadGitHubAppConfigForWorkspace(ctx, admin.WorkspaceID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, activeParams.AppID, unchanged.AppID)
	require.Equal(t, activeParams.AppSlug, unchanged.AppSlug)
}

func TestGetGitHubSetupProposalNeverReturnsExpiredRemainderPastCleanupLimit(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "github_proposal_cleanup_limit", now)
	requester := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "github_proposal_cleanup_limit_requester", now)
	params := validGitHubAppConfigParams()

	seed, err := store.SaveVerifiedGitHubSetupProposalForUser(ctx, requester.UserID, admin.WorkspaceID, params)
	require.NoError(t, err)
	expiredAt := time.Now().UTC().Add(-githubSetupProposalTTL - time.Minute).Truncate(time.Second)
	_, err = pool.Exec(ctx, `UPDATE service_installation_requests SET created_at = $1, updated_at = $1 WHERE id = $2`, expiredAt, seed.ID)
	require.NoError(t, err)
	for index := 0; index < 100; index++ {
		_, err = pool.Exec(ctx, `
INSERT INTO service_installation_requests (
  id, workspace_id, service_definition_id, service_kind, request_kind,
  requested_version, requested_config, base_config, requested_scopes,
  status, request_reason, requested_by_member_id, decision_reason,
  service_installation_id, created_at, updated_at
)
SELECT $1, workspace_id, service_definition_id, service_kind, request_kind,
       requested_version, requested_config, base_config, requested_scopes,
       status, request_reason, requested_by_member_id, decision_reason,
       service_installation_id, created_at, updated_at
FROM service_installation_requests WHERE id = $2
`, fmt.Sprintf("ghsetup_expired_%03d", index), seed.ID)
		require.NoError(t, err)
	}

	_, found, err := store.GetGitHubSetupProposalForUser(ctx, requester.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	require.False(t, found, "the query cutoff must hide the one expired row left after LIMIT 100 cleanup")
	var remainder int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*) FROM service_installation_requests
WHERE workspace_id = $1 AND service_definition_id = $2 AND status = 'setup_ready'
`, admin.WorkspaceID, registry.GitHubAppID).Scan(&remainder))
	require.Equal(t, 1, remainder, "test must exercise cleanup truncation rather than deleting every expired row")
}
