package app

import (
	"context"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_catalog_visibility_admin", now)
	peer := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_catalog_visibility_peer", now)

	params := directAppOnboardingParams("public-catalog-app")
	params.Description = "Safe public description"
	params.CatalogVisibility = "public"
	created, err := store.InstallCustomAppForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, params)
	require.NoError(t, err)
	require.Equal(t, "public", created.Installation.CatalogVisibility)
	require.True(t, created.Installation.CanManageApp)

	var globalVisibility, catalogVisibility string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT app.visibility, attribution.catalog_visibility
FROM service_definitions app
JOIN extension_catalog_attributions attribution ON attribution.service_definition_id = app.id
WHERE attribution.workspace_id = $1 AND app.id = $2
`, admin.WorkspaceID, created.Installation.AppID).Scan(&globalVisibility, &catalogVisibility))
	require.Equal(t, "unlisted", globalVisibility, "workspace publication must not mutate global registry visibility")
	require.Equal(t, "public", catalogVisibility)

	definitions, err := store.ListAppsForUser(ctx, peer.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	publicDefinition := findAppDefinitionByIDForVisibilityTest(t, definitions, created.Installation.AppID)
	require.Equal(t, "unlisted", publicDefinition.Visibility)
	require.Equal(t, "public", publicDefinition.CatalogVisibility)
	require.Equal(t, params.Description, publicDefinition.Description)
	require.Nil(t, publicDefinition.RuntimeCapability, "ordinary public catalog reads must redact runtime transports")
	require.Empty(t, publicDefinition.LatestScopes)
	require.Empty(t, publicDefinition.LatestConfigSchema)

	installations, err := store.ListAppInstallationsForUser(ctx, peer.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	publicInstallation := findAppInstallationByIDForVisibilityTest(t, installations, created.Installation.ID)
	require.Equal(t, "public", publicInstallation.CatalogVisibility)
	require.False(t, publicInstallation.CanManageApp)
	require.Nil(t, publicInstallation.Config)
	require.Empty(t, publicInstallation.Connections)
	require.Empty(t, publicInstallation.Grants)
	require.Nil(t, publicInstallation.RuntimeCapability, "ordinary public installation reads must redact runtime transports")

	_, err = store.UpdateAppVisibilityForUser(
		ctx, peer.UserID, admin.WorkspaceID, created.Installation.ID, "private", now.Add(time.Second),
	)
	require.ErrorIs(t, err, ErrNotFound, "publication must not grant App management authority")
	_, err = store.RotateAppCallbackSecretForUser(ctx, peer.UserID, admin.WorkspaceID, created.Installation.ID)
	require.ErrorIs(t, err, ErrNotFound, "publication must not expose credential management")
	_, err = store.UninstallAppInstallationForUser(ctx, peer.UserID, admin.WorkspaceID, created.Installation.ID)
	require.ErrorIs(t, err, ErrNotFound, "publication must not expose uninstall authority")
	_, err = store.UpdateAppVisibilityForUser(
		ctx, admin.UserID, admin.WorkspaceID, created.Installation.ID, "workspace", now.Add(time.Second),
	)
	require.ErrorIs(t, err, ErrBadRequest)
	require.ErrorContains(t, err, "catalogVisibility must be private or public")

	channelID := "chn_bot_catalog_visibility"
	seedChannelParticipantProjectionForTests(
		t, pool, admin.WorkspaceID, channelID, channelID, "Public App channel", "private", peer.MemberID, peer.MemberID, now,
	)
	_, err = store.AddChannelServiceForUserWithEvent(
		ctx, peer.UserID, admin.WorkspaceID, channelID, created.Installation.MemberID,
	)
	require.NoError(t, err)

	updated, err := store.UpdateAppVisibilityForUser(
		ctx, admin.UserID, admin.WorkspaceID, created.Installation.ID, "private", now.Add(2*time.Second),
	)
	require.NoError(t, err)
	require.Equal(t, "private", updated.CatalogVisibility)
	require.Equal(t, []string{channelID}, updated.RemovedChannelIDs)
	requireAppTargetSingleInt(t, ctx, pool, 0, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1 AND member_id = $2 AND removed_at IS NULL
`, channelID, created.Installation.MemberID)
	assertWorkspaceCatalogOmitsApp(t, ctx, store, peer.UserID, admin.WorkspaceID, params.Slug)
	assertWorkspaceInstallationIDs(t, ctx, store, peer.UserID, admin.WorkspaceID)

	// private -> private still runs cleanup and converges successfully when
	// there is no historical residue left.
	replayed, err := store.UpdateAppVisibilityForUser(
		ctx, admin.UserID, admin.WorkspaceID, created.Installation.ID, "private", now.Add(3*time.Second),
	)
	require.NoError(t, err)
	require.Empty(t, replayed.RemovedChannelIDs)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_requested_public_admin", now)
	requester := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_requested_public_requester", now)
	peer := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_requested_public_peer", now)

	params := directAppOnboardingParams("requested-public-app")
	params.CatalogVisibility = "public"
	requested, err := store.RequestCustomAppInstallationForUser(
		ctx, requester.UserID, admin.WorkspaceID, params,
	)
	require.NoError(t, err)
	require.Equal(t, "public", requested.Request.AppCatalogVisibility)

	var pendingCatalogVisibility string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT catalog_visibility
FROM extension_catalog_attributions
WHERE workspace_id = $1 AND service_definition_id = $2
`, admin.WorkspaceID, requested.Request.AppID).Scan(&pendingCatalogVisibility))
	require.Equal(t, "private", pendingCatalogVisibility)
	assertWorkspaceCatalogOmitsApp(t, ctx, store, peer.UserID, admin.WorkspaceID, params.Slug)

	approved, err := store.ApproveAppInstallationRequestForUser(
		ctx,
		admin.UserID,
		admin.WorkspaceID,
		requested.Request.ID,
		DecideAppInstallationRequestParams{Reason: "safe to publish"},
	)
	require.NoError(t, err)
	require.Equal(t, AppInstallationRequestStatusApproved, approved.Status)
	require.Equal(t, "public", approved.AppCatalogVisibility)
	require.NoError(t, pool.QueryRow(ctx, `
SELECT catalog_visibility
FROM extension_catalog_attributions
WHERE workspace_id = $1 AND service_definition_id = $2
`, admin.WorkspaceID, requested.Request.AppID).Scan(&pendingCatalogVisibility))
	require.Equal(t, "public", pendingCatalogVisibility)

	installations, err := store.ListAppInstallationsForUser(ctx, peer.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	publicInstallation := findAppInstallationByIDForVisibilityTest(t, installations, approved.InstallationID)
	require.Equal(t, "public", publicInstallation.CatalogVisibility)
	require.False(t, publicInstallation.CanManageApp)
	require.Nil(t, publicInstallation.Config)

	managerUpdate, err := store.UpdateAppVisibilityForUser(
		ctx, requester.UserID, admin.WorkspaceID, approved.InstallationID, "private", now.Add(time.Second),
	)
	require.NoError(t, err, "the initial active App manager may change catalog visibility")
	require.Equal(t, "private", managerUpdate.CatalogVisibility)
	assertWorkspaceInstallationIDs(t, ctx, store, peer.UserID, admin.WorkspaceID)
	_, err = store.UpdateAppVisibilityForUser(
		ctx, requester.UserID, admin.WorkspaceID, approved.InstallationID, "public", now.Add(2*time.Second),
	)
	require.NoError(t, err)
}

func TestAppCatalogVisibilityDefaultsPrivate(t *testing.T) {
	params := directAppOnboardingParams("default-private-app")
	prepared, err := prepareWorkspaceAppDefinition(params)
	require.NoError(t, err)
	require.Equal(t, "private", prepared.catalogVisibility)
	require.Equal(t, "private", prepared.reviewSnapshot.CatalogVisibility)

	_, err = prepareWorkspaceAppDefinition(RequestCustomAppInstallationParams{
		Slug:              params.Slug,
		Name:              params.Name,
		Manifest:          params.Manifest,
		Config:            params.Config,
		CatalogVisibility: "workspace",
	})
	require.ErrorIs(t, err, ErrBadRequest)
	require.ErrorContains(t, err, "catalogVisibility must be private or public")
}

func findAppDefinitionByIDForVisibilityTest(
	t *testing.T,
	definitions []registry.AppDefinitionView,
	appID string,
) registry.AppDefinitionView {
	t.Helper()
	for _, definition := range definitions {
		if definition.ID == appID {
			return definition
		}
	}
	t.Fatalf("App definition %q was not returned", appID)
	return registry.AppDefinitionView{}
}

func findAppInstallationByIDForVisibilityTest(
	t *testing.T,
	installations []AppInstallationView,
	installationID string,
) AppInstallationView {
	t.Helper()
	for _, installation := range installations {
		if installation.ID == installationID {
			return installation
		}
	}
	t.Fatalf("App installation %q was not returned", installationID)
	return AppInstallationView{}
}
