package converter

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

func TestToAppAttachmentsPreservesNilForEmptyInput(t *testing.T) {
	if got := ToAppAttachments(nil); got != nil {
		t.Fatalf("ToAppAttachments(nil) = %#v, want nil", got)
	}
	if got := ToAppAttachments([]attachment.AppAttachment{}); got != nil {
		t.Fatalf("ToAppAttachments(empty) = %#v, want nil", got)
	}

	got := ToAppAttachments([]attachment.AppAttachment{{ID: "att-1", Filename: "a.png", ContentType: "image/png", SizeBytes: 123, ThumbnailURL: "/thumb"}})
	if len(got) != 1 || got[0].ID != "att-1" || got[0].Filename != "a.png" || got[0].ContentType != "image/png" || got[0].SizeBytes != 123 || got[0].ThumbnailURL != "/thumb" {
		t.Fatalf("ToAppAttachments(non-empty) = %#v, want mapped attachment", got)
	}
}

func TestToPersonalStickersPreservesEmptySliceForEmptyInput(t *testing.T) {
	if got := ToPersonalStickers(nil); got == nil || len(got) != 0 {
		t.Fatalf("ToPersonalStickers(nil) = %#v, want non-nil empty slice", got)
	}
	if got := ToPersonalStickers([]attachment.PersonalSticker{}); got == nil || len(got) != 0 {
		t.Fatalf("ToPersonalStickers(empty) = %#v, want non-nil empty slice", got)
	}
}

func TestToMessageReactionAggregatesPreservesEmptySliceBehavior(t *testing.T) {
	if got := ToMessageReactionAggregates(nil); got == nil || len(got) != 0 {
		t.Fatalf("ToMessageReactionAggregates(nil) = %#v, want non-nil empty slice", got)
	}

	got := ToMessageReactionAggregates([]message.MessageReactionAggregate{{
		Emoji: "👍",
		Count: 1,
		Reactors: []message.MessageReactionReactor{{
			AuthorProfile: message.AuthorProfile{
				ActorType:   message.AuthorActorTypeHuman,
				MemberID:    "mem_human",
				TargetID:    "andc5vq",
				DisplayName: "郑少峰",
				AvatarURL:   "/avatars/u.png",
			},
		}},
	}})
	if len(got) != 1 {
		t.Fatalf("ToMessageReactionAggregates(non-empty) len = %d, want 1", len(got))
	}
	if got[0].Emoji != "👍" || got[0].Count != 1 {
		t.Fatalf("ToMessageReactionAggregates(non-empty) = %#v, want mapped aggregate", got[0])
	}
	require.Equal(t, "郑少峰", got[0].Reactors[0].AuthorProfile.DisplayName)
	require.Equal(t, "/avatars/u.png", got[0].Reactors[0].AuthorProfile.AvatarURL)
}

func TestToMessageReactionAggregatesPreservesEmptyReactorsSlice(t *testing.T) {
	got := ToMessageReactionAggregates([]message.MessageReactionAggregate{{Emoji: "👍", Count: 1}})
	if got[0].Reactors == nil || len(got[0].Reactors) != 0 {
		t.Fatalf("ToMessageReactionAggregate empty reactors = %#v, want non-nil empty slice", got[0].Reactors)
	}
}

func TestToChannelPrivateAgentSummaryDerivesOwnerCompatFromProfile(t *testing.T) {
	got := ToChannelPrivateAgentSummary(message.ChannelPrivateAgentSummary{
		ID:          "agt_private",
		Name:        "private-agent",
		DisplayName: "Private Agent",
		MemberID:    "mem_agent",
		Model:       "gpt-5.4",
		Visibility:  "private",
		OwnerProfile: message.AuthorProfile{
			ActorType:   message.AuthorActorTypeHuman,
			PrincipalID: "usr_resolved",
			MemberID:    "mem_resolved",
			TargetID:    "mem_resolved",
			DisplayName: "Resolved Owner",
		},
		OwnerMemberID:    "mem_legacy",
		OwnerUserID:      "usr_legacy",
		OwnerName:        "Legacy Owner",
		OwnerDisplayName: "Legacy Owner",
	})

	require.NotNil(t, got.OwnerProfile)
	require.Equal(t, "mem_resolved", got.OwnerProfile.MemberID)
	require.Equal(t, "usr_resolved", got.OwnerProfile.PrincipalID)
	require.Equal(t, "Resolved Owner", got.OwnerProfile.DisplayName)
	require.Equal(t, "mem_resolved", got.OwnerMemberID)
	require.Equal(t, "usr_resolved", got.OwnerUserID)
	require.Equal(t, "Resolved Owner", got.OwnerName)
	require.Equal(t, "Resolved Owner", got.OwnerDisplayName)
	require.Equal(t, "gpt-5.4", got.Model)
	require.Equal(t, "private", got.Visibility)
}

func TestToChannelMemberSummaryIncludesAgentSummaryFields(t *testing.T) {
	got := ToChannelMemberSummary(message.ChannelMemberSummary{
		ID:          "agt_public",
		Name:        "public-agent",
		DisplayName: "Public Agent",
		MemberID:    "mem_agent",
		OwnerProfile: message.AuthorProfile{
			ActorType:   message.AuthorActorTypeHuman,
			PrincipalID: "usr_owner",
			MemberID:    "mem_owner",
			TargetID:    "mem_owner",
			DisplayName: "Agent Owner",
		},
		OwnerUserID: "usr_owner",
		Model:       "gpt-5.4",
		Visibility:  "public",
	})

	require.NotNil(t, got.OwnerProfile)
	require.Equal(t, "Agent Owner", got.OwnerProfile.DisplayName)
	require.Equal(t, "gpt-5.4", got.Model)
	require.Equal(t, "public", got.Visibility)
}

func TestToReplyThreadSummaryPreservesEmptyParticipantIDsSlice(t *testing.T) {
	value := ToReplyThreadSummary(message.ReplyThreadSummary{
		ThreadChannelID: "chn_thread",
		ReplyCount:      0,
	})
	require.NotNil(t, value.ParticipantIDs)
	require.Empty(t, value.ParticipantIDs)

	raw, err := json.Marshal(value)
	require.NoError(t, err)
	require.JSONEq(t, `{
		"threadChannelId": "chn_thread",
		"replyCount": 0,
		"participantIds": [],
		"participantCount": 0,
		"replyAuthorIds": [],
		"replyAuthors": []
	}`, string(raw))
}

func TestToReplyThreadSummaryIncludesViewerSubscriptionState(t *testing.T) {
	value := ToReplyThreadSummary(message.ReplyThreadSummary{
		ThreadChannelID:   "chn_thread",
		SubscriptionState: message.HumanScopeSubscriptionStateQuiet,
	})

	require.Equal(t, message.HumanScopeSubscriptionStateQuiet, value.SubscriptionState)
	raw, err := json.Marshal(value)
	require.NoError(t, err)
	require.Contains(t, string(raw), `"subscriptionState":"quiet"`)
}

func TestToSubscribedThreadPreservesEmptyParticipantIDsSlice(t *testing.T) {
	value := ToSubscribedThread(message.SubscribedThread{
		ThreadChannelID:      "chn_thread",
		ParentChannelID:      "chn_parent",
		ParentChannelType:    "channel",
		ParentLabel:          "agentteam",
		ParentMessageID:      "msg_parent",
		ParentMessageShortID: "00000001",
	})
	require.NotNil(t, value.ParticipantIDs)
	require.Empty(t, value.ParticipantIDs)

	raw, err := json.Marshal(value)
	require.NoError(t, err)
	require.JSONEq(t, `{
		"threadChannelId": "chn_thread",
		"parentChannelId": "chn_parent",
		"parentChannelType": "channel",
		"parentLabel": "agentteam",
		"parentMessageId": "msg_parent",
		"parentMessageShortId": "00000001",
		"title": "agentteam",
		"participantIds": [],
		"participants": [],
		"participantCount": 0,
		"replyAuthorIds": [],
		"replyAuthors": [],
		"replyCount": 0
	}`, string(raw))
}

func TestToAppMessageDeliverySummaryPreviewNil(t *testing.T) {
	if got := ToAppMessageDeliverySummaryPreview(nil); got != nil {
		t.Fatalf("ToAppMessageDeliverySummaryPreview(nil) = %#v, want nil", got)
	}
}

func TestToAppMessagePreservesClientMessageID(t *testing.T) {
	got := ToAppMessage(message.AppMessage{
		ID:                "msg_1",
		ChannelType:       "thread",
		ClientMessageID:   "client-1",
		ParentChannelID:   "chn_parent_1",
		ParentChannelType: "channel",
		ParentMessageID:   "msg_parent_1",
	})
	if got.ClientMessageID != "client-1" {
		t.Fatalf("ClientMessageID = %q, want client-1", got.ClientMessageID)
	}
	require.Equal(t, "thread", got.ChannelType)
	require.Equal(t, "chn_parent_1", got.ParentChannelID)
	require.Equal(t, "channel", got.ParentChannelType)
	require.Equal(t, "msg_parent_1", got.ParentMessageID)
}

func TestToAppMessageIncludesTaskContext(t *testing.T) {
	got := ToAppMessage(message.AppMessage{
		ID:          "msg_task_1",
		ChannelType: "task",
		TaskContext: &message.AppMessageTaskContext{
			ChannelID:  "chn_tasks",
			TaskNumber: 42,
		},
	})

	require.NotNil(t, got.TaskContext)
	require.Equal(t, "chn_tasks", got.TaskContext.ChannelID)
	require.EqualValues(t, 42, got.TaskContext.TaskNumber)
}

func TestToAppMessagePreservesEditState(t *testing.T) {
	editedAt := time.Date(2026, 6, 19, 12, 0, 0, 0, time.UTC)

	got := ToAppMessage(message.AppMessage{
		ID:               "msg_edited",
		EditedAt:         &editedAt,
		EditedByMemberID: "mem_editor",
		EditRevision:     2,
	})

	require.NotNil(t, got.EditedAt)
	require.True(t, got.EditedAt.Equal(editedAt))
	require.Equal(t, "mem_editor", got.EditedByMemberID)
	require.EqualValues(t, 2, got.EditRevision)
}

func TestToAppMessageEmitsAuthorProfile(t *testing.T) {
	storeMessage := message.AppMessage{
		ID:      "msg_1",
		Content: "hello",
	}
	storeMessage.ApplyAuthorProfile(message.AuthorProfile{
		ActorType:   "service",
		PrincipalID: "app_install_1",
		MemberID:    "mem_app",
		TargetID:    "gitea",
		DisplayName: "Gitea",
		AvatarURL:   "https://gitea.example/avatar.svg",
		ServiceKind: "app",
		ServiceSlug: "gitea",
	})

	got := ToAppMessage(storeMessage)
	require.NotNil(t, got.AuthorProfile)
	require.Equal(t, "service", got.AuthorProfile.ActorType)
	require.Equal(t, "app_install_1", got.AuthorProfile.PrincipalID)
	require.Equal(t, "mem_app", got.AuthorProfile.MemberID)
	require.Equal(t, "gitea", got.AuthorProfile.TargetID)
	require.Equal(t, "Gitea", got.AuthorProfile.DisplayName)
	require.Equal(t, "https://gitea.example/avatar.svg", got.AuthorProfile.AvatarURL)
	require.Equal(t, "app", got.AuthorProfile.ServiceKind)
	require.Equal(t, "gitea", got.AuthorProfile.ServiceSlug)
	require.Equal(t, "app", got.AuthorProfile.Badge)
}

func TestToAppMessageCarriesOnlyValidatedCardPresentation(t *testing.T) {
	presentation := domain.NewWorkflowRunApprovalPresentation(
		"pending", "pln_1", "plv_1", "wri_1", "review", "Agent-authored summary",
	)
	encoded, err := json.Marshal(map[string]any{
		"schemaVersion": "1",
		"template":      "workflow_run_approval",
		"titleKey":      "workflow_run_approval_pending",
		"status":        "pending",
		"presentation":  presentation,
	})
	require.NoError(t, err)
	got := ToAppMessage(message.AppMessage{ID: "msg_card", AppNotificationCard: encoded})
	require.NotNil(t, got.AppNotificationCard)
	require.NotEmpty(t, got.AppNotificationCard.Presentation)

	malformed, err := json.Marshal(map[string]any{
		"schemaVersion": "1",
		"template":      "workflow_run_approval",
		"presentation":  map[string]any{"version": 1, "key": "client.key", "args": map[string]any{}},
	})
	require.NoError(t, err)
	got = ToAppMessage(message.AppMessage{ID: "msg_malformed_card", AppNotificationCard: malformed})
	require.NotNil(t, got.AppNotificationCard)
	require.Empty(t, got.AppNotificationCard.Presentation)
}

func TestToAppMessageFiltersInternalMentionMetadata(t *testing.T) {
	got := ToAppMessage(message.AppMessage{
		ID: "msg_1",
		Metadata: map[string]any{
			message.MentionMetadataKey: []any{
				map[string]any{"type": "agent", "id": "agt_private", "token": "private"},
			},
			"ownerQuote": true,
		},
	})

	require.Equal(t, map[string]any{"ownerQuote": true}, got.Metadata)
}

func TestToReceiveMessagePreservesChannelDisplayNames(t *testing.T) {
	got := ToReceiveMessage(message.ReceiveMessage{
		ChannelName:              "1",
		ChannelDisplayName:       "中文频道 1",
		ParentChannelName:        "ops",
		ParentChannelDisplayName: "Operations",
	})

	if got.ChannelDisplayName != "中文频道 1" {
		t.Fatalf("ChannelDisplayName = %q, want display name", got.ChannelDisplayName)
	}
	if got.ParentChannelDisplayName != "Operations" {
		t.Fatalf("ParentChannelDisplayName = %q, want parent display name", got.ParentChannelDisplayName)
	}
}

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

	got := ToReceiveMessage(message.ReceiveMessage{
		MessageID: "msg_1",
		Source: message.ContributionSource{
			Kind:    message.ContributionSourceKindScheduleRun,
			RefType: message.ContributionSourceRefTypeAutomationRun,
			RefID:   "arun_1",
			RunID:   "arun_1",
			Label:   "daily nudge",
		},
	})

	require.NotNil(t, got.Source)
	require.Equal(t, message.ContributionSourceKindScheduleRun, got.Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeAutomationRun, got.Source.RefType)
	require.Equal(t, "arun_1", got.Source.RefID)
	require.Equal(t, "daily nudge", got.Source.Label)
}

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

	got := ToOldDaemonReceiveMessage(message.ReceiveMessage{
		MessageID: " msg_1 ",
		Source: message.ContributionSource{
			Kind:    " schedule_run ",
			RefType: " automation_run ",
			RefID:   " arun_1 ",
			RunID:   " arun_1 ",
			Label:   " daily nudge ",
		},
	})

	require.NotNil(t, got.Source)
	require.Equal(t, message.ContributionSourceKindScheduleRun, got.Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeAutomationRun, got.Source.RefType)
	require.Equal(t, "arun_1", got.Source.RefID)
	require.Equal(t, "arun_1", got.Source.RunID)
	require.Equal(t, "daily nudge", got.Source.Label)
}

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

	got := ToHistoryMessages([]message.HistoryMessage{{
		ID: "msg_1",
		Source: message.ContributionSource{
			Kind:      message.ContributionSourceKindProviderEvent,
			RefType:   message.ContributionSourceRefTypeServiceEventDelivery,
			RefID:     "svced_1",
			RunID:     "svced_1",
			EventID:   "evt_1",
			EventType: "gitea_pull_request.synchronized",
			Label:     "Gitea PR watch",
		},
	}})

	require.Len(t, got, 1)
	require.NotNil(t, got[0].Source)
	require.Equal(t, message.ContributionSourceKindProviderEvent, got[0].Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeServiceEventDelivery, got[0].Source.RefType)
	require.Equal(t, "svced_1", got[0].Source.RefID)
	require.Equal(t, "evt_1", got[0].Source.EventID)
	require.Equal(t, "gitea_pull_request.synchronized", got[0].Source.EventType)
	require.Equal(t, "Gitea PR watch", got[0].Source.Label)
}

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

	got := ToReceiveMessage(message.ReceiveMessage{
		MessageID: "msg_1",
		AuthorProfile: message.AuthorProfile{
			ActorType:   message.AuthorActorTypeAgent,
			PrincipalID: "agt_profile",
			MemberID:    "mem_profile",
			TargetID:    "agent-profile",
			DisplayName: "Profile Agent",
		},
	})

	require.NotNil(t, got.AuthorProfile)
	require.Equal(t, message.AuthorActorTypeAgent, got.AuthorProfile.ActorType)
	require.Equal(t, "Profile Agent", got.AuthorProfile.DisplayName)
	require.Equal(t, message.AuthorActorTypeAgent, got.SenderType)
	require.Equal(t, "agent-profile", got.SenderName)
}

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

	got := ToReceiveMessage(message.ReceiveMessage{
		MessageID: "msg_team",
		TeamContexts: []message.ReceiveTeamContext{{
			TeamID:            "team_core",
			TeamHandle:        "core-team",
			TeamDisplayName:   "Core Team",
			TeamDescription:   "Core ownership",
			TeamMemberID:      "wtm_agent",
			RoleLabel:         "Builder",
			RoleOverlayPrompt: "Build the task.",
			MemberRoster: []message.ReceiveTeamMemberContext{{
				TeamMemberID:      "wtm_agent",
				ActorKind:         "agent",
				ActorID:           "mem_agent",
				DisplayName:       "AgentTeam Agent",
				Handle:            "agentteam-agent",
				RoleLabel:         "Builder",
				RoleOverlayPrompt: "Build the task.",
				WakeOnTeamMention: true,
				DeliveryKind:      "wake",
				Self:              true,
			}},
		}},
	})

	require.Len(t, got.TeamContexts, 1)
	context := got.TeamContexts[0]
	require.Equal(t, "team_core", context.TeamID)
	require.Equal(t, "core-team", context.TeamHandle)
	require.Equal(t, "Core Team", context.TeamDisplayName)
	require.Equal(t, "Build the task.", context.RoleOverlayPrompt)
	require.Len(t, context.MemberRoster, 1)
	require.Equal(t, "wtm_agent", context.MemberRoster[0].TeamMemberID)
	require.True(t, context.MemberRoster[0].Self)
	require.Equal(t, "Build the task.", context.MemberRoster[0].RoleOverlayPrompt)
	require.True(t, context.MemberRoster[0].WakeOnTeamMention)

	raw, err := json.Marshal(got)
	require.NoError(t, err)
	var decoded map[string]any
	require.NoError(t, json.Unmarshal(raw, &decoded))
	require.Contains(t, decoded, "teamContexts")
	teamContexts := decoded["teamContexts"].([]any)
	memberRoster := teamContexts[0].(map[string]any)["memberRoster"].([]any)
	require.Equal(t, "wtm_agent", memberRoster[0].(map[string]any)["teamMemberId"])
	require.NotContains(t, string(raw), "Version")
	require.NotContains(t, string(raw), "roleOverlayVersion")
}

func TestToWorkspaceChannelsPreservesDisplayName(t *testing.T) {
	got := ToWorkspaceChannels([]agent.WorkspaceChannel{{
		Name:        "1",
		DisplayName: "中文频道 1",
		Joined:      true,
	}})

	if len(got) != 1 {
		t.Fatalf("ToWorkspaceChannels len = %d, want 1", len(got))
	}
	if got[0].DisplayName != "中文频道 1" {
		t.Fatalf("DisplayName = %q, want display name", got[0].DisplayName)
	}
}

func TestToMessageDeliveryDiagnosticsUsesStableDTOJSONKeys(t *testing.T) {
	value := ToMessageDeliveryDiagnostics(message.MessageDeliveryDiagnostics{
		Summary: message.MessageDeliveryDiagnosticsSummary{
			OwnerPrivateRequired: true,
		},
		Agents: []message.MessageDeliveryDiagnosticsAgent{{
			SubscriptionState:       "joined",
			AgentActivity:           "running",
			AgentActivityDetail:     "handling",
			ScopeStatus:             "included",
			ScopeExplanation:        "same channel",
			ExecutionResult:         "start",
			OwnerPrivateState:       "allowed",
			OwnerPrivateExplanation: "owned",
		}},
	})
	raw, err := json.Marshal(value)
	if err != nil {
		t.Fatalf("marshal diagnostics DTO: %v", err)
	}

	var decoded map[string]any
	if err := json.Unmarshal(raw, &decoded); err != nil {
		t.Fatalf("unmarshal diagnostics DTO: %v", err)
	}
	summary := decoded["summary"].(map[string]any)
	if summary["ownerPrivateRequired"] != true {
		t.Fatalf("summary JSON = %#v, want ownerPrivateRequired", summary)
	}
	agents := decoded["agents"].([]any)
	agent := agents[0].(map[string]any)
	for _, key := range []string{
		"subscriptionState",
		"agentActivity",
		"agentActivityDetail",
		"scopeStatus",
		"scopeExplanation",
		"executionResult",
		"ownerPrivateState",
		"ownerPrivateExplanation",
	} {
		if _, ok := agent[key]; !ok {
			t.Fatalf("agent JSON keys = %#v, missing %s", agent, key)
		}
	}
}

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

	dto := ToAppMessage(message.AppMessage{
		ID:        "msg_1",
		ChannelID: "chn_1",
		Source: message.ContributionSource{
			Kind:      message.ContributionSourceKindProviderEvent,
			RefType:   message.ContributionSourceRefTypeServiceEventDelivery,
			RefID:     "sed_1",
			RunID:     "sed_1",
			EventID:   "evt_1",
			EventType: "gitea_pull_request.synchronized",
			Label:     "Gitea PR watch",
		},
	})

	require.NotNil(t, dto.Source)
	require.Equal(t, message.ContributionSourceKindProviderEvent, dto.Source.Kind)
	require.Equal(t, message.ContributionSourceRefTypeServiceEventDelivery, dto.Source.RefType)
	require.Equal(t, "sed_1", dto.Source.RefID)
	require.Equal(t, "evt_1", dto.Source.EventID)
	require.Equal(t, "gitea_pull_request.synchronized", dto.Source.EventType)
	require.Equal(t, "Gitea PR watch", dto.Source.Label)
}
