package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_display_name", now)
	member := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_display_name_member", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, admin, "display_name", now)

	_, err := appStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, member.UserID, admin.WorkspaceID, installation.ID, "Forbidden Name")
	require.ErrorIs(t, err, ErrNotFound)

	updated, err := appStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, installation.ID, "  Renamed App  ")
	require.NoError(t, err)
	require.Equal(t, "Renamed App", updated.AppName)

	const appID = "app_deploy_bot_target_display_name"
	var appName, definitionName, serviceMemberName, memberName string
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, appID).Scan(&appName))
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, appID).Scan(&definitionName))
	require.NoError(t, pool.QueryRow(ctx, `SELECT display_name FROM service_members WHERE service_installation_id = $1`, installation.ID).Scan(&serviceMemberName))
	require.NoError(t, pool.QueryRow(ctx, `SELECT display_name FROM members WHERE id = $1`, installation.MemberID).Scan(&memberName))
	require.Equal(t, "Renamed App", appName)
	require.Equal(t, "Renamed App", definitionName)
	require.Equal(t, "Renamed App", serviceMemberName)
	require.Equal(t, "Renamed App", memberName)

	catalogue, err := appStore.ListAppsForUser(ctx, admin.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	require.Condition(t, func() bool {
		for _, definition := range catalogue {
			if definition.ID == appID {
				return definition.Name == "Renamed App"
			}
		}
		return false
	})
	installations, err := appStore.ListAppInstallationsForUser(ctx, admin.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	require.Condition(t, func() bool {
		for _, candidate := range installations {
			if candidate.ID == installation.ID {
				return candidate.AppName == "Renamed App"
			}
		}
		return false
	})
	mustExec(t, pool, `UPDATE members SET status = 'disabled', updated_at = $2 WHERE id = $1`, installation.MemberID, time.Now().UTC())
	_, err = appStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, installation.ID, "Disabled Member Rename")
	require.ErrorIs(t, err, ErrConflict)
	require.ErrorContains(t, err, "active service-type member")
	mustExec(t, pool, `UPDATE members SET status = 'active', updated_at = $2 WHERE id = $1`, installation.MemberID, time.Now().UTC())
	mustExec(t, pool, `
INSERT INTO service_installation_requests (
    id, workspace_id, service_definition_id, service_kind, request_kind,
    requested_version, requested_config, base_config, requested_scopes,
    status, approval_required, request_reason, requested_by_member_id, decision_reason,
    service_installation_id, created_at, updated_at
) VALUES (
    'svcinstreq_bot_display_name_pending', $1, $2, $3, $4,
    '1.0.0', '{"environment":"production"}', '{"environment":"staging"}', '[]',
    $5, TRUE, 'switch environment', $6, '',
    $7, $8, $8
)
`, admin.WorkspaceID, installation.AppID, serviceactor.KindApp, AppInstallationRequestKindConfigure,
		AppInstallationRequestStatusPending, member.MemberID, installation.ID, time.Now().UTC())
	updated, err = appStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, installation.ID, "Allowed While Pending")
	require.NoError(t, err)
	require.Equal(t, "Allowed While Pending", updated.AppName)
	var pendingStatus string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT status
FROM service_installation_requests
WHERE id = 'svcinstreq_bot_display_name_pending'
`).Scan(&pendingStatus))
	require.Equal(t, AppInstallationRequestStatusPending, pendingStatus)

	_, err = appStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, installation.ID, "   ")
	require.ErrorIs(t, err, ErrBadRequest)
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, appID).Scan(&appName))
	require.Equal(t, "Allowed While Pending", appName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_display_name_manager_admin", now)
	manager := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_display_name_manager", now)
	created, err := appStore.InstallCustomAppForWorkspaceAdmin(
		ctx,
		admin.UserID,
		admin.WorkspaceID,
		directAppOnboardingParams("app-display-name-manager"),
	)
	require.NoError(t, err)

	assignment, err := appStore.AddAppManagerForUser(
		ctx,
		admin.UserID,
		admin.WorkspaceID,
		created.Installation.ID,
		manager.MemberID,
		now.Add(time.Second),
	)
	require.NoError(t, err)

	updated, err := appStore.UpdateAppDisplayNameForWorkspaceAdmin(
		ctx,
		manager.UserID,
		admin.WorkspaceID,
		created.Installation.ID,
		"Manager Renamed App",
	)
	require.NoError(t, err)
	require.Equal(t, "Manager Renamed App", updated.AppName)

	_, err = appStore.RemoveAppManagerForUser(
		ctx,
		admin.UserID,
		admin.WorkspaceID,
		created.Installation.ID,
		assignment.ID,
		assignment.Version,
		now.Add(2*time.Second),
	)
	require.NoError(t, err)
	_, err = appStore.UpdateAppDisplayNameForWorkspaceAdmin(
		ctx,
		manager.UserID,
		admin.WorkspaceID,
		created.Installation.ID,
		"Former Manager Rename",
	)
	require.ErrorIs(t, err, ErrNotFound)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_display_name_manager_changed", now)
	manager := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_display_name_manager_changed_manager", now)
	peer := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_display_name_manager_changed_peer", now)
	installation := installAppAppForTargetBindingTests(t, ctx, pool, appStore, admin, "display_name_manager_changed", now)
	assignment, err := appStore.AddAppManagerForUser(
		ctx, admin.UserID, admin.WorkspaceID, installation.ID, manager.MemberID, now.Add(time.Second),
	)
	require.NoError(t, err)
	var originalName string
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, installation.AppID).Scan(&originalName))

	guardedDB := &afterOuterAppManagerReadDB{
		DB:               pool,
		triggerAfterRead: 1,
		afterRead: func() {
			mustExec(t, pool, `
UPDATE extension_definition_managers
SET member_id = $2,
    granted_by_member_id = $2,
    version = version + 1,
    updated_at = $3
WHERE id = $1
`, assignment.ID, peer.MemberID, time.Now().UTC())
		},
	}
	guardedStore := New(guardedDB, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})

	_, err = guardedStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, manager.UserID, admin.WorkspaceID, installation.ID, "Renamed App")
	require.ErrorIs(t, err, ErrNotFound)
	outerReads, beganTx, sawLockedRead := guardedDB.snapshot()
	require.Equal(t, 1, outerReads)
	require.True(t, beganTx)
	require.True(t, sawLockedRead)
	var persistedName string
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, installation.AppID).Scan(&persistedName))
	require.Equal(t, originalName, persistedName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	appStore := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	firstAdmin := seedAppAdminFixture(t, pool, "bot_display_name_shared_first", now)
	secondAdmin := seedAppAdminFixture(t, pool, "bot_display_name_shared_second", now.Add(time.Second))
	firstInstallation := installAppAppForTargetBindingTestsWithTransports(
		t,
		ctx,
		pool,
		appStore,
		firstAdmin,
		"display_name_shared",
		now,
		[]string{serviceactor.RuntimeTransportKindCallback},
	)
	const appID = "app_deploy_bot_target_display_name_shared"
	mustExec(t, pool, `DELETE FROM extension_definition_managers WHERE service_definition_id = $1`, appID)
	mustExec(t, pool, `DELETE FROM extension_catalog_attributions WHERE service_definition_id = $1`, appID)
	secondInstallation, err := appStore.installAppForUserForTest(ctx, secondAdmin.UserID, secondAdmin.WorkspaceID, InstallAppParams{AppID: appID})
	require.NoError(t, err)
	require.NotEmpty(t, secondInstallation.ID)

	var originalName string
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, appID).Scan(&originalName))
	_, err = appStore.UpdateAppDisplayNameForWorkspaceAdmin(ctx, firstAdmin.UserID, firstAdmin.WorkspaceID, firstInstallation.ID, "Renamed App")
	require.ErrorIs(t, err, ErrConflict)
	require.ErrorContains(t, err, "associated with multiple workspaces")
	var persistedName string
	require.NoError(t, pool.QueryRow(ctx, `SELECT name FROM service_definitions WHERE id = $1`, appID).Scan(&persistedName))
	require.Equal(t, originalName, persistedName)
}
