package app

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

	"github.com/stretchr/testify/require"

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

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)

	var (
		description   string
		avatarURL     string
		appID         string
		kind          string
		visibility    string
		latestVersion string
	)
	err := pool.QueryRow(ctx, `
SELECT id, description, avatar_url, JSON_UNQUOTE(JSON_EXTRACT(COALESCE(config, JSON_OBJECT()), '$.appKind')), visibility, latest_version
FROM service_definitions
WHERE slug = 'github'
`).Scan(&appID, &description, &avatarURL, &kind, &visibility, &latestVersion)
	require.NoError(t, err)
	require.Equal(t, "app_github", appID)
	require.NotContains(t, description, "CI")
	require.Equal(t, "https://github.githubassets.com/favicons/favicon.png", avatarURL)
	require.Equal(t, "first_party", kind)
	require.Equal(t, "public", visibility)
	require.Equal(t, "1.0.0", latestVersion)

	var (
		manifestRaw []byte
		version     string
	)
	err = pool.QueryRow(ctx, `
SELECT
    v.manifest,
    v.version
FROM extension_definition_versions v
WHERE v.service_definition_id = $1
`, appID).Scan(&manifestRaw, &version)
	require.NoError(t, err)
	require.Equal(t, "1.0.0", version)

	var manifest struct {
		IconURL     string   `json:"icon_url"`
		Slug        string   `json:"slug"`
		Description string   `json:"description"`
		Scopes      []string `json:"scopes"`
		Commands    []struct {
			Pattern string `json:"pattern"`
		} `json:"commands"`
		EventTypes []struct {
			Type string `json:"type"`
		} `json:"event_types"`
	}
	require.NoError(t, json.Unmarshal(manifestRaw, &manifest))
	require.Equal(t, "https://github.githubassets.com/favicons/favicon.png", manifest.IconURL)
	require.Equal(t, "github", manifest.Slug)
	require.Len(t, manifest.Scopes, 3)
	require.NotContains(t, manifest.Description, "CI")

	require.Empty(t, manifest.Commands)

	eventTypes := make([]string, 0, len(manifest.EventTypes))
	for _, eventType := range manifest.EventTypes {
		eventTypes = append(eventTypes, eventType.Type)
	}
	require.ElementsMatch(t, []string{
		"pull_request_review_submitted",
		"pull_request_review_comment_created",
		"issue_comment_created",
		"pull_request_synchronize",
		"pull_request_closed",
		"pull_request_reopened",
	}, eventTypes)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)

	var (
		description   string
		avatarURL     string
		appID         string
		kind          string
		visibility    string
		latestVersion string
	)
	err := pool.QueryRow(ctx, `
SELECT id, description, avatar_url, JSON_UNQUOTE(JSON_EXTRACT(COALESCE(config, JSON_OBJECT()), '$.appKind')), visibility, latest_version
FROM service_definitions
WHERE slug = 'gitea'
	`).Scan(&appID, &description, &avatarURL, &kind, &visibility, &latestVersion)
	require.NoError(t, err)
	require.Equal(t, "app_gitea", appID)
	require.Equal(t, "https://gitea.com/assets/img/logo.svg", avatarURL)
	require.Equal(t, "first_party", kind)
	require.Equal(t, "public", visibility)
	require.Equal(t, "1.0.0", latestVersion)
	require.NotContains(t, description, "CI")

	var (
		manifestRaw []byte
		version     string
	)
	err = pool.QueryRow(ctx, `
SELECT
    v.manifest,
    v.version
FROM extension_definition_versions v
WHERE v.service_definition_id = $1
`, appID).Scan(&manifestRaw, &version)
	require.NoError(t, err)
	require.Equal(t, "1.0.0", version)

	var manifest struct {
		IconURL     string `json:"icon_url"`
		Slug        string `json:"slug"`
		Description string `json:"description"`
		Commands    []struct {
			Pattern string `json:"pattern"`
		} `json:"commands"`
		EventTypes []struct {
			Type string `json:"type"`
		} `json:"event_types"`
	}
	require.NoError(t, json.Unmarshal(manifestRaw, &manifest))
	require.Equal(t, "https://gitea.com/assets/img/logo.svg", manifest.IconURL)
	require.Equal(t, "gitea", manifest.Slug)
	require.NotContains(t, manifest.Description, "CI")

	require.Empty(t, manifest.Commands)

	eventTypes := make([]string, 0, len(manifest.EventTypes))
	for _, eventType := range manifest.EventTypes {
		eventTypes = append(eventTypes, eventType.Type)
	}
	require.ElementsMatch(t, []string{
		"pull_request_review_submitted",
		"pull_request_review_comment_created",
		"issue_comment_created",
		"pull_request_synchronize",
		"base_branch_pushed",
		"ci_status",
		"workflow_run_status",
		"workflow_job_status",
		"pull_request_closed",
		"pull_request_reopened",
	}, eventTypes)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{})
	now := time.Now().UTC()

	const (
		userID         = "usr_app_install_admin"
		organizationID = "org_app_install_admin"
		workspaceID    = "wsp_app_install_admin"
		memberID       = "mem_app_install_admin"
		serviceMember  = "svc_app_install_github"
		accountMember  = "svc_account_app_install_admin"
		channelAllID   = "chn_app_install_all"
		installationID = "appinst_github_admin"
		accountInstall = "svcinst_app_install_admin"
	)

	mustExec(t, pool, `
INSERT INTO users (id, email, name, display_name, password_hash, is_admin, email_verified_at, created_at, updated_at)
VALUES ($1, 'app-install-admin@example.com', 'app-install-admin', 'App Install Admin', 'hash', FALSE, $2, $2, $2)
`, userID, now)
	seedWorkspaceOrganizationForTests(t, pool, organizationID, "app-install-admin", userID, now)
	mustExec(t, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'app-install-admin', 'app-install-admin', 0, $3, $3)
`, workspaceID, organizationID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'App Install Admin', $3, 'admin', $4, $4)
`, memberID, workspaceID, userID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'service', 'GitHub', NULL, NULL, $3, $3)
`, serviceMember, workspaceID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'service', 'Deployment Bot', NULL, NULL, $3, $3)
`, accountMember, workspaceID, now)
	seedChannelParticipantProjectionForTests(t, pool, workspaceID, channelAllID, "all", "", "public", memberID, memberID, now)
	seedChannelParticipantProjectionForTests(t, pool, workspaceID, channelAllID, "all", "", "public", memberID, serviceMember, now)
	mustExec(t, pool, `
INSERT INTO service_installations (
    id, workspace_id, service_definition_id, service_kind, instance_key,
    status, installed_version, config, metadata, created_by_member_id,
    created_by_actor_type, created_by_actor_id, created_at, updated_at
)
VALUES (
    $1, $2, 'app_github', 'app', 'default', 'active', '1.0.0', '{}',
    NULL, $3, 'human', $3, $4, $4
)
`, installationID, workspaceID, serviceMember, now)
	mustExec(t, pool, `
INSERT INTO service_installations (
    id, workspace_id, service_definition_id, service_kind, instance_key,
    status, installed_version, config, metadata, created_by_member_id,
    created_by_actor_type, created_by_actor_id, created_at, updated_at
)
VALUES (
    $1, $2, 'svcdef_service_account', 'service_account', 'deployment-bot',
    'active', '', '{}', NULL, $3, 'human', $3, $4, $4
)
`, accountInstall, workspaceID, memberID, now)
	mustExec(t, pool, `
INSERT INTO service_members (
    id, workspace_id, service_definition_id, service_installation_id,
    member_id, service_kind, handle, display_name, avatar_url, status,
    created_at, updated_at
)
VALUES (
    'svcmem_registry_deployment_bot', $2, 'svcdef_service_account', $1, $3,
    'service_account', 'deployment-bot', 'Deployment Bot', '', 'active', $4, $4
)
`, accountInstall, workspaceID, accountMember, now)
	mustExec(t, pool, `
INSERT INTO service_members (
    id, workspace_id, service_definition_id, service_installation_id,
    member_id, service_kind, handle, display_name, avatar_url, status,
    created_at, updated_at
)
VALUES (
    'svcmem_registry_github', $2, 'app_github', $1, $3, 'app', 'github',
    'GitHub', 'https://github.githubassets.com/favicons/favicon.png', 'active', $4, $4
)
`, installationID, workspaceID, serviceMember, now)

	installations, err := appStore.ListAppInstallationsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1, "non-App service installations must not enter App replay")
	require.Equal(t, installationID, installations[0].ID)
	require.Equal(t, "app_github", installations[0].AppID)
	require.Equal(t, "github", installations[0].AppSlug)
	require.Equal(t, "GitHub", installations[0].AppName)
	require.Equal(t, "https://github.githubassets.com/favicons/favicon.png", installations[0].AvatarURL)
	require.Equal(t, serviceMember, installations[0].MemberID)
	require.Equal(t, "1.0.0", installations[0].InstalledVersion)

	apps, err := appStore.ListAppsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	require.NotEmpty(t, apps)

	summaryBySlug := make(map[string]registry.AppDefinitionView, len(apps))
	for _, app := range apps {
		summaryBySlug[app.Slug] = app
	}

	require.Contains(t, summaryBySlug, "github")
	require.Contains(t, summaryBySlug, "gitea")
	require.Contains(t, summaryBySlug, registry.EmailAppSlug)
}

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

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

	const (
		catalogAppID       = "app_approval_catalog_projection"
		catalogVersionID   = "appver_approval_catalog_projection_1"
		installationID     = "appinst_approval_catalog_projection"
		serviceMemberID    = "mem_approval_catalog_projection"
		serviceMemberRow   = "svcmem_approval_catalog_projection"
		approvalDefinition = "svcdef_agentteam_approval"
	)

	mustExec(t, pool, `
INSERT INTO extension_definition_versions (
    id, service_definition_id, version, manifest, created_at, updated_at
)
VALUES (
    $1, $2, '1.0.0',
    '{"schema_version":"1","slug":"agentteam-approval","name":"Approval","version":"1.0.0","scopes":["messages.write"]}',
    $3, $3
)
`, catalogVersionID, approvalDefinition, now)
	mustExec(t, pool, `
INSERT INTO members (
    id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at
)
VALUES ($1, $2, 'service', 'Approval', NULL, NULL, $3, $3)
`, serviceMemberID, fixture.WorkspaceID, now)
	mustExec(t, pool, `
INSERT INTO service_installations (
    id, workspace_id, service_definition_id, service_kind, instance_key,
    status, installed_version, config, metadata, created_by_member_id,
    created_by_actor_type, created_by_actor_id, created_at, updated_at
)
VALUES (
    $1, $2, $3, 'app', 'default', 'active', '1.0.0', '{}',
    '{"builtin":true,"identity":"agentteam.approval"}', $4,
    'internal_service', 'agentteam.approval', $5, $5
)
`, installationID, fixture.WorkspaceID, approvalDefinition, fixture.MemberID, now)
	mustExec(t, pool, `
INSERT INTO service_members (
    id, workspace_id, service_definition_id, service_installation_id,
    member_id, service_kind, handle, display_name, avatar_url, status,
    created_at, updated_at
)
VALUES (
    $1, $2, $3, $4, $5, 'app', 'agentteam-approval', 'Approval',
    '/builtin-apps/approval.svg', 'active', $6, $6
)
`, serviceMemberRow, fixture.WorkspaceID, approvalDefinition, installationID, serviceMemberID, now)

	apps, err := appStore.ListAppsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	for _, listed := range apps {
		require.NotEqual(t, "agentteam-approval", listed.Slug)
	}
	require.NotEmpty(t, apps, "ordinary marketplace apps should remain visible")

	installations, err := appStore.ListAppInstallationsForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.Empty(t, installations, "builtin Apps must not appear in app management")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{})
	now := time.Now().UTC()

	const (
		userID                   = "usr_app_install_unlisted"
		workspaceID              = "wsp_app_install_unlisted"
		memberID                 = "mem_app_install_unlisted"
		visibleUnlistedAppID     = "app_tracker_visible_unlisted"
		hiddenUnlistedAppID      = "app_tracker_hidden_unlisted"
		visibleUnlistedVersionID = "appver_tracker_visible_unlisted_1"
		hiddenUnlistedVersionID  = "appver_tracker_hidden_unlisted_1"
		visibleUnlistedMemberID  = "mem_tracker_visible_unlisted"
		hiddenUnlistedMemberID   = "mem_tracker_hidden_unlisted"
		visibleUnlistedInstallID = "appinst_tracker_visible_unlisted"
		hiddenUnlistedInstallID  = "appinst_tracker_hidden_unlisted"
	)

	mustExec(t, pool, `
INSERT INTO users (id, email, name, display_name, password_hash, is_admin, email_verified_at, created_at, updated_at)
VALUES ($1, 'app-install-unlisted@example.com', 'app-install-unlisted', 'App Install Unlisted', 'hash', FALSE, $2, $2, $2)
`, userID, now)
	seedWorkspaceOrganizationForTests(t, pool, "org_app_install_unlisted", "app-install-unlisted", userID, now)
	mustExec(t, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, 'org_app_install_unlisted', 'app-install-unlisted', 'app-install-unlisted', 0, $2, $2)
`, workspaceID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'App Install Unlisted', $3, 'admin', $4, $4)
`, memberID, workspaceID, userID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'service', 'Tracker Visible', NULL, NULL, $3, $3)
`, visibleUnlistedMemberID, workspaceID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'service', 'Tracker Hidden', NULL, NULL, $3, $3)
`, hiddenUnlistedMemberID, workspaceID, now)

	mustExec(t, pool, `
INSERT INTO service_definitions (
    id, service_kind, installation_mode, slug, name, description, avatar_url,
    visibility, developer_id, latest_version, manifest, config, status, created_at, updated_at
)
VALUES
    ($1, 'app', 'singleton', 'tracker-visible', 'Tracker Visible', 'Installed unlisted tracker', '', 'unlisted', '', '1.0.0',
     '{"schema_version":"1","slug":"tracker-visible","name":"Tracker Visible","version":"1.0.0","scopes":["messages.write"]}',
     '{"appKind":"third_party"}', 'inactive', $3, $3),
    ($2, 'app', 'singleton', 'tracker-hidden', 'Tracker Hidden', 'Uninstalled unlisted tracker', '', 'unlisted', '', '1.0.0',
     '{"schema_version":"1","slug":"tracker-hidden","name":"Tracker Hidden","version":"1.0.0","scopes":["messages.write"]}',
     '{"appKind":"third_party"}', 'inactive', $3, $3)
`, visibleUnlistedAppID, hiddenUnlistedAppID, now)
	mustExec(t, pool, `
INSERT INTO extension_definition_versions (id, service_definition_id, version, manifest, created_at, updated_at)
VALUES
    ($1, $2, '1.0.0', '{"schema_version":"1","slug":"tracker-visible","name":"Tracker Visible","version":"1.0.0","scopes":["messages.write"]}', $5, $5),
    ($3, $4, '1.0.0', '{"schema_version":"1","slug":"tracker-hidden","name":"Tracker Hidden","version":"1.0.0","scopes":["messages.write"]}', $5, $5)
`, visibleUnlistedVersionID, visibleUnlistedAppID, hiddenUnlistedVersionID, hiddenUnlistedAppID, now)
	mustExec(t, pool, `
INSERT INTO service_installations (
    id, workspace_id, service_definition_id, service_kind, instance_key,
    status, installed_version, config, metadata, created_by_member_id,
    created_by_actor_type, created_by_actor_id, created_at, updated_at
)
VALUES (
    $1, $3, $2, 'app', 'default', 'active', '1.0.0', '{}', NULL, $4, 'human', $4, $5, $5)
`, visibleUnlistedInstallID, visibleUnlistedAppID, workspaceID, visibleUnlistedMemberID, now)
	mustExec(t, pool, `
INSERT INTO service_members (
    id, workspace_id, service_definition_id, service_installation_id,
    member_id, service_kind, handle, display_name, avatar_url, status,
    created_at, updated_at
)
VALUES (
    'svcmem_tracker_visible', $3, $2, $1, $4, 'app', 'tracker-visible',
    'Tracker Visible', '', 'active', $5, $5
)
`, visibleUnlistedInstallID, visibleUnlistedAppID, workspaceID, visibleUnlistedMemberID, now)
	mustExec(t, pool, `
INSERT INTO service_installations (
    id, workspace_id, service_definition_id, service_kind, instance_key,
    status, installed_version, config, metadata, created_by_member_id,
    created_by_actor_type, created_by_actor_id, created_at, updated_at
)
VALUES (
    $1, $3, $2, 'app', 'default', 'disabled', '1.0.0', '{}', NULL, $4, 'human', $4, $5, $5)
`, hiddenUnlistedInstallID, hiddenUnlistedAppID, workspaceID, hiddenUnlistedMemberID, now)
	mustExec(t, pool, `
INSERT INTO service_members (
    id, workspace_id, service_definition_id, service_installation_id,
    member_id, service_kind, handle, display_name, avatar_url, status,
    created_at, updated_at
)
VALUES (
    'svcmem_tracker_hidden', $3, $2, $1, $4, 'app', 'tracker-hidden',
    'Tracker Hidden', '', 'disabled', $5, $5
)
`, hiddenUnlistedInstallID, hiddenUnlistedAppID, workspaceID, hiddenUnlistedMemberID, now)

	apps, err := appStore.ListAppsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)

	summaryBySlug := make(map[string]registry.AppDefinitionView, len(apps))
	for _, app := range apps {
		summaryBySlug[app.Slug] = app
	}

	require.Contains(t, summaryBySlug, "github")
	require.NotContains(t, summaryBySlug, "tracker-visible")
	require.NotContains(t, summaryBySlug, "tracker-hidden")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{})
	now := time.Now().UTC()

	const (
		userID         = "usr_app_install_member"
		organizationID = "org_app_install_member"
		workspaceID    = "wsp_app_install_member"
		memberID       = "mem_app_install_member"
	)

	mustExec(t, pool, `
INSERT INTO users (id, email, name, display_name, password_hash, is_admin, email_verified_at, created_at, updated_at)
VALUES ($1, 'app-install-member@example.com', 'app-install-member', 'App Install Member', 'hash', FALSE, $2, $2, $2)
`, userID, now)
	seedWorkspaceOrganizationForTests(t, pool, organizationID, "app-install-member", userID, now)
	mustExec(t, pool, `
INSERT INTO workspaces (id, organization_id, slug, name, latest_message_seq, created_at, updated_at)
VALUES ($1, $2, 'app-install-member', 'app-install-member', 0, $3, $3)
`, workspaceID, organizationID, now)
	mustExec(t, pool, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, created_at, updated_at)
VALUES ($1, $2, 'human', 'App Install Member', $3, 'member', $4, $4)
`, memberID, workspaceID, userID, now)

	apps, err := appStore.ListAppsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	require.NotEmpty(t, apps)

	installations, err := appStore.ListAppInstallationsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	require.Empty(t, installations)
}
