package message

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

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

	for _, filename := range []string{"routing.go", "shared_query_builders.go"} {
		source, err := os.ReadFile(filename)
		require.NoError(t, err)
		require.NotContains(t, string(source), legacyTableNameForMessageTest("channel", "agent", "entries"), filename)
		require.NotContains(t, string(source), legacyTableNameForMessageTest("space", "participants"), filename)
		require.NotContains(t, string(source), legacyTableNameForMessageTest("space", "subscriptions"), filename)
		require.NotContains(t, string(source), legacyTableNameForMessageTest("targeted", "delivery", "events"), filename)
	}
}

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

	plan := FinalizeAgentRoutingPlan(AgentRoutingPlan{
		DurableRecipients: []AgentRecipient{
			{
				AgentID:        "agt-durable",
				MachineID:      "mach-durable",
				AgentMemberID:  "mem-durable",
				DeliveryReason: AgentDeliveryReasonSpaceDefault,
			},
		},
		WakeRecipients: []AgentRecipient{
			{
				AgentID:        "agt-wake-only",
				MachineID:      "mach-wake-only",
				AgentMemberID:  "mem-wake-only",
				DeliveryReason: AgentDeliveryReasonDirectMention,
				MentionToken:   "wake-only",
			},
		},
	})

	require.Equal(t, []string{"mem-durable", "mem-wake-only"}, recipientMemberIDs(plan.DurableRecipients))
	require.Equal(t, []string{"mem-wake-only"}, recipientMemberIDs(plan.WakeRecipients))
	require.Equal(t, AgentDeliveryReasonDirectMention, recipientReasonByMember(plan.DurableRecipients)["mem-wake-only"])
	require.Equal(t, AgentDeliveryReasonDirectMention, recipientReasonByMember(plan.WakeRecipients)["mem-wake-only"])
}

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

	plan := FinalizeAgentRoutingPlan(AgentRoutingPlan{
		DurableRecipients: []AgentRecipient{
			{
				AgentID:        "agt-shared",
				MachineID:      "mach-shared",
				AgentMemberID:  "mem-shared",
				DeliveryReason: AgentDeliveryReasonOwnerPrivateContext,
			},
		},
		WakeRecipients: []AgentRecipient{
			{
				AgentID:        "agt-shared",
				MachineID:      "mach-shared",
				AgentMemberID:  "mem-shared",
				DeliveryReason: AgentDeliveryReasonDirectMention,
				MentionToken:   "shared",
			},
			{
				AgentID:        "agt-shared-later",
				MachineID:      "mach-shared-later",
				AgentMemberID:  "mem-shared",
				DeliveryReason: AgentDeliveryReasonTaskCommandNotify,
			},
		},
	})

	require.Equal(t, []string{"mem-shared"}, recipientMemberIDs(plan.DurableRecipients))
	require.Equal(t, []string{"mem-shared"}, recipientMemberIDs(plan.WakeRecipients))
	require.Equal(t, AgentDeliveryReasonOwnerPrivateContext, recipientReasonByMember(plan.DurableRecipients)["mem-shared"])
	require.Equal(t, AgentDeliveryReasonDirectMention, recipientReasonByMember(plan.WakeRecipients)["mem-shared"])
}

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

	merged := MergeAgentRecipients(
		[]AgentRecipient{{
			AgentID:        "agt_1",
			MachineID:      "mac_1",
			AgentMemberID:  "mem_agent_1",
			DeliveryReason: AgentDeliveryReasonDirectMention,
			MentionToken:   "helper",
		}},
		[]AgentRecipient{{
			AgentID:        "agt_1",
			MachineID:      "mac_1",
			AgentMemberID:  "mem_agent_1",
			DeliveryReason: AgentDeliveryReasonTeamMention,
			MentionToken:   "core-team",
			TeamContexts: []TeamDeliveryContext{{
				TeamID:       "team_core",
				TeamMemberID: "wtm_core_agent",
				RoleLabel:    "Builder",
			}},
		}},
	)

	require.Len(t, merged, 1)
	require.Equal(t, AgentDeliveryReasonDirectMention, merged[0].DeliveryReason)
	require.Equal(t, "helper", merged[0].MentionToken)
	require.Len(t, merged[0].TeamContexts, 1)
	require.Equal(t, "team_core", merged[0].TeamContexts[0].TeamID)
}

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

	merged := MergeAgentRecipients(
		[]AgentRecipient{{
			AgentID:        "agt_1",
			MachineID:      "mac_1",
			AgentMemberID:  "mem_agent_1",
			DeliveryReason: AgentDeliveryReasonTeamMention,
			MentionToken:   "core-team",
			TeamContexts: []TeamDeliveryContext{{
				TeamID:       "team_core",
				TeamMemberID: "wtm_core_agent",
			}},
		}},
		[]AgentRecipient{{
			AgentID:        "agt_1",
			MachineID:      "mac_1",
			AgentMemberID:  "mem_agent_1",
			DeliveryReason: AgentDeliveryReasonTeamMention,
			MentionToken:   "platform-team",
			TeamContexts: []TeamDeliveryContext{{
				TeamID:       "team_platform",
				TeamMemberID: "wtm_platform_agent",
			}},
		}},
	)

	require.Len(t, merged, 1)
	require.Len(t, merged[0].TeamContexts, 2)
	require.ElementsMatch(t, []string{"team_core", "team_platform"}, []string{
		merged[0].TeamContexts[0].TeamID,
		merged[0].TeamContexts[1].TeamID,
	})
}
func TestLoadScopeMentionedAgentRecipientsUsesLiveOwnerAuthority(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	pool := startMessagingTestPool(t, ctx)
	now := time.Date(2026, 8, 1, 11, 0, 0, 0, time.UTC)
	const (
		workspaceID       = "wsp_routing_authority"
		ownerUserID       = "usr_routing_authority_owner"
		ownerMemberID     = "mem_routing_authority_owner"
		channelID         = "chn_routing_authority"
		authorMachineID   = "mach_routing_authority_author"
		authorAgentID     = "agt_routing_authority_author"
		authorMemberID    = "mem_routing_authority_author"
		authorName        = "routing-author"
		allowedMachineID  = "mach_routing_authority_allowed"
		allowedAgentID    = "agt_routing_authority_allowed"
		allowedMemberID   = "mem_routing_authority_allowed"
		allowedName       = "routing-allowed"
		publicMachineID   = "mach_routing_authority_public"
		publicAgentID     = "agt_routing_authority_public"
		publicMemberID    = "mem_routing_authority_public"
		publicName        = "routing-public"
		longMachineID     = "mach_routing_authority_long"
		longAgentID       = "agt_routing_authority_long"
		longMemberID      = "mem_routing_authority_long"
		longName          = "助手长"
		removedMemberID   = "mem_routing_authority_removed"
		archivedMemberID  = "mem_routing_authority_archived"
		closedMemberID    = "mem_routing_authority_closed"
		expiredMemberID   = "mem_routing_authority_expired"
		archivedAgentID   = "agt_routing_authority_archived"
		inactiveAgentID   = "agt_routing_authority_inactive_machine"
		inactiveMachineID = "mach_routing_authority_inactive"
		crossUserID       = "usr_routing_authority_cross"
		crossMemberID     = "mem_routing_authority_cross"
		crossMachineID    = "mach_routing_authority_cross"
		crossAgentID      = "agt_routing_authority_cross"
		crossAgentMember  = "mem_routing_authority_cross_agent"
	)

	insertMessagingWorkspace(t, ctx, pool, workspaceID, now)
	insertMessagingUser(t, ctx, pool, ownerUserID, "routing-authority-owner@example.com", now)
	insertMessagingHumanMember(t, ctx, pool, ownerMemberID, workspaceID, ownerUserID, now)
	insertMessagingChannel(t, ctx, pool, channelID, workspaceID, "routing-authority", ownerMemberID, "", now)

	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, authorMachineID, authorAgentID, authorMemberID, authorName, "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, allowedMachineID, allowedAgentID, allowedMemberID, allowedName, "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, publicMachineID, publicAgentID, publicMemberID, publicName, "public", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, longMachineID, longAgentID, longMemberID, longName, "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, "mach_routing_authority_removed", "agt_routing_authority_removed", removedMemberID, "routing-removed", "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, "mach_routing_authority_archived", "agt_routing_authority_archived", archivedMemberID, "routing-archived", "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, "mach_routing_authority_closed", "agt_routing_authority_closed", closedMemberID, "routing-closed", "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, "mach_routing_authority_expired", "agt_routing_authority_expired", expiredMemberID, "routing-expired", "private", now)
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, ownerUserID, channelID, ownerMemberID, inactiveMachineID, inactiveAgentID, "mem_routing_authority_inactive_machine", "routing-inactive-machine", "private", now)

	insertMessagingUser(t, ctx, pool, crossUserID, "routing-authority-cross@example.com", now)
	insertMessagingHumanMember(t, ctx, pool, crossMemberID, workspaceID, crossUserID, now)
	require.NoError(t, EnsureScopeMembership(ctx, pool, channelID, crossMemberID, "public", "", "active", 0, 0, now))
	insertRoutingAuthorityAgent(t, ctx, pool, workspaceID, crossUserID, channelID, crossMemberID, crossMachineID, crossAgentID, crossAgentMember, "routing-cross", "private", now)

	mustExecMessaging(t, ctx, pool, `
UPDATE scope_memberships
SET removed_at = $2
WHERE scope_id = $1 AND member_id = $3
`, channelID, now.Add(-time.Minute), removedMemberID)
	mustExecMessaging(t, ctx, pool, `
UPDATE scope_memberships
SET archived_at = $2
WHERE scope_id = $1 AND member_id = $3
`, channelID, now.Add(-time.Minute), archivedMemberID)
	mustExecMessaging(t, ctx, pool, `
UPDATE scope_memberships
SET effective_to_seq = 1
WHERE scope_id = $1 AND member_id = $2
`, channelID, closedMemberID)
	mustExecMessaging(t, ctx, pool, `
UPDATE scope_memberships
SET authority_to_at = $2
WHERE scope_id = $1 AND member_id = $3
`, channelID, now.Add(-time.Minute), expiredMemberID)
	mustExecMessaging(t, ctx, pool, `
UPDATE machines
SET status = 'inactive'
WHERE id = $1
`, inactiveMachineID)

	tokens := []string{
		allowedName,
		publicName,
		longName + "测试",
		authorName,
		"routing-removed",
		"routing-archived",
		"routing-closed",
		"routing-expired",
		"routing-inactive-machine",
		"routing-cross",
	}
	tx, err := pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	recipients, err := LoadScopeMentionedAgentRecipientsForTokens(ctx, tx, channelID, authorMemberID, tokens)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		allowedMemberID: AgentDeliveryReasonDirectMention,
		publicMemberID:  AgentDeliveryReasonDirectMention,
		longMemberID:    AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMemberForMessagingTest(recipients))
	for _, recipient := range recipients {
		if recipient.AgentMemberID == longMemberID {
			require.Equal(t, longName, recipient.MentionToken, "CJK suffix matching must retain the longest canonical token")
		}
	}
	require.NotContains(t, agentDeliveryReasonsByMemberForMessagingTest(recipients), authorMemberID, "self must be excluded")
	persistedRecipients, err := LoadScopeMentionedAgentRecipientsWithMetadata(ctx, tx, channelID, authorMemberID, "@"+allowedName, map[string]any{
		MentionMetadataKey: []any{map[string]any{"type": "agent", "id": allowedAgentID, "token": allowedName}},
	})
	require.NoError(t, err)
	require.Equal(t, map[string]string{allowedMemberID: AgentDeliveryReasonDirectMention}, agentDeliveryReasonsByMemberForMessagingTest(persistedRecipients))
	require.NoError(t, tx.Rollback(ctx))

	tx, err = pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	recipients, err = LoadScopeMentionedAgentRecipientsForTokens(ctx, tx, channelID, ownerMemberID, tokens)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		allowedMemberID: AgentDeliveryReasonDirectMention,
		publicMemberID:  AgentDeliveryReasonDirectMention,
		longMemberID:    AgentDeliveryReasonDirectMention,
		authorMemberID:  AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMemberForMessagingTest(recipients))
	require.NoError(t, tx.Rollback(ctx))

	mustExecMessaging(t, ctx, pool, `
UPDATE scope_memberships
SET removed_at = $3
WHERE scope_id = $1 AND member_id = $2
`, channelID, authorMemberID, now.Add(-time.Minute))
	tx, err = pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	defer tx.Rollback(ctx)
	recipients, err = LoadScopeMentionedAgentRecipientsForTokens(ctx, tx, channelID, authorMemberID, tokens)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		publicMemberID: AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMemberForMessagingTest(recipients))
	require.NoError(t, tx.Rollback(ctx))

	mustExecMessaging(t, ctx, pool, `
UPDATE members
SET status = 'inactive'
WHERE id = $1
`, ownerMemberID)
	tx, err = pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)
	defer tx.Rollback(ctx)
	recipients, err = LoadScopeMentionedAgentRecipientsForTokens(ctx, tx, channelID, ownerMemberID, tokens)
	require.NoError(t, err)
	require.Equal(t, map[string]string{
		publicMemberID: AgentDeliveryReasonDirectMention,
	}, agentDeliveryReasonsByMemberForMessagingTest(recipients))
}

func insertRoutingAuthorityAgent(t *testing.T, ctx context.Context, pool coredb.QueryExecer, workspaceID, userID, channelID, ownerMemberID, machineID, agentID, memberID, name, visibility string, now time.Time) {
	t.Helper()
	mustExecMessaging(t, ctx, pool, `
INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at)
VALUES ($1, $2, $3, $1, $4, $5, $5)
`, machineID, workspaceID, userID, machineID+"-key", now)
	insertMessagingAgentMember(t, ctx, pool, memberID, workspaceID, now)
	insertMessagingAgent(t, ctx, pool, agentID, workspaceID, machineID, memberID, name, now)
	membershipOwner := ""
	if visibility == "private" {
		membershipOwner = ownerMemberID
	}
	require.NoError(t, EnsureScopeMembership(ctx, pool, channelID, memberID, visibility, membershipOwner, "auto-wake", 0, 0, now))
}
