package app

import (
	"context"
	"database/sql"
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func requireEnabledConnectionByExternalRef(t *testing.T, ctx context.Context, appStore *Store, appID string, externalSubjectRef string) webhookAppConnection {
	t.Helper()

	connections, err := appStore.loadEnabledConnectionsByExternalRef(ctx, appID, externalSubjectRef)
	require.NoError(t, err)
	require.Len(t, connections, 1)
	return connections[0]
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "app_connection_scope", now)

	githubInstallation, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{
		AppSlug: "github",
	})
	require.NoError(t, err)

	giteaInstallation, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{
		AppSlug: "gitea",
	})
	require.NoError(t, err)

	githubConnection, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     githubInstallation.ID,
		ExternalSubjectRef: "12345",
		DisplayLabel:       "GitHub installation 12345",
		Credentials:        map[string]any{"token": "github-token"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)

	giteaConnection, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     giteaInstallation.ID,
		ExternalSubjectRef: "12345",
		DisplayLabel:       "Gitea org 12345",
		Credentials:        map[string]any{"webhook_secret": "gitea-secret"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)

	require.NotEqual(t, githubConnection.ID, giteaConnection.ID)

	type storedConnection struct {
		AppID string
		ID    string
	}
	rows, err := pool.Query(ctx, `
SELECT service_definition_id, id
FROM service_connections
WHERE external_subject_ref = $1
ORDER BY service_definition_id ASC
`, "12345")
	require.NoError(t, err)
	defer rows.Close()

	stored := make([]storedConnection, 0, 2)
	for rows.Next() {
		var entry storedConnection
		require.NoError(t, rows.Scan(&entry.AppID, &entry.ID))
		stored = append(stored, entry)
	}
	require.NoError(t, rows.Err())
	require.Len(t, stored, 2)
	require.Equal(t, []storedConnection{
		{AppID: "app_gitea", ID: giteaConnection.ID},
		{AppID: "app_github", ID: githubConnection.ID},
	}, stored)

	githubResolved := requireEnabledConnectionByExternalRef(t, ctx, appStore, "app_github", "12345")
	require.Equal(t, githubInstallation.ID, githubResolved.InstallationID)
	require.Equal(t, githubConnection.ID, githubResolved.ConnectionID)

	giteaResolved := requireEnabledConnectionByExternalRef(t, ctx, appStore, "app_gitea", "12345")
	require.Equal(t, giteaInstallation.ID, giteaResolved.InstallationID)
	require.Equal(t, giteaConnection.ID, giteaResolved.ConnectionID)

	secondFixture := seedAppAdminFixture(t, pool, "app_connection_scope_second", now.Add(time.Minute))
	secondGitHubInstallation, err := appStore.installAppForUserForTest(ctx, secondFixture.UserID, secondFixture.WorkspaceID, InstallAppParams{
		AppSlug: "github",
	})
	require.NoError(t, err)

	secondConnection, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     secondGitHubInstallation.ID,
		ExternalSubjectRef: "12345",
		DisplayLabel:       "GitHub installation 12345 duplicate",
		Credentials:        map[string]any{"token": "github-token-2"},
		CreatedByUserID:    secondFixture.UserID,
	})
	require.NoError(t, err)
	require.NotEmpty(t, secondConnection.ID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "app_connection_update_existing", now)

	installation, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{
		AppSlug: "github",
	})
	require.NoError(t, err)

	first, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "12345",
		DisplayLabel:       "GitHub installation 12345",
		Credentials:        map[string]any{"token": "github-token"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)

	second, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "12345",
		DisplayLabel:       "GitHub installation 12345 renamed",
		Credentials:        map[string]any{"token": "github-token-2"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)
	require.Equal(t, first.ID, second.ID)
	require.Equal(t, "GitHub installation 12345 renamed", second.DisplayLabel)

	connections, err := appStore.ListAppConnections(ctx, installation.ID)
	require.NoError(t, err)
	require.Len(t, connections, 1)
	require.Equal(t, first.ID, connections[0].ID)
	require.Equal(t, "GitHub installation 12345 renamed", connections[0].DisplayLabel)

	resolved := requireEnabledConnectionByExternalRef(t, ctx, appStore, "app_github", "12345")
	require.Equal(t, first.ID, resolved.ConnectionID)
	var resolvedCredentials map[string]any
	require.NoError(t, json.Unmarshal(resolved.Credentials, &resolvedCredentials))
	require.Equal(t, map[string]any{"token": "github-token-2"}, resolvedCredentials)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "app_connection_webhook_endpoint", now)

	installation, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{
		AppSlug: "github",
	})
	require.NoError(t, err)

	connection, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "789",
		DisplayLabel:       "acme",
		Credentials:        map[string]any{"webhook_secret": "github-webhook-secret"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)

	var (
		status                  string
		callbackPath            string
		signingSecretCiphertext string
		configJSON              string
		activeEndpointKey       string
	)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT status, callback_path, signing_secret_ciphertext, config, active_endpoint_key
FROM service_webhook_endpoints
WHERE service_connection_id = $1
  AND endpoint_kind = 'provider_webhook'
`, connection.ID).Scan(&status, &callbackPath, &signingSecretCiphertext, &configJSON, &activeEndpointKey))
	require.Equal(t, "active", status)
	require.Equal(t, "/api/apps/github/webhook", callbackPath)
	require.Contains(t, signingSecretCiphertext, CredentialStorageScheme)
	require.JSONEq(t, `{"provider":"github","secretField":"webhook_secret","secretMaterial":"encrypted_connection_credentials"}`, configJSON)
	require.Equal(t, serviceWebhookEndpointActiveKey(connection.ID, "provider_webhook"), activeEndpointKey)

	require.NoError(t, appStore.DisableAppConnection(ctx, installation.ID, connection.ID))

	var disabledActiveKey sql.NullString
	require.NoError(t, pool.QueryRow(ctx, `
SELECT status, active_endpoint_key
FROM service_webhook_endpoints
WHERE service_connection_id = $1
  AND endpoint_kind = 'provider_webhook'
`, connection.ID).Scan(&status, &disabledActiveKey))
	require.Equal(t, "disabled", status)
	require.False(t, disabledActiveKey.Valid)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "app_connection_seed_read", now)

	installation, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppSlug: "github"})
	require.NoError(t, err)
	connection, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "789",
		DisplayLabel:       "acme",
		Credentials:        map[string]any{"webhook_secret": "github-webhook-secret"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)

	// Rotate the connection through the expected store API, which re-syncs the
	// webhook endpoint from the rotated credential envelope.
	updated, err := appStore.UpsertAppConnection(ctx, UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "789",
		DisplayLabel:       "acme-renamed",
		Credentials:        map[string]any{"webhook_secret": "github-webhook-secret-rotated"},
		CreatedByUserID:    fixture.UserID,
	})
	require.NoError(t, err)
	require.Equal(t, connection.ID, updated.ID)

	rotatedCredentials, err := appStore.marshalAppCredentialsObject(map[string]any{"webhook_secret": "github-webhook-secret-rotated"})
	require.NoError(t, err)

	connections, err := appStore.ListAppConnections(ctx, installation.ID)
	require.NoError(t, err)
	require.Len(t, connections, 1)
	require.Equal(t, connection.ID, connections[0].ID)
	require.Equal(t, "acme-renamed", connections[0].DisplayLabel)

	var (
		serviceDisplayLabel string
		serviceCredentials  []byte
		endpointSecret      string
	)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT connection.display_label, connection.credentials, endpoint.signing_secret_ciphertext
FROM service_connections connection
JOIN service_webhook_endpoints endpoint
  ON endpoint.service_connection_id = connection.id
 AND endpoint.endpoint_kind = 'provider_webhook'
WHERE connection.id = $1
`, connection.ID).Scan(&serviceDisplayLabel, &serviceCredentials, &endpointSecret))
	require.Equal(t, "acme-renamed", serviceDisplayLabel)
	// The expected write re-encrypts credentials with a fresh nonce, so the
	// stored envelope must decrypt to the rotated plaintext, not match the
	// pre-rotation ciphertext verbatim.
	decrypted, err := appStore.decryptStoredAppCredentialsJSON(serviceCredentials)
	require.NoError(t, err)
	require.JSONEq(t, `{"webhook_secret":"github-webhook-secret-rotated"}`, string(decrypted))
	require.NotEqual(t, string(rotatedCredentials), string(serviceCredentials))

	require.NoError(t, appStore.DisableAppConnection(ctx, installation.ID, connection.ID))

	connections, err = appStore.ListAppConnections(ctx, installation.ID)
	require.NoError(t, err)
	require.Empty(t, connections)

	var (
		connectionStatus  string
		endpointStatus    string
		endpointActiveKey sql.NullString
	)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT connection.status, endpoint.status, endpoint.active_endpoint_key
FROM service_connections connection
JOIN service_webhook_endpoints endpoint
  ON endpoint.service_connection_id = connection.id
 AND endpoint.endpoint_kind = 'provider_webhook'
WHERE connection.id = $1
`, connection.ID).Scan(&connectionStatus, &endpointStatus, &endpointActiveKey))
	require.Equal(t, "disabled", connectionStatus)
	require.Equal(t, "disabled", endpointStatus)
	require.False(t, endpointActiveKey.Valid)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "service_connection_resolution", now)

	installation, err := appStore.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppSlug: "github"})
	require.NoError(t, err)
	seedProviderWatchReadyConnection(t, ctx, appStore, fixture, installation, "github", "789", "acme")
	connectionRef, status, err := appStore.resolvePullRequestSubscriptionConnection(ctx, "github", fixture.WorkspaceID, "acme/agentteam", "")
	require.NoError(t, err)
	require.Equal(t, webhooks.ServiceEventConnectionReady, status)
	require.Equal(t, "789", connectionRef)
}
