package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestAgentThreadTargetContextCoversChannelDMReplyThreadAndTask(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	board := taskboard.New(pool)
	now := time.Date(2026, 7, 20, 12, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_target_contexts", now)
	user := model.User{ID: fixture.UserID, Name: "agent_target_contexts", DisplayName: "Agent Target User"}
	const (
		machineID     = "mach_agent_target_contexts"
		agentID       = "agt_agent_target_contexts"
		agentMemberID = "mem_runtime_agent_target_contexts"
		agentName     = "agent-target-contexts"
	)
	seedAppAgentParticipant(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)
	authenticated := agent.AuthenticatedAgent{WorkspaceID: fixture.WorkspaceID, AgentID: agentID, MemberID: agentMemberID}

	channelMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "channel target context", nil)
	require.NoError(t, err)

	dm, err := store.OpenDMChannelWithAgent(ctx, fixture.UserID, fixture.WorkspaceID, agentID)
	require.NoError(t, err)
	dmMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, dm.ID, "dm target context", nil)
	require.NoError(t, err)

	_, rootDelivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@"+agentName+" start target context thread", nil, SendAppMessageOptions{
		StartAgentThread: true,
		MentionHints:     []message.MessageMentionHint{{Type: "agent", ID: agentID, Token: agentName}},
	})
	require.NoError(t, err)
	require.NotNil(t, rootDelivery.StartedReplyThread)
	threadMessage, _, err := store.SendMessageForUser(ctx, user, fixture.WorkspaceID, rootDelivery.StartedReplyThread.ThreadChannelID, "reply thread target context", nil)
	require.NoError(t, err)

	created, err := board.CreateTasks(ctx, taskboard.CreateTasksRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Board:   taskboard.BoardRef{ChannelID: fixture.AllChannelID},
		Task:    &taskboard.TaskCreateInput{Title: "Task target context", AssigneeRef: agentMemberID},
	})
	require.NoError(t, err)
	require.Len(t, created.Tasks, 1)
	taskMessage, err := board.SendTaskScopeMessage(ctx, taskboard.SendTaskScopeMessageRequest{
		Context: taskboard.NewUserServiceContext(fixture.WorkspaceID, fixture.UserID, user.DisplayName),
		Task:    taskboard.TaskRef{TaskID: created.Tasks[0].ID},
		Message: taskboard.TaskScopeMessageInput{Content: "task target context"},
	})
	require.NoError(t, err)

	tests := []struct {
		name       string
		targetType string
		targetKey  string
		messageID  string
	}{
		{name: "channel", targetType: "channel", targetKey: fixture.AllChannelID, messageID: channelMessage.ID},
		{name: "dm", targetType: "dm", targetKey: dm.ID, messageID: dmMessage.ID},
		{name: "reply thread", targetType: "thread", targetKey: rootDelivery.StartedReplyThread.ThreadChannelID, messageID: threadMessage.ID},
		{name: "task", targetType: "task", targetKey: created.Tasks[0].TaskScopeID, messageID: taskMessage.Message.ID},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			attestation, err := store.ResolveAgentThreadTargetContextForAgent(ctx, authenticated, message.AgentThreadTarget{
				TargetKey: test.targetKey, TargetType: test.targetType, MessageID: test.messageID,
			})
			require.NoError(t, err)
			require.Equal(t, fixture.WorkspaceID, attestation.WorkspaceID)
			require.Equal(t, test.targetKey, attestation.TargetKey)
			require.Equal(t, test.targetType, attestation.TargetType)
			require.Equal(t, test.messageID, attestation.MessageID)
			require.Equal(t, test.targetKey, attestation.ThreadChannelID)
			require.Equal(t, fixture.UserID, attestation.ActingUserID)
			require.Equal(t, fixture.MemberID, attestation.ActingMemberID)
		})
	}

	_, err = store.ResolveAgentThreadTargetContextForAgent(ctx, authenticated, message.AgentThreadTarget{
		TargetKey: dm.ID, TargetType: "dm", MessageID: channelMessage.ID,
	})
	require.ErrorIs(t, err, ErrForbidden)
	_, err = store.ResolveAgentThreadTargetContextForAgent(ctx, authenticated, message.AgentThreadTarget{
		TargetKey: fixture.AllChannelID, TargetType: "task", MessageID: channelMessage.ID,
	})
	require.ErrorIs(t, err, ErrForbidden)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 4, 12, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_thread_session", now)
	const (
		machineID     = "mac_agent_thread_session"
		agentID       = "agt_agent_thread_session"
		agentMemberID = "mem_session_agent_actor"
		agentName     = "kbretriever"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	channel, err := store.EnsureAgentThreadSessionChannelForUser(ctx, fixture.UserID, fixture.WorkspaceID)
	require.NoError(t, err)
	require.NotEmpty(t, channel.ChannelID)
	require.NoError(t, store.AddChannelPrivateAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, channel.ChannelID, agentID))

	metadata := map[string]any{
		message.AgentThreadSessionMetadataKey: map[string]any{
			"kind": message.AgentThreadSessionMetadataKind,
		},
		"productOpaque": map[string]any{
			"query":            "Explain the subsystem plan",
			"knowledgeBaseIds": []any{"kb_1", "kb_2"},
		},
	}
	created, delivery, err := store.SendAppMessageForUserWithOptions(ctx, model.User{ID: fixture.UserID, Name: "agent_thread_session"}, fixture.WorkspaceID, channel.ChannelID, "@"+agentName+" Explain the subsystem plan", nil, SendAppMessageOptions{
		Metadata:         metadata,
		StartAgentThread: true,
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}},
	})
	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)

	subscribedThreads, err := store.messageStore().ListSubscribedThreadsForUserWithLimit(ctx, fixture.UserID, fixture.WorkspaceID, 0)
	require.NoError(t, err)
	require.Empty(t, subscribedThreads)

	recentThreads, err := store.messageStore().ListReplyThreadSummariesForUserWithLimit(ctx, fixture.UserID, fixture.WorkspaceID, channel.ChannelID, 10)
	require.NoError(t, err)
	require.Contains(t, recentThreads, created.ID)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, recentThreads[created.ID].ThreadChannelID)
	require.ElementsMatch(t, []string{fixture.UserID, agentID}, recentThreads[created.ID].ParticipantIDs)

	var subsystem string
	var ownerMemberID string
	var visibility string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT subsystem, owner_member_id, visibility
FROM scope_channel_profiles
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.WorkspaceID, channel.ChannelID).Scan(&subsystem, &ownerMemberID, &visibility))
	require.Equal(t, message.ChannelSubsystemAgentSession, subsystem)
	require.Equal(t, fixture.MemberID, ownerMemberID)
	require.Equal(t, "private", visibility)

	channels, err := message.ListChannelsForMember(ctx, pool, fixture.MemberID, fixture.WorkspaceID)
	require.NoError(t, err)
	for _, visibleChannel := range channels {
		require.NotEqual(t, channel.ChannelID, visibleChannel.ChannelID, "hidden agent-thread session channel must not appear in ordinary channel list")
	}

	context, err := store.ResolveAgentThreadSessionContextForAgent(ctx, agent.AuthenticatedAgent{WorkspaceID: fixture.WorkspaceID, AgentID: agentID, MemberID: agentMemberID}, delivery.StartedReplyThread.ThreadChannelID)
	require.NoError(t, err)
	require.Equal(t, created.ID, context.RootMessageID)
	require.Equal(t, channel.ChannelID, context.ParentChannelID)
	require.Equal(t, agentID, context.AgentID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 30, 2, 0, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_thread_session_fallback", now)
	user := model.User{ID: fixture.UserID, Name: "agent_thread_session_fallback", DisplayName: "agent_thread_session_fallback"}
	const (
		machineID     = "mach_ats_fallback"
		agentID       = "agt_ats_fallback"
		agentMemberID = "mem_ats_fallback_agent"
		agentName     = "agent-thread-session-fallback"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, machineID, agentID, agentMemberID, agentName, now)

	created, delivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@"+agentName+" explain KB tools", nil, SendAppMessageOptions{
		StartAgentThread: true,
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    agentID,
			Token: agentName,
		}},
	})
	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)

	context, err := store.ResolveAgentThreadSessionContextForAgent(ctx, agent.AuthenticatedAgent{WorkspaceID: fixture.WorkspaceID, AgentID: agentID, MemberID: agentMemberID}, delivery.StartedReplyThread.ThreadChannelID)
	require.NoError(t, err)
	require.Equal(t, fixture.WorkspaceID, context.WorkspaceID)
	require.Equal(t, delivery.StartedReplyThread.ThreadChannelID, context.ThreadChannelID)
	require.Equal(t, fixture.AllChannelID, context.ParentChannelID)
	require.Equal(t, created.ID, context.RootMessageID)
	require.Equal(t, agentID, context.AgentID)
	require.Equal(t, agentMemberID, context.AgentMemberID)
	require.Equal(t, fixture.UserID, context.ActingUserID)
	require.Equal(t, fixture.MemberID, context.ActingMemberID)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Date(2026, 6, 30, 2, 5, 0, 0, time.UTC)
	fixture := seedAppAdminFixture(t, pool, "agent_thread_session_fallback_denied", now)
	other := seedWorkspaceMemberFixture(t, pool, fixture.WorkspaceID, "agent_thread_session_fallback_other", now)
	user := model.User{ID: fixture.UserID, Name: "agent_thread_session_fallback_denied", DisplayName: "agent_thread_session_fallback_denied"}
	const (
		targetMachineID     = "mach_ats_fallback_target"
		targetAgentID       = "agt_ats_fallback_target"
		targetAgentMemberID = "mem_ats_fallback_target"
		targetAgentName     = "agent-thread-session-fallback-target"
		peerMachineID       = "mach_ats_fallback_peer"
		peerAgentID         = "agt_ats_fallback_peer"
		peerAgentMemberID   = "mem_ats_fallback_peer"
		peerAgentName       = "agent-thread-session-fallback-peer"
	)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID, fixture.AllChannelID, targetMachineID, targetAgentID, targetAgentMemberID, targetAgentName, now)
	seedAppPrivateAgentEntry(t, pool, fixture.WorkspaceID, other.UserID, other.MemberID, fixture.AllChannelID, peerMachineID, peerAgentID, peerAgentMemberID, peerAgentName, now)

	_, delivery, err := store.SendAppMessageForUserWithOptions(ctx, user, fixture.WorkspaceID, fixture.AllChannelID, "@"+targetAgentName+" explain KB tools", nil, SendAppMessageOptions{
		StartAgentThread: true,
		MentionHints: []message.MessageMentionHint{{
			Type:  "agent",
			ID:    targetAgentID,
			Token: targetAgentName,
		}},
	})
	require.NoError(t, err)
	require.NotNil(t, delivery.StartedReplyThread)

	_, err = store.ResolveAgentThreadSessionContextForAgent(ctx, agent.AuthenticatedAgent{WorkspaceID: fixture.WorkspaceID, AgentID: peerAgentID, MemberID: peerAgentMemberID}, delivery.StartedReplyThread.ThreadChannelID)
	require.ErrorIs(t, err, ErrForbidden)
}
