package provider

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/provider"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// fakeStore implements provider.Store for tests.
type fakeStore struct {
	mu sync.Mutex

	authenticated   identity.AuthenticatedUser
	authenticateErr error
	workspace       workspace.Summary
	workspaceErr    error

	authorizeRegisterErr error
	authorizeInstallErr  error

	githubConfigView                provider.GitHubAppConfigView
	githubConfigErr                 error
	githubSaveResult                provider.GitHubAppConfigView
	githubSaveErr                   error
	githubSaveCalls                 int
	lastGithubSave                  provider.UpsertGitHubAppConfigParams
	githubSetupMode                 string
	githubSetupModeErr              error
	githubConnectionMode            string
	githubConnectionModeErr         error
	githubConnectionRequest         app.AppInstallationRequestView
	githubConnectionRequestErr      error
	githubConnectionRequestCalls    int
	lastGithubConnectionConfig      provider.GitHubAppConfig
	lastGithubConnectionTarget      string
	lastGithubConnectionLabel       string
	githubProposal                  app.GitHubSetupProposalView
	githubProposalFound             bool
	githubProposalErr               error
	githubProposalSave              app.GitHubSetupProposalView
	githubProposalSaveErr           error
	githubProposalSaveCalls         int
	lastGithubProposalSave          provider.UpsertGitHubAppConfigParams
	githubAdminDraftConfig          provider.GitHubAppConfig
	githubCompletionLease           app.GitHubSetupCompletionLease
	githubCompletionErr             error
	githubCompletionBeginCalls      int
	githubCompletionMarkCalls       int
	githubCompletionFinalizeCalls   int
	lastGithubCompletionIDs         []string
	lastGithubCompletionTargets     []app.GitHubSetupCompletionTarget
	githubVerificationLease         app.GitHubSetupVerificationLease
	githubVerificationBeginErr      error
	githubVerificationFinalizeErr   error
	githubVerificationMarkErr       error
	githubVerificationAbortErr      error
	githubVerificationBeginCalls    int
	githubVerificationResumeCalls   int
	githubVerificationResumeFound   bool
	githubVerificationFinalizeCalls int
	githubVerificationMarkCalls     int
	githubVerificationAbortCalls    int
	lastGithubVerificationInput     app.GitHubSetupVerificationInput
	githubDeleteResult              provider.GitHubAppConfigView
	githubDeleteErr                 error
	githubLoadConfig                provider.GitHubAppConfig
	githubLoadFound                 bool
	githubLoadErr                   error
	githubConnectionBase            provider.GitHubConnectionMutationBase
	githubConnectionsBase           provider.GitHubConnectionsMutationBase
	githubConnectionPrepareErr      error
	githubConnectionPrepareCalls    int
	lastGithubConnectionPrepare     string
	lastGithubConnectionPrepareIDs  []string
	lastGithubVerifiedTargets       []provider.GitHubVerifiedConnectionTarget
	githubSetupResult               provider.AppInstallationView
	githubSetupErr                  error
	githubSetupCalls                int
	lastGithubSetupBase             provider.GitHubConnectionMutationBase
	githubConnections               []provider.AppConnectionView
	githubListErr                   error
	githubDisablePrepareErr         error
	githubDisablePrepareCalls       int
	githubDisableErr                error
	lastGithubDisable               string
	lastGithubDisableBase           provider.GitHubConnectionMutationBase

	giteaConfigView     provider.GiteaAppConfigView
	giteaConfigErr      error
	giteaConnections    []provider.AppConnectionView
	giteaListErr        error
	giteaSaveResult     provider.GiteaAppConfigView
	giteaSaveErr        error
	lastGiteaSave       provider.UpsertGiteaAppConfigParams
	giteaCreateResult   provider.AppInstallationView
	giteaCreateErr      error
	lastGiteaTarget     string
	giteaDisconnectErr  error
	lastGiteaDisconnect string
}

func newFakeStore() *fakeStore {
	now := time.Now().UTC()
	return &fakeStore{
		authenticated: identity.AuthenticatedUser{
			User: identity.User{
				ID:              "usr_1",
				Name:            "alice",
				Email:           "alice@example.com",
				EmailVerifiedAt: &now,
			},
		},
		workspace:            workspace.Summary{WorkspaceID: "wsp_1"},
		githubSetupMode:      app.GitHubAppSetupModeAdmin,
		githubConnectionMode: app.GitHubAppSetupModeAdmin,
		githubVerificationLease: app.GitHubSetupVerificationLease{
			ID: "ghverify_1",
		},
	}
}

func (fake *fakeStore) AuthenticateSession(context.Context, string) (identity.AuthenticatedUser, error) {
	if fake.authenticateErr != nil {
		return identity.AuthenticatedUser{}, fake.authenticateErr
	}
	return fake.authenticated, nil
}

func (fake *fakeStore) GetWorkspaceForUser(context.Context, string, string) (workspace.Summary, error) {
	if fake.workspaceErr != nil {
		return workspace.Summary{}, fake.workspaceErr
	}
	return fake.workspace, nil
}

// GitHub ---------------------------------------------------------------------

func (fake *fakeStore) AuthorizeGitHubAppRegistrationForUser(context.Context, string, string) error {
	return fake.authorizeRegisterErr
}

func (fake *fakeStore) AuthorizeGitHubAppInstallForUser(context.Context, string, string) error {
	return fake.authorizeInstallErr
}

func (fake *fakeStore) GitHubAppSetupModeForUser(context.Context, string, string) (string, error) {
	if fake.githubSetupModeErr != nil {
		return "", fake.githubSetupModeErr
	}
	if fake.authorizeRegisterErr != nil {
		return "", fake.authorizeRegisterErr
	}
	return fake.githubSetupMode, nil
}

func (fake *fakeStore) GitHubConnectionSetupModeForUser(context.Context, string, string) (string, error) {
	if fake.githubConnectionModeErr != nil {
		return "", fake.githubConnectionModeErr
	}
	return fake.githubConnectionMode, nil
}

func (fake *fakeStore) RequestVerifiedGitHubConnectionForUser(
	_ context.Context,
	_ string,
	_ string,
	target string,
	label string,
	config provider.GitHubAppConfig,
) (app.AppInstallationRequestView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubConnectionRequestCalls++
	fake.lastGithubConnectionTarget = target
	fake.lastGithubConnectionLabel = label
	fake.lastGithubConnectionConfig = config
	return fake.githubConnectionRequest, fake.githubConnectionRequestErr
}

func (fake *fakeStore) SaveVerifiedGitHubSetupProposalForUser(_ context.Context, _ string, _ string, params provider.UpsertGitHubAppConfigParams) (app.GitHubSetupProposalView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubProposalSaveCalls++
	fake.lastGithubProposalSave = params
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID:         params.AppID,
		AppSlug:       params.AppSlug,
		AppName:       params.AppName,
		HTMLURL:       params.HTMLURL,
		ClientID:      params.ClientID,
		ClientSecret:  params.ClientSecret,
		PrivateKeyPEM: params.PrivateKeyPEM,
		WebhookSecret: params.WebhookSecret,
		OwnerLogin:    params.OwnerLogin,
		OwnerID:       params.OwnerID,
	}
	return fake.githubProposalSave, fake.githubProposalSaveErr
}

func (fake *fakeStore) SaveVerifiedGitHubSetupDraftForRequester(ctx context.Context, userID string, workspaceID string, params provider.UpsertGitHubAppConfigParams) (app.GitHubSetupProposalView, error) {
	return fake.SaveVerifiedGitHubSetupProposalForUser(ctx, userID, workspaceID, params)
}

func (fake *fakeStore) GetGitHubSetupDraftForRequester(_ context.Context, _ string, _ string, draftID string) (app.GitHubSetupProposalView, provider.GitHubAppConfig, error) {
	proposal := fake.githubProposalSave
	if strings.TrimSpace(proposal.ID) == "" {
		proposal = fake.githubProposal
	}
	if strings.TrimSpace(proposal.ID) == "" {
		proposal.ID = strings.TrimSpace(draftID)
	}
	return proposal, fake.githubAdminDraftConfig, fake.githubProposalErr
}

func (fake *fakeStore) SaveVerifiedGitHubSetupDraftForWorkspaceAdmin(_ context.Context, _ string, _ string, params provider.UpsertGitHubAppConfigParams) (app.GitHubSetupProposalView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubProposalSaveCalls++
	fake.lastGithubProposalSave = params
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID:         params.AppID,
		AppSlug:       params.AppSlug,
		AppName:       params.AppName,
		HTMLURL:       params.HTMLURL,
		ClientID:      params.ClientID,
		ClientSecret:  params.ClientSecret,
		PrivateKeyPEM: params.PrivateKeyPEM,
		WebhookSecret: params.WebhookSecret,
		OwnerLogin:    params.OwnerLogin,
		OwnerID:       params.OwnerID,
	}
	return fake.githubProposalSave, fake.githubProposalSaveErr
}

func (fake *fakeStore) BeginGitHubAppSetupVerificationForUser(_ context.Context, _ string, _ string, input app.GitHubSetupVerificationInput) (app.GitHubSetupVerificationLease, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubVerificationBeginCalls++
	fake.lastGithubVerificationInput = input
	if fake.githubVerificationBeginErr != nil {
		return app.GitHubSetupVerificationLease{}, fake.githubVerificationBeginErr
	}
	lease := fake.githubVerificationLease
	lease.Mode = input.Mode
	lease.WebhookSecret = input.Config.WebhookSecret
	return lease, nil
}

func (fake *fakeStore) ResumeGitHubSetupVerificationForUser(_ context.Context, _ string, _ string, mode string) (provider.GitHubAppConfig, app.GitHubSetupVerificationLease, bool, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubVerificationResumeCalls++
	if !fake.githubVerificationResumeFound {
		return provider.GitHubAppConfig{}, app.GitHubSetupVerificationLease{}, false, nil
	}
	lease := fake.githubVerificationLease
	lease.Mode = mode
	return fake.githubAdminDraftConfig, lease, true, nil
}

func (fake *fakeStore) FinalizeGitHubSetupVerificationForRequester(_ context.Context, _ string, _ string, _ string) (app.GitHubSetupProposalView, error) {
	fake.mu.Lock()
	fake.githubVerificationFinalizeCalls++
	input := fake.lastGithubVerificationInput
	err := fake.githubVerificationFinalizeErr
	fake.mu.Unlock()
	if err != nil {
		return app.GitHubSetupProposalView{}, err
	}
	return fake.SaveVerifiedGitHubSetupProposalForUser(context.Background(), "", "", input.Config)
}

func (fake *fakeStore) MarkGitHubSetupVerificationProviderMutationStartedForUser(context.Context, string, string, string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubVerificationMarkCalls++
	return fake.githubVerificationMarkErr
}

func (fake *fakeStore) FinalizeGitHubSetupVerificationForWorkspaceAdmin(_ context.Context, _ string, _ string, _ string) (provider.GitHubAppConfigView, error) {
	fake.mu.Lock()
	fake.githubVerificationFinalizeCalls++
	input := fake.lastGithubVerificationInput
	err := fake.githubVerificationFinalizeErr
	fake.mu.Unlock()
	if err != nil {
		return provider.GitHubAppConfigView{}, err
	}
	return fake.SaveGitHubAppConfigForWorkspaceAdmin(context.Background(), "", "", input.Config)
}

func (fake *fakeStore) AbortGitHubSetupVerificationForUser(context.Context, string, string, string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubVerificationAbortCalls++
	return fake.githubVerificationAbortErr
}

func (fake *fakeStore) GetGitHubSetupProposalForUser(context.Context, string, string) (app.GitHubSetupProposalView, bool, error) {
	return fake.githubProposal, fake.githubProposalFound, fake.githubProposalErr
}

func (fake *fakeStore) GetGitHubSetupDraftForWorkspaceAdmin(context.Context, string, string, string) (app.GitHubSetupProposalView, provider.GitHubAppConfig, error) {
	if fake.githubProposalErr != nil {
		return app.GitHubSetupProposalView{}, provider.GitHubAppConfig{}, fake.githubProposalErr
	}
	return fake.githubProposal, fake.githubAdminDraftConfig, nil
}

func (fake *fakeStore) BeginGitHubSetupCompletionForWorkspaceAdmin(_ context.Context, _ string, _ string, draftID string, installationIDs []string) (provider.GitHubAppConfig, app.GitHubSetupCompletionLease, error) {
	fake.mu.Lock()
	fake.githubCompletionBeginCalls++
	fake.lastGithubCompletionIDs = append([]string(nil), installationIDs...)
	fake.mu.Unlock()
	if fake.githubCompletionErr != nil {
		return provider.GitHubAppConfig{}, app.GitHubSetupCompletionLease{}, fake.githubCompletionErr
	}
	lease := fake.githubCompletionLease
	if lease.ID == "" {
		lease.ID = "ghverify_completion"
	}
	lease.DraftID = draftID
	lease.InstallationIDs = append([]string(nil), installationIDs...)
	if len(installationIDs) > 0 {
		lease.InstallationID = installationIDs[0]
	}
	return fake.githubAdminDraftConfig, lease, nil
}

func (fake *fakeStore) MarkGitHubSetupCompletionProviderMutationStartedForWorkspaceAdmin(_ context.Context, _ string, _ string, _ app.GitHubSetupCompletionLease, targets []app.GitHubSetupCompletionTarget) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubCompletionMarkCalls++
	fake.lastGithubCompletionTargets = append([]app.GitHubSetupCompletionTarget(nil), targets...)
	return fake.githubCompletionErr
}

func (fake *fakeStore) FinalizeGitHubSetupCompletionForWorkspaceAdmin(context.Context, string, string, app.GitHubSetupCompletionLease) (provider.AppInstallationView, error) {
	fake.mu.Lock()
	fake.githubCompletionFinalizeCalls++
	fake.mu.Unlock()
	if fake.githubCompletionErr != nil {
		return provider.AppInstallationView{}, fake.githubCompletionErr
	}
	return fake.githubSetupResult, nil
}

func (fake *fakeStore) GetGitHubAppConfigForUser(context.Context, string, string) (provider.GitHubAppConfigView, error) {
	return fake.githubConfigView, fake.githubConfigErr
}

func (fake *fakeStore) SaveGitHubAppConfigForWorkspaceAdmin(_ context.Context, _ string, _ string, params provider.UpsertGitHubAppConfigParams) (provider.GitHubAppConfigView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubSaveCalls++
	fake.lastGithubSave = params
	if fake.githubSaveErr != nil {
		return provider.GitHubAppConfigView{}, fake.githubSaveErr
	}
	return fake.githubSaveResult, nil
}

func (fake *fakeStore) FinalizeGitHubAppSetupForWorkspaceAdmin(ctx context.Context, userID string, workspaceID string, params provider.UpsertGitHubAppConfigParams) (provider.GitHubAppConfigView, error) {
	return fake.SaveGitHubAppConfigForWorkspaceAdmin(ctx, userID, workspaceID, params)
}

func (fake *fakeStore) DeleteGitHubAppConfigForUser(context.Context, string, string) (provider.GitHubAppConfigView, error) {
	return fake.githubDeleteResult, fake.githubDeleteErr
}

func (fake *fakeStore) LoadGitHubAppConfigForWorkspace(context.Context, string) (provider.GitHubAppConfig, bool, error) {
	return fake.githubLoadConfig, fake.githubLoadFound, fake.githubLoadErr
}

func (fake *fakeStore) PrepareGitHubConnectionsForUser(_ context.Context, _ string, _ string, installationIDs []string) (provider.GitHubAppConfig, []string, provider.GitHubConnectionsMutationBase, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubConnectionPrepareCalls++
	fake.lastGithubConnectionPrepareIDs = append([]string(nil), installationIDs...)
	if len(installationIDs) == 1 {
		fake.lastGithubConnectionPrepare = strings.TrimSpace(installationIDs[0])
	}
	if fake.githubConnectionPrepareErr != nil {
		return provider.GitHubAppConfig{}, nil, provider.GitHubConnectionsMutationBase{}, fake.githubConnectionPrepareErr
	}
	if len(installationIDs) != 1 {
		return provider.GitHubAppConfig{}, nil, provider.GitHubConnectionsMutationBase{}, storeerr.BadRequest("Exactly one GitHub installation id is required.")
	}
	if fake.githubLoadErr != nil {
		return provider.GitHubAppConfig{}, nil, provider.GitHubConnectionsMutationBase{}, fake.githubLoadErr
	}
	if !fake.githubLoadFound {
		return provider.GitHubAppConfig{}, nil, provider.GitHubConnectionsMutationBase{}, storeerr.BadRequest("GitHub App is not registered")
	}
	return fake.githubLoadConfig, append([]string(nil), installationIDs...), fake.githubConnectionsBase, nil
}

func (fake *fakeStore) ApplyVerifiedGitHubConnectionsForUser(_ context.Context, _ string, _ string, targets []provider.GitHubVerifiedConnectionTarget, _ provider.GitHubConnectionsMutationBase) (provider.AppInstallationView, error) {
	fake.mu.Lock()
	fake.githubSetupCalls++
	fake.lastGithubVerifiedTargets = append([]provider.GitHubVerifiedConnectionTarget(nil), targets...)
	fake.mu.Unlock()
	if fake.githubSetupErr != nil {
		return provider.AppInstallationView{}, fake.githubSetupErr
	}
	return fake.githubSetupResult, nil
}

func (fake *fakeStore) PrepareGitHubConnectionForUser(_ context.Context, _ string, _ string, externalSubjectRef string) (provider.GitHubAppConfig, provider.GitHubConnectionMutationBase, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubConnectionPrepareCalls++
	fake.lastGithubConnectionPrepare = externalSubjectRef
	if fake.githubConnectionPrepareErr != nil {
		return provider.GitHubAppConfig{}, provider.GitHubConnectionMutationBase{}, fake.githubConnectionPrepareErr
	}
	if fake.githubLoadErr != nil {
		return provider.GitHubAppConfig{}, provider.GitHubConnectionMutationBase{}, fake.githubLoadErr
	}
	if !fake.githubLoadFound {
		return provider.GitHubAppConfig{}, provider.GitHubConnectionMutationBase{}, storeerr.BadRequest("GitHub App is not registered")
	}
	return fake.githubLoadConfig, fake.githubConnectionBase, nil
}

func (fake *fakeStore) ApplyGitHubAppSetupForUser(_ context.Context, _ string, _ string, _ string, _ string, base provider.GitHubConnectionMutationBase) (provider.AppInstallationView, error) {
	fake.mu.Lock()
	fake.githubSetupCalls++
	fake.lastGithubSetupBase = base
	fake.mu.Unlock()
	if fake.githubSetupErr != nil {
		return provider.AppInstallationView{}, fake.githubSetupErr
	}
	return fake.githubSetupResult, nil
}

func (fake *fakeStore) ListGitHubConnectionsForUser(context.Context, string, string) ([]provider.AppConnectionView, error) {
	return fake.githubConnections, fake.githubListErr
}

func (fake *fakeStore) PrepareGitHubConnectionDisableForUser(_ context.Context, _ string, _ string, connectionID string) (provider.GitHubConnectionMutationBase, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.githubDisablePrepareCalls++
	if fake.githubDisablePrepareErr != nil {
		return provider.GitHubConnectionMutationBase{}, fake.githubDisablePrepareErr
	}
	for _, connection := range fake.githubConnections {
		if connection.ID == connectionID {
			return fake.githubConnectionBase, nil
		}
	}
	return provider.GitHubConnectionMutationBase{}, storeerr.ErrNotFound
}

func (fake *fakeStore) DisableGitHubConnectionForUser(_ context.Context, _ string, _ string, connectionID string, base provider.GitHubConnectionMutationBase) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastGithubDisable = connectionID
	fake.lastGithubDisableBase = base
	return fake.githubDisableErr
}

// Gitea ----------------------------------------------------------------------

func (fake *fakeStore) GetGiteaAppConfigForUser(context.Context, string, string) (provider.GiteaAppConfigView, error) {
	return fake.giteaConfigView, fake.giteaConfigErr
}

func (fake *fakeStore) ListGiteaConnectionsForUser(context.Context, string, string) ([]provider.AppConnectionView, error) {
	return fake.giteaConnections, fake.giteaListErr
}

func (fake *fakeStore) SaveGiteaAppConfigForWorkspaceAdmin(_ context.Context, _ string, _ string, params provider.UpsertGiteaAppConfigParams) (provider.GiteaAppConfigView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastGiteaSave = params
	return fake.giteaSaveResult, fake.giteaSaveErr
}

func (fake *fakeStore) CreateGiteaConnectionForWorkspaceAdmin(_ context.Context, _ string, _ string, target string) (provider.AppInstallationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastGiteaTarget = target
	return fake.giteaCreateResult, fake.giteaCreateErr
}

func (fake *fakeStore) DisconnectGiteaConnectionForWorkspaceAdmin(_ context.Context, _ string, _ string, connectionID string) error {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastGiteaDisconnect = connectionID
	return fake.giteaDisconnectErr
}

// generateTestPEM returns a fresh RSA private key encoded as PKCS#1 PEM.
func generateTestPEM(t *testing.T) string {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	require.NoError(t, err)
	return string(pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(key),
	}))
}

// newProviderRouter builds a router with full RegisterRoutes wiring.
func newProviderRouter(deps Dependencies) *gin.Engine {
	if deps.SessionAuth == nil {
		if s, ok := deps.Store.(interface {
			AuthenticateSession(context.Context, string) (identity.AuthenticatedUser, error)
		}); ok {
			deps.SessionAuth = s
		}
	}
	if deps.WorkspaceStore == nil {
		if s, ok := deps.Store.(interface {
			GetWorkspaceForUser(context.Context, string, string) (workspace.Summary, error)
		}); ok {
			deps.WorkspaceStore = s
		}
	}
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	RegisterRoutes(router, deps)
	return router
}
