package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 8, 6, 0, 0, 0, time.UTC)

	owner := seedAppAdminFixture(t, pool, "share_card_owner", now)
	member := seedWorkspaceMemberFixture(t, pool, owner.WorkspaceID, "share_card_member", now)
	guest := seedWorkspaceMemberFixture(t, pool, owner.WorkspaceID, "share_card_guest", now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, owner.AllChannelID, "all", "", "public", owner.MemberID, member.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, owner.AllChannelID, "all", "", "public", owner.MemberID, guest.MemberID, now)

	const privateChannelID = "chn_share_card_private"
	seedChannelParticipantProjectionWithDisplayNameForTests(t, pool, owner.WorkspaceID, privateChannelID, "channel-abc12345", "Share Card Room", "private room shared by card", "private", owner.MemberID, owner.MemberID, now)

	cardMessage, _, err := store.CreateChannelShareCardForUser(ctx, model.User{ID: owner.UserID, Name: "share_card_owner", DisplayName: "Share Card Owner"}, owner.WorkspaceID, privateChannelID, owner.AllChannelID)
	require.NoError(t, err)
	require.Equal(t, owner.AllChannelID, cardMessage.ChannelID)
	require.Equal(t, message.MessageTypeChannelShareCard, cardMessage.MessageType)
	fallback, ok := cardMessage.Metadata["channelShare"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "Share Card Room", fallback["channelName"])
	require.Equal(t, "Share Card Room", fallback["channelDisplayName"])
	require.Equal(t, "channel-abc12345", fallback["channelSlug"])
	require.NotContains(t, cardMessage.Metadata, "card")
	var scopeShareCardCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_share_cards
WHERE workspace_id = $1
  AND scope_id = $2
  AND message_id = $3
  AND source_scope_id = $4
`, owner.WorkspaceID, owner.AllChannelID, cardMessage.ID, privateChannelID).Scan(&scopeShareCardCount))
	require.Equal(t, 1, scopeShareCardCount)

	memberCard, err := store.LoadChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, cardMessage.ID, memberCard.MessageID)
	require.Equal(t, privateChannelID, memberCard.ChannelID)
	require.Equal(t, owner.AllChannelID, memberCard.CardChannelID)
	require.Equal(t, "Share Card Room", memberCard.Name)
	require.Equal(t, "private room shared by card", memberCard.Description)
	require.Equal(t, "joinable", memberCard.Status)
	require.True(t, memberCard.CanJoin)
	require.False(t, memberCard.CanOpen)
	require.False(t, memberCard.CanRevoke)
	require.False(t, memberCard.Revoked)

	joinedChannel, err := store.JoinChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, privateChannelID, joinedChannel.ChannelID)
	require.Equal(t, "channel-abc12345", joinedChannel.Slug)
	require.Equal(t, "Share Card Room", joinedChannel.DisplayName)
	require.Equal(t, "private", joinedChannel.Visibility)
	require.True(t, joinedChannel.Joined)

	memberCard, err = store.LoadChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "joined", memberCard.Status)
	require.True(t, memberCard.Joined)
	require.False(t, memberCard.CanJoin)
	require.True(t, memberCard.CanOpen)

	_, err = store.RevokeChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.ErrorIs(t, err, ErrForbidden)

	revokedCard, err := store.RevokeChannelShareCardForUser(ctx, owner.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.True(t, revokedCard.Revoked)

	guestCard, err := store.LoadChannelShareCardForUser(ctx, guest.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "revoked", guestCard.Status)
	require.True(t, guestCard.Revoked)
	require.False(t, guestCard.CanJoin)

	_, err = store.JoinChannelShareCardForUser(ctx, guest.UserID, owner.WorkspaceID, cardMessage.ID)
	require.ErrorIs(t, err, ErrGone)

	memberCard, err = store.LoadChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "joined", memberCard.Status)
	require.True(t, memberCard.CanOpen)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 22, 2, 45, 0, 0, time.UTC)

	owner := seedAppAdminFixture(t, pool, "public_share_card_owner", now)
	member := seedWorkspaceMemberFixture(t, pool, owner.WorkspaceID, "public_share_card_member", now)
	guest := seedWorkspaceMemberFixture(t, pool, owner.WorkspaceID, "public_share_card_guest", now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, owner.AllChannelID, "all", "", "public", owner.MemberID, member.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, owner.AllChannelID, "all", "", "public", owner.MemberID, guest.MemberID, now)

	const publicChannelID = "chn_share_card_public"
	seedChannelParticipantProjectionWithDisplayNameForTests(t, pool, owner.WorkspaceID, publicChannelID, "public-room", "Public Room", "public room shared by card", "public", owner.MemberID, owner.MemberID, now)

	cardMessage, _, err := store.CreateChannelShareCardForUser(ctx, model.User{ID: owner.UserID, Name: "public_share_card_owner", DisplayName: "Public Share Owner"}, owner.WorkspaceID, publicChannelID, owner.AllChannelID)
	require.NoError(t, err)
	require.Equal(t, owner.AllChannelID, cardMessage.ChannelID)
	require.Equal(t, message.MessageTypeChannelShareCard, cardMessage.MessageType)
	fallback, ok := cardMessage.Metadata["channelShare"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "public", fallback["channelVisibility"])

	memberCard, err := store.LoadChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, publicChannelID, memberCard.ChannelID)
	require.Equal(t, "public", memberCard.Visibility)
	require.Equal(t, "joinable", memberCard.Status)
	require.True(t, memberCard.CanJoin)
	require.False(t, memberCard.CanOpen)

	joinedChannel, err := store.JoinChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, publicChannelID, joinedChannel.ChannelID)
	require.Equal(t, "public-room", joinedChannel.Slug)
	require.Equal(t, "public", joinedChannel.Visibility)
	require.True(t, joinedChannel.Joined)

	memberCard, err = store.LoadChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "joined", memberCard.Status)
	require.True(t, memberCard.CanOpen)

	revokedCard, err := store.RevokeChannelShareCardForUser(ctx, owner.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "public", revokedCard.Visibility)
	require.True(t, revokedCard.Revoked)

	_, err = store.JoinChannelShareCardForUser(ctx, guest.UserID, owner.WorkspaceID, cardMessage.ID)
	require.ErrorIs(t, err, ErrGone)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 22, 11, 45, 0, 0, time.UTC)

	owner := seedAppAdminFixture(t, pool, "reshare_card_owner", now)
	reader := seedWorkspaceMemberFixture(t, pool, owner.WorkspaceID, "reshare_card_reader", now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, owner.AllChannelID, "all", "", "public", owner.MemberID, reader.MemberID, now)

	const sourceChannelID = "chn_share_card_reshare_source"
	const targetChannelID = "chn_share_card_reshare_target"
	const nextTargetChannelID = "chn_share_card_reshare_next_target"
	seedChannelParticipantProjectionWithDisplayNameForTests(t, pool, owner.WorkspaceID, sourceChannelID, "reshare-source", "Reshare Source", "source shared by card", "public", owner.MemberID, owner.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, targetChannelID, "reshare-target", "target for reshared card", "public", owner.MemberID, owner.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, targetChannelID, "reshare-target", "target for reshared card", "public", owner.MemberID, reader.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, nextTargetChannelID, "reshare-next-target", "second target for reshared card", "public", owner.MemberID, owner.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, nextTargetChannelID, "reshare-next-target", "second target for reshared card", "public", owner.MemberID, reader.MemberID, now)

	cardMessage, _, err := store.CreateChannelShareCardForUser(ctx, model.User{ID: owner.UserID, Name: "reshare_card_owner"}, owner.WorkspaceID, sourceChannelID, owner.AllChannelID)
	require.NoError(t, err)

	resharedMessage, _, err := store.ShareChannelShareCardForUser(ctx, model.User{ID: owner.UserID, Name: "reshare_card_owner"}, owner.WorkspaceID, cardMessage.ID, targetChannelID)
	require.NoError(t, err)
	require.Equal(t, targetChannelID, resharedMessage.ChannelID)

	secondResharedMessage, _, err := store.ShareChannelShareCardForUser(ctx, model.User{ID: owner.UserID, Name: "reshare_card_owner"}, owner.WorkspaceID, resharedMessage.ID, nextTargetChannelID)
	require.NoError(t, err)
	require.Equal(t, nextTargetChannelID, secondResharedMessage.ChannelID)

	var rootMessageID string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COALESCE(source_message_id, '')
FROM scope_share_cards
WHERE workspace_id = $1
  AND message_id = $2
`, owner.WorkspaceID, secondResharedMessage.ID).Scan(&rootMessageID))
	require.Equal(t, cardMessage.ID, rootMessageID)

	readerCard, err := store.LoadChannelShareCardForUser(ctx, reader.UserID, owner.WorkspaceID, secondResharedMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "joinable", readerCard.Status)
	require.True(t, readerCard.CanJoin)

	revokedCard, err := store.RevokeChannelShareCardForUser(ctx, owner.UserID, owner.WorkspaceID, resharedMessage.ID)
	require.NoError(t, err)
	require.True(t, revokedCard.Revoked)

	rootCard, err := store.LoadChannelShareCardForUser(ctx, reader.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "revoked", rootCard.Status)

	nextCard, err := store.LoadChannelShareCardForUser(ctx, reader.UserID, owner.WorkspaceID, secondResharedMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "revoked", nextCard.Status)
	require.False(t, nextCard.CanJoin)

	_, err = store.JoinChannelShareCardForUser(ctx, reader.UserID, owner.WorkspaceID, secondResharedMessage.ID)
	require.ErrorIs(t, err, ErrGone)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 8, 2, 5, 0, 0, 0, time.UTC)

	owner := seedAppAdminFixture(t, pool, "team_share_card_owner", now)
	member := seedWorkspaceMemberFixture(t, pool, owner.WorkspaceID, "team_share_card_member", now)
	seedChannelParticipantProjectionForTests(t, pool, owner.WorkspaceID, owner.AllChannelID, "all", "", "public", owner.MemberID, member.MemberID, now)

	const sourceChannelID = "chn_historical_team_share_card"
	seedChannelParticipantProjectionWithDisplayNameForTests(
		t,
		pool,
		owner.WorkspaceID,
		sourceChannelID,
		"historical-team-share",
		"Historical Team Share",
		"created before Team share cards were closed",
		"private",
		owner.MemberID,
		owner.MemberID,
		now,
	)
	cardMessage, _, err := store.CreateChannelShareCardForUser(
		ctx,
		model.User{ID: owner.UserID, Name: "team_share_card_owner"},
		owner.WorkspaceID,
		sourceChannelID,
		owner.AllChannelID,
	)
	require.NoError(t, err)

	// Simulate a share card written before the source channel was classified as
	// a Team subsystem channel. The durable card remains visible in history but
	// must not expose Team membership or management actions.
	mustExec(t, pool, `
UPDATE scope_channel_profiles
SET subsystem = 'team'
WHERE scope_id = $1
`, sourceChannelID)

	card, err := store.LoadChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.NoError(t, err)
	require.Equal(t, "unavailable", card.Status)
	require.False(t, card.CanJoin)
	require.False(t, card.CanOpen)
	require.False(t, card.CanRevoke)

	_, err = store.JoinChannelShareCardForUser(ctx, member.UserID, owner.WorkspaceID, cardMessage.ID)
	require.ErrorIs(t, err, ErrNotFound)
	var memberCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND archived_at IS NULL
  AND effective_to_seq IS NULL
`, sourceChannelID, member.MemberID).Scan(&memberCount))
	require.Zero(t, memberCount)
	_, err = store.RevokeChannelShareCardForUser(ctx, owner.UserID, owner.WorkspaceID, cardMessage.ID)
	require.ErrorIs(t, err, ErrNotFound)
	_, _, err = store.ShareChannelShareCardForUser(
		ctx,
		model.User{ID: owner.UserID, Name: "team_share_card_owner"},
		owner.WorkspaceID,
		cardMessage.ID,
		owner.AllChannelID,
	)
	require.ErrorIs(t, err, ErrBadRequest)
	_, _, err = store.CreateChannelShareCardForUser(
		ctx,
		model.User{ID: owner.UserID, Name: "team_share_card_owner"},
		owner.WorkspaceID,
		sourceChannelID,
		owner.AllChannelID,
	)
	require.ErrorIs(t, err, ErrBadRequest)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 5, 13, 13, 5, 0, 0, time.UTC)

	owner := seedAppAdminFixture(t, pool, "share_card_sender_display_name", now)
	mustExec(t, pool, `
UPDATE members
SET display_name = 'Old Member Name'
WHERE id = $1
`, owner.MemberID)

	const privateChannelID = "chn_share_card_display_name_private"
	seedChannelParticipantProjectionWithDisplayNameForTests(t, pool, owner.WorkspaceID, privateChannelID, "channel-xyz98765", "Display Name Room", "private room for display-name precedence", "private", owner.MemberID, owner.MemberID, now)

	cardMessage, _, err := store.CreateChannelShareCardForUser(ctx, model.User{
		ID:          owner.UserID,
		Name:        "share_card_sender_display_name",
		DisplayName: "New User Display",
		AvatarURL:   "https://example.com/share-card-owner.png",
	}, owner.WorkspaceID, privateChannelID, owner.AllChannelID)
	require.NoError(t, err)
	require.Equal(t, "New User Display", cardMessage.AuthorProfile.DisplayName)
	require.Equal(t, "https://example.com/share-card-owner.png", cardMessage.AuthorProfile.AvatarURL)
}
