package app

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

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

	for name, test := range map[string]struct {
		appID  string
		digest string
	}{
		"searxng": {appID: "app_searxng", digest: model.SearXNGBuiltinExtensionDefinitionDigest},
		"brave":   {appID: "app_brave_search", digest: model.BraveBuiltinExtensionDefinitionDigest},
		"tavily":  {appID: "app_tavily", digest: model.TavilyBuiltinExtensionDefinitionDigest},
		"serper":  {appID: "app_serper", digest: model.SerperBuiltinExtensionDefinitionDigest},
		"arxiv":   {appID: "app_arxiv", digest: model.ArxivBuiltinExtensionDefinitionDigest},
		"hotnews": {
			appID:  "app_hotnews",
			digest: model.HotNewsBuiltinExtensionDefinitionDigest,
		},
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			config, ok := builtinExtensionCapabilityConfig(test.appID)
			require.True(t, ok)
			require.Equal(t, model.ExtensionCapabilityConfig{
				Extensions: []model.ExtensionCapabilityEntry{{
					Domain:           name,
					Version:          1,
					DefinitionDigest: test.digest,
					DefaultName:      name,
					DefaultEnabled:   true,
				}},
			}, config)
		})
	}
}

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

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

	installation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: "app_github"})
	require.NoError(t, err)

	selections, err := serviceactor.New(pool).ListInstallationExtensions(ctx, fixture.WorkspaceID, installation.ID)
	require.NoError(t, err)
	require.Len(t, selections, 1)
	require.Equal(t, "github", selections[0].ExtensionName)
	require.Equal(t, "github", selections[0].DefinitionDomain)
	require.Equal(t, 1, selections[0].DefinitionVersion)
	require.Equal(t, model.GitHubBuiltinExtensionDefinitionDigest, selections[0].DefinitionDigest)
	require.Equal(t, serviceactor.StatusActive, selections[0].Status)
	require.Equal(t, serviceactor.InstallationExtensionSourceAppDefault, selections[0].SelectionSource)
}

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

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

	installation, err := store.installAppForUserForTest(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		InstallAppParams{AppID: "app_arxiv"},
	)
	require.NoError(t, err)

	selections, err := serviceactor.New(pool).ListInstallationExtensions(
		ctx,
		fixture.WorkspaceID,
		installation.ID,
	)
	require.NoError(t, err)
	require.Len(t, selections, 1)
	require.Equal(t, "arxiv", selections[0].ExtensionName)
	require.Equal(t, "arxiv", selections[0].DefinitionDomain)
	require.Equal(t, 1, selections[0].DefinitionVersion)
	require.Equal(
		t,
		model.ArxivBuiltinExtensionDefinitionDigest,
		selections[0].DefinitionDigest,
	)
	require.Equal(t, serviceactor.StatusActive, selections[0].Status)

	var connectionCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_connections
WHERE workspace_id = $1
  AND service_installation_id = $2
`, fixture.WorkspaceID, installation.ID).Scan(&connectionCount))
	require.Zero(t, connectionCount)
}

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

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

	installation, err := store.installAppForUserForTest(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		InstallAppParams{AppID: "app_hotnews"},
	)
	require.NoError(t, err)

	selections, err := serviceactor.New(pool).ListInstallationExtensions(
		ctx,
		fixture.WorkspaceID,
		installation.ID,
	)
	require.NoError(t, err)
	require.Len(t, selections, 1)
	require.Equal(t, "hotnews", selections[0].ExtensionName)
	require.Equal(t, "hotnews", selections[0].DefinitionDomain)
	require.Equal(t, 1, selections[0].DefinitionVersion)
	require.Equal(
		t,
		model.HotNewsBuiltinExtensionDefinitionDigest,
		selections[0].DefinitionDigest,
	)
	require.Equal(t, serviceactor.StatusActive, selections[0].Status)

	var connectionCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_connections
WHERE workspace_id = $1
  AND service_installation_id = $2
`, fixture.WorkspaceID, installation.ID).Scan(&connectionCount))
	require.Zero(t, connectionCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "extension_bot_only", now)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	insertExternalAppAppForTests(t, pool, externalAppAppSeed{
		AppID:     "app_extension_bot_only",
		VersionID: "appver_extension_bot_only",
		Slug:      "extension-bot-only",
		Name:      "Extension Bot Only",
		Version:   "1.0.0",
		CreatedAt: now,
	})

	installation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: "app_extension_bot_only"})
	require.NoError(t, err)
	selections, err := serviceactor.New(pool).ListInstallationExtensions(ctx, fixture.WorkspaceID, installation.ID)
	require.NoError(t, err)
	require.Empty(t, selections)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "extension_optional", now)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	installation := installExtensionTestHost(t, ctx, pool, store, fixture, "optional", now)
	digest := "sha256:" + strings.Repeat("d", 64)
	configJSON, err := model.EncodeExtensionCapabilityConfig(model.ExtensionCapabilityConfig{
		Extensions: []model.ExtensionCapabilityEntry{{
			Domain:           "optional-watch",
			Version:          2,
			DefinitionDigest: digest,
			DefaultName:      "optional-watch",
			DefaultEnabled:   false,
		}},
	})
	require.NoError(t, err)
	_, err = serviceactor.New(pool).UpsertCapability(ctx, serviceactor.CapabilityUpsert{
		ServiceDefinitionID: installation.AppID,
		ServiceKind:         serviceactor.KindApp,
		CapabilityKey:       serviceactor.CapabilityExtensions,
		Config:              configJSON,
		DefaultEnabled:      true,
		Now:                 now,
	})
	require.NoError(t, err)

	available, err := store.ListInstallationExtensionsForUser(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		installation.ID,
	)
	require.NoError(t, err)
	require.Len(t, available, 1)
	require.False(t, available[0].Selected)
	require.Empty(t, available[0].ID)
	require.Equal(t, digest, available[0].DefinitionDigest)

	selected, err := store.AddInstallationExtensionForUser(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		installation.ID,
		"optional-watch",
		"workspace-watch",
	)
	require.NoError(t, err)
	require.Equal(t, "workspace-watch", selected.ExtensionName)
	require.Equal(t, 2, selected.DefinitionVersion)
	require.Equal(t, digest, selected.DefinitionDigest)
	require.Equal(t, serviceactor.StatusActive, selected.Status)
	require.Equal(t, serviceactor.InstallationExtensionSourceUser, selected.SelectionSource)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "extension_default_conflict", now)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	existingInstallation := installExtensionTestHost(t, ctx, pool, store, fixture, "default_conflict", now)
	_, err := serviceactor.New(pool).UpsertInstallationExtension(ctx, serviceactor.InstallationExtensionUpsert{
		InstallationExtension: serviceactor.InstallationExtension{
			ID:                    "svcie_extension_default_conflict_existing",
			WorkspaceID:           fixture.WorkspaceID,
			ServiceDefinitionID:   existingInstallation.AppID,
			ServiceInstallationID: existingInstallation.ID,
			ExtensionName:         "github",
			DefinitionDomain:      "existing-github-name",
			DefinitionVersion:     1,
			DefinitionDigest:      "sha256:" + strings.Repeat("b", 64),
			Status:                serviceactor.StatusActive,
			SelectionSource:       serviceactor.InstallationExtensionSourceUser,
			CreatedByMemberID:     fixture.MemberID,
		},
		Now: now,
	})
	require.NoError(t, err)

	githubInstallation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: "app_github"})
	require.NoError(t, err)
	selections, err := serviceactor.New(pool).ListInstallationExtensions(ctx, fixture.WorkspaceID, githubInstallation.ID)
	require.NoError(t, err)
	require.Len(t, selections, 1)
	require.Equal(t, serviceactor.StatusDisabled, selections[0].Status)
	require.Equal(t, installationExtensionNameConflictReason, selections[0].DisabledReason)
	require.Equal(t, "github", selections[0].ExtensionName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "extension_rename_enable", now)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	existingInstallation := installExtensionTestHost(t, ctx, pool, store, fixture, "rename_enable", now)
	_, err := serviceactor.New(pool).UpsertInstallationExtension(ctx, serviceactor.InstallationExtensionUpsert{
		InstallationExtension: serviceactor.InstallationExtension{
			ID:                    "svcie_extension_rename_enable_existing",
			WorkspaceID:           fixture.WorkspaceID,
			ServiceDefinitionID:   existingInstallation.AppID,
			ServiceInstallationID: existingInstallation.ID,
			ExtensionName:         "github",
			DefinitionDomain:      "existing-github-name",
			DefinitionVersion:     1,
			DefinitionDigest:      "sha256:" + strings.Repeat("c", 64),
			Status:                serviceactor.StatusActive,
			SelectionSource:       serviceactor.InstallationExtensionSourceUser,
			CreatedByMemberID:     fixture.MemberID,
		},
		Now: now,
	})
	require.NoError(t, err)
	githubInstallation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: "app_github"})
	require.NoError(t, err)
	selections, err := store.ListInstallationExtensionsForUser(ctx, fixture.UserID, fixture.WorkspaceID, githubInstallation.ID)
	require.NoError(t, err)
	require.Len(t, selections, 1)

	renamed, err := store.RenameInstallationExtensionForUser(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		githubInstallation.ID,
		selections[0].ID,
		"github-watch",
	)
	require.NoError(t, err)
	require.Equal(t, "github-watch", renamed.ExtensionName)
	require.Equal(t, serviceactor.StatusDisabled, renamed.Status)

	enabled, err := store.EnableInstallationExtensionForUser(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		githubInstallation.ID,
		renamed.ID,
	)
	require.NoError(t, err)
	require.Equal(t, serviceactor.StatusActive, enabled.Status)
	require.Empty(t, enabled.DisabledReason)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "extension_disable_catalog", now)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	installation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: "app_github"})
	require.NoError(t, err)
	selections, err := store.ListInstallationExtensionsForUser(ctx, fixture.UserID, fixture.WorkspaceID, installation.ID)
	require.NoError(t, err)
	require.Len(t, selections, 1)

	agentID := "agt_extension_disable_catalog"
	seedAppAgentParticipant(
		t,
		pool,
		fixture.WorkspaceID,
		fixture.UserID,
		fixture.MemberID,
		fixture.AllChannelID,
		"mac_extension_disable_catalog",
		agentID,
		"mem_extension_disable_catalog_agent",
		"Extension Catalog Agent",
		now,
	)
	var hookCalls []string
	store.SetWorkspaceAppCatalogReconcileHook(func(_ context.Context, _ coredb.Tx, workspaceID string, _ time.Time) error {
		require.Equal(t, fixture.WorkspaceID, workspaceID)
		hookCalls = append(hookCalls, workspaceID)
		return nil
	})

	disabled, err := store.DisableInstallationExtensionForUser(
		ctx,
		fixture.UserID,
		fixture.WorkspaceID,
		installation.ID,
		selections[0].ID,
	)
	require.NoError(t, err)
	require.Equal(t, serviceactor.StatusDisabled, disabled.Status)
	require.Equal(t, []string{fixture.WorkspaceID}, hookCalls)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "extension_uninstall", now)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	installation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: "app_github"})
	require.NoError(t, err)

	var before int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installation_extensions
WHERE service_installation_id = $1
`, installation.ID).Scan(&before))
	require.Equal(t, 1, before)

	_, err = store.UninstallAppInstallationForUser(ctx, fixture.UserID, fixture.WorkspaceID, installation.ID)
	require.NoError(t, err)
	var after int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installation_extensions
WHERE service_installation_id = $1
`, installation.ID).Scan(&after))
	require.Zero(t, after)
}

func installExtensionTestHost(
	t *testing.T,
	ctx context.Context,
	pool coredb.QueryExecer,
	store *Store,
	fixture appAdminFixture,
	suffix string,
	now time.Time,
) AppInstallationView {
	t.Helper()
	appID := "app_extension_" + suffix
	insertExternalAppAppForTests(t, pool, externalAppAppSeed{
		AppID:     appID,
		VersionID: "appver_extension_" + suffix,
		Slug:      "extension-" + suffix,
		Name:      "Extension " + suffix,
		Version:   "1.0.0",
		CreatedAt: now,
	})
	installation, err := store.installAppForUserForTest(ctx, fixture.UserID, fixture.WorkspaceID, InstallAppParams{AppID: appID})
	require.NoError(t, err)
	return installation
}

func githubExtensionCapabilityConfig() model.ExtensionCapabilityConfig {
	return model.ExtensionCapabilityConfig{
		Extensions: []model.ExtensionCapabilityEntry{{
			Domain:           "github",
			Version:          1,
			DefinitionDigest: model.GitHubBuiltinExtensionDefinitionDigest,
			DefaultName:      "github",
			DefaultEnabled:   true,
		}},
	}
}
