package app

import (
	"context"
	"errors"
	"net/http"
	"sync"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/searchengine/runtime"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

// fakeStore implements all of the app.Store methods used by the handlers.
type fakeStore struct {
	mu sync.Mutex

	// Auth
	authenticated      identity.AuthenticatedUser
	authenticateErr    error
	workspace          workspace.Summary
	workspaceErr       error
	lastBearerToken    string
	lastRequiredScopes []string

	// Workspace apps
	apps                          []registry.AppDefinitionView
	appsErr                       error
	installations                 []app.AppInstallationView
	installationsErr              error
	installationChannels          []app.AppInstallationChannelView
	installationChannelsErr       error
	installationExtensions        []app.InstallationExtensionView
	installationExtension         serviceactor.InstallationExtension
	installationExtensionErr      error
	lastExtensionInstallID        string
	lastExtensionSelectionID      string
	lastExtensionDomain           string
	lastExtensionName             string
	lastExtensionOperation        string
	searchEngineConnection        app.SearchEngineConnectionView
	providerConnection            app.ExtensionProviderConnectionView
	providerConnectionErr         error
	providerAuthorizeErr          error
	runtimeProviderCredentials    map[string]string
	runtimeProviderConfig         runner.ConnectionConfig
	lastProviderInstallationID    string
	lastProviderOperation         string
	lastProviderExpectedUpdatedAt *time.Time
	lastProviderCredentials       map[string]string
	lastProviderConfig            runner.ConnectionConfig
	searchEngineRuntime           runtime.RuntimeConnection
	searchEngineErr               error
	lastSearchInstallationID      string
	lastSearchOperation           string
	lastSearchParams              app.UpsertSearchEngineConnectionParams
	directInstallResult           app.AppInstallationView
	directInstallErr              error
	lastDirectInstallParams       app.InstallAppParams
	directConfigResult            app.AppInstallationView
	directConfigErr               error
	lastDirectConfigID            string
	lastDirectConfigParams        app.ConfigureAppInstallationParams
	directAppResult               app.CustomAppInstallationResult
	directAppErr                  error
	lastDirectAppParams           app.RequestCustomAppInstallationParams
	requestInstallResult          app.AppInstallationRequestView
	requestInstallErr             error
	lastInstallReqParams          app.RequestAppInstallationParams
	pendingInstallRequests        []app.AppInstallationRequestView
	pendingInstallReqsErr         error
	requestAppResult              app.CustomAppRequestResult
	requestAppErr                 error
	lastAppRequestParams          app.RequestCustomAppInstallationParams
	initialCredentialsStatus      app.AppInitialCredentialsStatusView
	initialCredentialsClaim       app.AppInitialCredentialsClaimResult
	initialCredentialsErr         error
	lastInitialCredentialsID      string
	appManagers                   []app.AppManagerAssignment
	appManagerResult              app.AppManagerAssignment
	appManagerErr                 error
	lastAppManagerInstallID       string
	lastAppManagerMemberID        string
	lastAppManagerID              string
	lastAppManagerVersion         int64
	appAvatarResult               app.AppInstallationView
	appAvatarErr                  error
	lastAppAvatarInstallID        string
	lastAppAvatarAssetID          string
	decisionInstallResult         app.AppInstallationRequestView
	decisionInstallErr            error
	lastDecisionReqID             string
	lastDecisionReason            string
	lastDecisionAction            string
	lastMarkedRequestID           string
	lastProjectedPendingID        string
	lastProjectedDecisionID       string
	projectPendingErr             error
	projectDecisionErr            error
	projectionContextErr          error
	projectionHasDeadline         bool
	projectionReconcileErr        error
	projectionReconcileRuns       int
	projectionReconcileWS         string
	projectionReconcileCap        int
	uninstallResult               app.AppInstallationView
	uninstallErr                  error
	lastUninstallID               string
	appDisplayNameResult          app.AppInstallationView
	appDisplayNameErr             error
	lastAppDisplayNameID          string
	lastAppDisplayName            string
	appVisibilityResult           app.AppVisibilityUpdateResult
	appVisibilityErr              error
	lastAppVisibilityID           string
	lastAppCatalogVisibility      string
	agentAuthorizations           []app.AgentAppAuthorizationView
	agentAuthResult               app.AgentAppAuthorizationView
	agentAuthErr                  error
	lastAgentAuthAgentID          string
	lastAgentAuthInstallID        string
	eventSubscriptions            []app.ServiceEventSubscriptionView
	eventSubscriptionsErr         error
	eventSubscriptionResult       app.ServiceEventSubscriptionActionResult
	providerWatchErr              error
	lastWatchInstallID            string
	lastWatchTarget               string
	lastWatchResource             string
	lastWatchEvents               []string
	lastWatchID                   string
	lastWatchOperation            string
	lastWatchAPI                  string
	lastWatchLimit                int
	deliveries                    []app.AppWebhookDeliveryView
	deliveriesErr                 error
	lastDeliveriesLimit           int
	lastDeliveriesInstall         string

	// App notifications
	appNotificationInternalPrincipal       app.AppNotificationInternalPrincipal
	appNotificationInternalAuthErr         error
	lastAppNotificationInternalWorkspaceID string
	lastAppNotificationInternalCredential  string
	appNotificationInternalAuthCalls       int
	lastAppNotificationInternalMutation    app.AppNotificationMutationCommand
	appNotificationInternalMutationCalls   int
	lastAppNotificationActionUserID        string
	lastAppNotificationActionWorkspaceID   string
	lastAppNotificationActionCommand       app.AppNotificationActionCommand
	appNotificationActionCalls             int
	appNotificationCard                    app.AppNotificationCardView
	appNotificationCardErr                 error
	lastAppNotificationCardUserID          string
	lastAppNotificationCardWorkspaceID     string
	lastAppNotificationCardMessageID       string
	appNotificationCardCalls               int

	// Webhook
	handleResult app.AppWebhookResult
	handleErr    error
	handleCalled bool
}

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",
		},
		requestInstallResult: app.AppInstallationRequestView{
			ID:               "svcinstreq_1",
			AppSlug:          "github",
			RequestKind:      app.AppInstallationRequestKindInstall,
			RequestedVersion: "1.0.0",
			Status:           "pending",
		},
		directInstallResult: app.AppInstallationView{
			ID: "appinst_direct_1", AppID: "app_github", AppSlug: "github", AppName: "GitHub", Enabled: true,
		},
		directConfigResult: app.AppInstallationView{
			ID: "appinst_direct_1", AppID: "app_github", AppSlug: "github", AppName: "GitHub", Enabled: true,
		},
		installationExtension: serviceactor.InstallationExtension{
			ID:                    "svcie_1",
			ServiceInstallationID: "appinst_direct_1",
			ExtensionName:         "github",
			DefinitionDomain:      "github",
			DefinitionVersion:     1,
			DefinitionDigest:      "sha256:9cf723b2faa16f3b58b8ff4442a3c5f3e77d6c466c5c521f8e54e8d5b7536fd3",
			Status:                serviceactor.StatusActive,
			SelectionSource:       serviceactor.InstallationExtensionSourceAppDefault,
			CreatedAt:             now,
			UpdatedAt:             now,
		},
		directAppResult: app.CustomAppInstallationResult{
			Installation: app.AppInstallationView{ID: "appinst_bot_direct_1", AppSlug: "echo-app", AppName: "Echo App", Enabled: true},
		},
		requestAppResult: app.CustomAppRequestResult{
			Request: app.AppInstallationRequestView{
				ID:               "svcinstreq_bot_1",
				AppSlug:          "echo-app",
				RequestKind:      app.AppInstallationRequestKindInstall,
				RequestedVersion: "1.0.0",
				Status:           app.AppInstallationRequestStatusPending,
			},
		},
		initialCredentialsStatus: app.AppInitialCredentialsStatusView{
			Status: app.AppInitialCredentialsStatusReadyToClaim,
		},
		initialCredentialsClaim: app.AppInitialCredentialsClaimResult{
			CallbackSigningSecret: "initial-callback-secret",
			RunnerToken:           "initial-runner-token",
		},
		decisionInstallResult: app.AppInstallationRequestView{
			ID:             "svcinstreq_1",
			AppSlug:        "github",
			RequestKind:    app.AppInstallationRequestKindInstall,
			Status:         "approved",
			InstallationID: "ainst_1",
		},
		uninstallResult:      app.AppInstallationView{ID: "ainst_1", AppSlug: "github", Enabled: false},
		appDisplayNameResult: app.AppInstallationView{ID: "ainst_1", AppSlug: "echo-app", AppName: "Renamed App", Enabled: true},
		appVisibilityResult:  app.AppVisibilityUpdateResult{CatalogVisibility: registry.CatalogVisibilityPublic, RemovedChannelIDs: []string{}},
		agentAuthResult:      app.AgentAppAuthorizationView{AppInstallationID: "ainst_1", AppSlug: "github", AuthorizationStatus: "active"},
		eventSubscriptionResult: app.ServiceEventSubscriptionActionResult{
			Status:       "created",
			Subscription: app.ServiceEventSubscriptionView{ID: "trg_1", AppInstallationID: "ainst_1", Status: "active"},
		},
		appNotificationInternalPrincipal: app.AppNotificationInternalPrincipal{
			WorkspaceID: "wsp_1", ServiceInstallationID: "ainst_1", ServiceMemberID: "mem_bot_1",
		},
	}
}

func (fake *fakeStore) AuthenticateSession(_ context.Context, token string) (identity.AuthenticatedUser, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastBearerToken = token
	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
}

// Workspace apps -------------------------------------------------------------

func (fake *fakeStore) ListAppsForUser(context.Context, string, string) ([]registry.AppDefinitionView, error) {
	return fake.apps, fake.appsErr
}

func (fake *fakeStore) ListAppInstallationsForUser(context.Context, string, string) ([]app.AppInstallationView, error) {
	return fake.installations, fake.installationsErr
}

func (fake *fakeStore) ListAppInstallationChannelsForUser(context.Context, string, string, string) ([]app.AppInstallationChannelView, error) {
	return fake.installationChannels, fake.installationChannelsErr
}

func (fake *fakeStore) InstallAppForWorkspaceAdmin(_ context.Context, _ string, _ string, params app.InstallAppParams) (app.AppInstallationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastDirectInstallParams = params
	if fake.directInstallErr != nil {
		return app.AppInstallationView{}, fake.directInstallErr
	}
	result := fake.directInstallResult
	result.Config = params.Config
	return result, nil
}

func (fake *fakeStore) ConfigureAppInstallationForWorkspaceAdmin(_ context.Context, _ string, _ string, installationID string, params app.ConfigureAppInstallationParams) (app.AppInstallationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastDirectConfigID = installationID
	fake.lastDirectConfigParams = params
	if fake.directConfigErr != nil {
		return app.AppInstallationView{}, fake.directConfigErr
	}
	result := fake.directConfigResult
	result.Config = params.Config
	return result, nil
}

func (fake *fakeStore) InstallCustomAppForWorkspaceAdmin(_ context.Context, _ string, _ string, params app.RequestCustomAppInstallationParams) (app.CustomAppInstallationResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastDirectAppParams = params
	if fake.directAppErr != nil {
		return app.CustomAppInstallationResult{}, fake.directAppErr
	}
	result := fake.directAppResult
	result.Installation.Config = params.Config
	result.Installation.CatalogVisibility = params.CatalogVisibility
	return result, nil
}

func (fake *fakeStore) RequestCustomAppInstallationForUser(_ context.Context, _ string, _ string, params app.RequestCustomAppInstallationParams) (app.CustomAppRequestResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppRequestParams = params
	if fake.requestAppErr != nil {
		return app.CustomAppRequestResult{}, fake.requestAppErr
	}
	return fake.requestAppResult, nil
}

func (fake *fakeStore) GetAppInitialCredentialsStatusForUser(_ context.Context, _ string, _ string, installationID string) (app.AppInitialCredentialsStatusView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastInitialCredentialsID = installationID
	return fake.initialCredentialsStatus, fake.initialCredentialsErr
}

func (fake *fakeStore) ClaimAppInitialCredentialsForUser(_ context.Context, _ string, _ string, installationID string) (app.AppInitialCredentialsClaimResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastInitialCredentialsID = installationID
	return fake.initialCredentialsClaim, fake.initialCredentialsErr
}

func (fake *fakeStore) ListAppManagersForUser(_ context.Context, _ string, _ string, installationID string) ([]app.AppManagerAssignment, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppManagerInstallID = installationID
	return append([]app.AppManagerAssignment(nil), fake.appManagers...), fake.appManagerErr
}

func (fake *fakeStore) AddAppManagerForUser(_ context.Context, _ string, _ string, installationID, memberID string, _ time.Time) (app.AppManagerAssignment, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppManagerInstallID = installationID
	fake.lastAppManagerMemberID = memberID
	return fake.appManagerResult, fake.appManagerErr
}

func (fake *fakeStore) RemoveAppManagerForUser(_ context.Context, _ string, _ string, installationID, managerID string, expectedVersion int64, _ time.Time) (app.AppManagerAssignment, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppManagerInstallID = installationID
	fake.lastAppManagerID = managerID
	fake.lastAppManagerVersion = expectedVersion
	return fake.appManagerResult, fake.appManagerErr
}

func (fake *fakeStore) UpdateAppAvatarForUser(_ context.Context, _ string, _ string, installationID, avatarAssetID string, _ time.Time) (app.AppInstallationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppAvatarInstallID = installationID
	fake.lastAppAvatarAssetID = avatarAssetID
	return fake.appAvatarResult, fake.appAvatarErr
}

func (fake *fakeStore) RequestAppInstallationForUser(_ context.Context, _ string, _ string, params app.RequestAppInstallationParams) (app.AppInstallationRequestView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastInstallReqParams = params
	if fake.requestInstallErr != nil {
		return app.AppInstallationRequestView{}, fake.requestInstallErr
	}
	return fake.requestInstallResult, nil
}

func (fake *fakeStore) ListPendingAppInstallationRequestsForUser(_ context.Context, _ string, _ string) ([]app.AppInstallationRequestView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	if fake.pendingInstallReqsErr != nil {
		return nil, fake.pendingInstallReqsErr
	}
	return fake.pendingInstallRequests, nil
}

func (fake *fakeStore) ApproveAppInstallationRequestForUser(_ context.Context, _ string, _ string, requestID string, params app.DecideAppInstallationRequestParams) (app.AppInstallationRequestView, error) {
	return fake.decideAppInstallationRequest(requestID, params, "approve")
}

func (fake *fakeStore) RejectAppInstallationRequestForUser(_ context.Context, _ string, _ string, requestID string, params app.DecideAppInstallationRequestParams) (app.AppInstallationRequestView, error) {
	return fake.decideAppInstallationRequest(requestID, params, "reject")
}

func (fake *fakeStore) MarkAppInstallationRequestCannotBeApplied(_ context.Context, _, requestID string) (app.AppInstallationRequestView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastMarkedRequestID = requestID
	result := fake.decisionInstallResult
	result.ID = requestID
	result.Status = app.AppInstallationRequestStatusPending
	result.ReviewState = app.AppInstallationRequestReviewStateCannotBeApplied
	return result, nil
}

func (fake *fakeStore) decideAppInstallationRequest(requestID string, params app.DecideAppInstallationRequestParams, action string) (app.AppInstallationRequestView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastDecisionReqID = requestID
	fake.lastDecisionReason = params.Reason
	fake.lastDecisionAction = action
	if fake.decisionInstallErr != nil {
		return app.AppInstallationRequestView{}, fake.decisionInstallErr
	}
	result := fake.decisionInstallResult
	result.ID = requestID
	if action == "reject" {
		result.Status = "rejected"
		result.InstallationID = ""
	}
	return result, nil
}

func (fake *fakeStore) ProjectPendingAppInstallationRequest(ctx context.Context, request app.AppInstallationRequestView) ([]app.AppNotificationResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastProjectedPendingID = request.ID
	fake.projectionContextErr = ctx.Err()
	_, fake.projectionHasDeadline = ctx.Deadline()
	if fake.projectPendingErr != nil {
		return nil, fake.projectPendingErr
	}
	return []app.AppNotificationResult{{NotificationID: "botn_pending_1"}}, nil
}

func (fake *fakeStore) ProjectAppInstallationRequestDecision(ctx context.Context, request app.AppInstallationRequestView) (app.AppNotificationResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastProjectedDecisionID = request.ID
	fake.projectionContextErr = ctx.Err()
	_, fake.projectionHasDeadline = ctx.Deadline()
	if fake.projectDecisionErr != nil {
		return app.AppNotificationResult{}, fake.projectDecisionErr
	}
	return app.AppNotificationResult{NotificationID: "botn_result_1"}, nil
}

func (fake *fakeStore) ReconcileAppInstallationRequestProjections(ctx context.Context, workspaceID string, limit int) (int, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.projectionReconcileRuns++
	fake.projectionReconcileWS = workspaceID
	fake.projectionReconcileCap = limit
	fake.projectionContextErr = ctx.Err()
	_, fake.projectionHasDeadline = ctx.Deadline()
	if fake.projectionReconcileErr != nil {
		return 0, fake.projectionReconcileErr
	}
	return 1, nil
}

func (fake *fakeStore) UninstallAppInstallationForUser(_ context.Context, _ string, _ string, installationID string) (app.AppInstallationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastUninstallID = installationID
	if fake.uninstallErr != nil {
		return app.AppInstallationView{}, fake.uninstallErr
	}
	return fake.uninstallResult, nil
}

func (fake *fakeStore) UpdateAppDisplayNameForWorkspaceAdmin(_ context.Context, _ string, _ string, installationID string, name string) (app.AppInstallationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppDisplayNameID = installationID
	fake.lastAppDisplayName = name
	if fake.appDisplayNameErr != nil {
		return app.AppInstallationView{}, fake.appDisplayNameErr
	}
	return fake.appDisplayNameResult, nil
}

func (fake *fakeStore) UpdateAppVisibilityForUser(_ context.Context, _ string, _ string, installationID, catalogVisibility string, _ time.Time) (app.AppVisibilityUpdateResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppVisibilityID = installationID
	fake.lastAppCatalogVisibility = catalogVisibility
	if fake.appVisibilityErr != nil {
		return app.AppVisibilityUpdateResult{}, fake.appVisibilityErr
	}
	result := fake.appVisibilityResult
	result.CatalogVisibility = catalogVisibility
	return result, nil
}

func (fake *fakeStore) ListAppPermissionsForUser(context.Context, string, string, string) (app.AppPermissionsView, error) {
	return app.AppPermissionsView{}, nil
}

func (fake *fakeStore) UpdateAppPermissionsForUser(context.Context, string, string, string, string, []string, time.Time) (app.AppPermissionsUpdateResult, error) {
	return app.AppPermissionsUpdateResult{}, nil
}

func (fake *fakeStore) ListAppRunnerCredentialsForUser(context.Context, string, string, string) ([]app.AppRunnerCredentialView, error) {
	return []app.AppRunnerCredentialView{}, nil
}

func (fake *fakeStore) CreateAppRunnerCredentialForUser(context.Context, string, string, string, app.AppRunnerCredentialCreateParams) (app.AppRunnerCredentialTokenView, error) {
	return app.AppRunnerCredentialTokenView{}, nil
}

func (fake *fakeStore) DisableAppRunnerCredentialForUser(context.Context, string, string, string) (app.AppRunnerCredentialView, error) {
	return app.AppRunnerCredentialView{}, nil
}

func (fake *fakeStore) RotateAppCallbackSecretForUser(context.Context, string, string, string) (app.AppCallbackSecretView, error) {
	return app.AppCallbackSecretView{}, nil
}

func (fake *fakeStore) ListAgentAppAuthorizationsForUser(_ context.Context, _ string, _ string, agentID string) ([]app.AgentAppAuthorizationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAgentAuthAgentID = agentID
	if fake.agentAuthErr != nil {
		return nil, fake.agentAuthErr
	}
	return fake.agentAuthorizations, nil
}

func (fake *fakeStore) EnableAgentAppAuthorizationForUser(_ context.Context, _ string, _ string, agentID string, appInstallationID string) (app.AgentAppAuthorizationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAgentAuthAgentID = agentID
	fake.lastAgentAuthInstallID = appInstallationID
	if fake.agentAuthErr != nil {
		return app.AgentAppAuthorizationView{}, fake.agentAuthErr
	}
	return fake.agentAuthResult, nil
}

func (fake *fakeStore) DisableAgentAppAuthorizationForUser(_ context.Context, _ string, _ string, agentID string, appInstallationID string) (app.AgentAppAuthorizationView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAgentAuthAgentID = agentID
	fake.lastAgentAuthInstallID = appInstallationID
	if fake.agentAuthErr != nil {
		return app.AgentAppAuthorizationView{}, fake.agentAuthErr
	}
	return fake.agentAuthResult, nil
}

func (fake *fakeStore) ListServiceEventSubscriptionsForUser(_ context.Context, _ string, _ string, appInstallationID string, target string, resource string, status string, limit int) ([]app.ServiceEventSubscriptionView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWatchInstallID = appInstallationID
	fake.lastWatchTarget = target
	fake.lastWatchResource = resource
	fake.lastWatchOperation = status
	fake.lastWatchAPI = "service_event"
	fake.lastWatchLimit = limit
	return fake.eventSubscriptions, fake.eventSubscriptionsErr
}

func (fake *fakeStore) CreateServiceEventSubscriptionForUser(_ context.Context, _ string, _ string, appInstallationID string, target string, prRefOrURL string, events []string, notifyPrincipal *bool) (app.ServiceEventSubscriptionActionResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWatchInstallID = appInstallationID
	fake.lastWatchTarget = target
	fake.lastWatchResource = prRefOrURL
	fake.lastWatchEvents = events
	fake.lastWatchAPI = "service_event"
	if fake.providerWatchErr != nil {
		return app.ServiceEventSubscriptionActionResult{}, fake.providerWatchErr
	}
	return fake.serviceEventSubscriptionResult(), nil
}

func (fake *fakeStore) PauseServiceEventSubscriptionForUser(_ context.Context, _ string, _ string, subscriptionID string) (app.ServiceEventSubscriptionActionResult, error) {
	return fake.serviceEventSubscriptionOperation(subscriptionID, "pause")
}

func (fake *fakeStore) ResumeServiceEventSubscriptionForUser(_ context.Context, _ string, _ string, subscriptionID string) (app.ServiceEventSubscriptionActionResult, error) {
	return fake.serviceEventSubscriptionOperation(subscriptionID, "resume")
}

func (fake *fakeStore) RemoveServiceEventSubscriptionForUser(_ context.Context, _ string, _ string, subscriptionID string) (app.ServiceEventSubscriptionActionResult, error) {
	return fake.serviceEventSubscriptionOperation(subscriptionID, "delete")
}

func (fake *fakeStore) serviceEventSubscriptionOperation(subscriptionID string, operation string) (app.ServiceEventSubscriptionActionResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWatchID = subscriptionID
	fake.lastWatchOperation = operation
	fake.lastWatchAPI = "service_event"
	if fake.providerWatchErr != nil {
		return app.ServiceEventSubscriptionActionResult{}, fake.providerWatchErr
	}
	return fake.serviceEventSubscriptionResult(), nil
}

func (fake *fakeStore) serviceEventSubscriptionResult() app.ServiceEventSubscriptionActionResult {
	return fake.eventSubscriptionResult
}

func (fake *fakeStore) ListProviderWatchesForUser(_ context.Context, _ string, _ string, appInstallationID string, target string, resource string, status string, limit int) ([]app.ProviderWatchView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWatchInstallID = appInstallationID
	fake.lastWatchTarget = target
	fake.lastWatchResource = resource
	fake.lastWatchOperation = status
	fake.lastWatchAPI = "provider_watch"
	fake.lastWatchLimit = limit
	return fake.eventSubscriptions, fake.eventSubscriptionsErr
}

func (fake *fakeStore) CreateProviderWatchForUser(_ context.Context, _ string, _ string, appInstallationID string, target string, prRefOrURL string, events []string, notifyPrincipal *bool) (app.ProviderWatchActionResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWatchInstallID = appInstallationID
	fake.lastWatchTarget = target
	fake.lastWatchResource = prRefOrURL
	fake.lastWatchEvents = events
	fake.lastWatchAPI = "provider_watch"
	if fake.providerWatchErr != nil {
		return app.ProviderWatchActionResult{}, fake.providerWatchErr
	}
	return fake.providerWatchActionResult(), nil
}

func (fake *fakeStore) PauseProviderWatchForUser(_ context.Context, _ string, _ string, watchID string) (app.ProviderWatchActionResult, error) {
	return fake.providerWatchSubscriptionOperation(watchID, "pause")
}

func (fake *fakeStore) ResumeProviderWatchForUser(_ context.Context, _ string, _ string, watchID string) (app.ProviderWatchActionResult, error) {
	return fake.providerWatchSubscriptionOperation(watchID, "resume")
}

func (fake *fakeStore) RemoveProviderWatchForUser(_ context.Context, _ string, _ string, watchID string) (app.ProviderWatchActionResult, error) {
	return fake.providerWatchSubscriptionOperation(watchID, "delete")
}

func (fake *fakeStore) providerWatchSubscriptionOperation(watchID string, operation string) (app.ProviderWatchActionResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastWatchID = watchID
	fake.lastWatchOperation = operation
	fake.lastWatchAPI = "provider_watch"
	if fake.providerWatchErr != nil {
		return app.ProviderWatchActionResult{}, fake.providerWatchErr
	}
	return fake.providerWatchActionResult(), nil
}

func (fake *fakeStore) providerWatchActionResult() app.ProviderWatchActionResult {
	result := fake.serviceEventSubscriptionResult()
	return app.ProviderWatchActionResult{
		Status:        result.Status,
		AlreadyExists: result.AlreadyExists,
		Watch:         result.Subscription,
		BindingID:     result.BindingID,
		Message:       result.Message,
	}
}

func (fake *fakeStore) ListAppWebhookDeliveriesForUser(_ context.Context, _ string, _ string, installationID string, limit int) ([]app.AppWebhookDeliveryView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastDeliveriesInstall = installationID
	fake.lastDeliveriesLimit = limit
	return fake.deliveries, fake.deliveriesErr
}

func (fake *fakeStore) AuthenticateAppNotificationInternalService(_ context.Context, workspaceID string, credential string) (app.AppNotificationInternalPrincipal, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppNotificationInternalWorkspaceID = workspaceID
	fake.lastAppNotificationInternalCredential = credential
	fake.appNotificationInternalAuthCalls++
	if fake.appNotificationInternalAuthErr != nil {
		return app.AppNotificationInternalPrincipal{}, fake.appNotificationInternalAuthErr
	}
	return fake.appNotificationInternalPrincipal, nil
}

func (fake *fakeStore) CreateAppNotificationForInternal(context.Context, app.AppNotificationInternalPrincipal, app.AppNotificationCommand) (app.AppNotificationResult, error) {
	return app.AppNotificationResult{}, nil
}

func (fake *fakeStore) GetAppNotificationForInternal(context.Context, app.AppNotificationInternalPrincipal, string) (app.AppNotificationResult, error) {
	return app.AppNotificationResult{}, nil
}

func (fake *fakeStore) MutateAppNotificationForInternal(_ context.Context, _ app.AppNotificationInternalPrincipal, command app.AppNotificationMutationCommand) (app.AppNotificationResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppNotificationInternalMutation = command
	fake.appNotificationInternalMutationCalls++
	return app.AppNotificationResult{}, nil
}

func (fake *fakeStore) InvokeAppNotificationActionForUser(_ context.Context, userID string, workspaceID string, command app.AppNotificationActionCommand) (app.AppNotificationActionResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppNotificationActionUserID = userID
	fake.lastAppNotificationActionWorkspaceID = workspaceID
	fake.lastAppNotificationActionCommand = command
	fake.appNotificationActionCalls++
	return app.AppNotificationActionResult{}, nil
}

func (fake *fakeStore) GetAppNotificationCardForUser(_ context.Context, userID string, workspaceID string, messageID string) (app.AppNotificationCardView, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.lastAppNotificationCardUserID = userID
	fake.lastAppNotificationCardWorkspaceID = workspaceID
	fake.lastAppNotificationCardMessageID = messageID
	fake.appNotificationCardCalls++
	if fake.appNotificationCardErr != nil {
		return app.AppNotificationCardView{}, fake.appNotificationCardErr
	}
	return fake.appNotificationCard, nil
}

// Webhook --------------------------------------------------------------------

func (fake *fakeStore) HandleAppWebhook(context.Context, string, http.Header, []byte) (app.AppWebhookResult, error) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.handleCalled = true
	if fake.handleErr != nil {
		return app.AppWebhookResult{}, fake.handleErr
	}
	return fake.handleResult, nil
}

// fakeBlocker delegates to the shared httputil.FakeBlocker.
type fakeBlocker = httputil.FakeBlocker

// errSentinel delegates to the shared httputil.ErrSentinel.
var errSentinel = httputil.ErrSentinel

type fakeFollowupDeliverer struct {
	mu              sync.Mutex
	calls           int
	lastWorkspaceID string
	lastDeliveries  []message.MessageDelivery
}

func (fake *fakeFollowupDeliverer) DeliverFollowupMessages(_ context.Context, workspaceID string, deliveries []message.MessageDelivery) {
	fake.mu.Lock()
	defer fake.mu.Unlock()
	fake.calls++
	fake.lastWorkspaceID = workspaceID
	fake.lastDeliveries = deliveries
}

// newAppRouter builds a router with the package's own middleware. The fake
// Store accepts any bearer token / workspace ID and returns a verified user.
func newAppRouter(deps Dependencies) *gin.Engine {
	if deps.SessionAuth == nil {
		if s, ok := deps.AppStore.(interface {
			AuthenticateSession(context.Context, string) (identity.AuthenticatedUser, error)
		}); ok {
			deps.SessionAuth = s
		}
	}
	if deps.WorkspaceStore == nil {
		if s, ok := deps.AppStore.(interface {
			GetWorkspaceForUser(context.Context, string, string) (workspace.Summary, error)
		}); ok {
			deps.WorkspaceStore = s
		}
	}
	if deps.InstallationRequestProjector == nil {
		if projector, ok := deps.AppStore.(AppInstallationRequestProjector); ok {
			deps.InstallationRequestProjector = projector
		}
	}
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	RegisterRoutes(router, deps)
	return router
}

func (fake *fakeStore) GetAppInstallationRequest(_ context.Context, _ string, requestID string) (app.AppInstallationRequestView, error) {
	candidates := append([]app.AppInstallationRequestView{}, fake.pendingInstallRequests...)
	candidates = append(candidates, fake.requestInstallResult, fake.decisionInstallResult)
	for _, view := range candidates {
		if view.ID == requestID {
			return view, nil
		}
	}
	return app.AppInstallationRequestView{}, errors.New("request not found")
}
