package task

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
)

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

	first := time.Date(2026, 5, 2, 9, 0, 0, 0, time.UTC)
	second := first.Add(time.Minute)
	third := first.Add(2 * time.Minute)
	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: 4, Title: "Parity task"},
			Timeline: []taskboard.TaskTimelineEntry{
				{
					ID:        "tl_discussion_1",
					Kind:      taskboard.TaskTimelineEntryKindDiscussion,
					CreatedAt: second,
					Discussion: &taskboard.TaskDiscussionEntry{
						ID:        "tds_1",
						Body:      "Discussion",
						CreatedAt: second,
					},
				},
				{
					ID:        "tl_activity_1",
					Kind:      taskboard.TaskTimelineEntryKindActivity,
					CreatedAt: third,
					Activity: &taskboard.TaskActivityEvent{
						ID:        "evt_1",
						Type:      "discussion_added",
						CreatedAt: third,
					},
				},
			},
			Artifacts: []taskboard.TaskArtifact{{
				ID:        "art_1",
				Kind:      "link",
				Title:     "Artifact",
				CreatedAt: first,
			}},
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodGet, "/api/v1/workspaces/wsp_1/tasks/tsk_1/execution-history", "")

	api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board}).ExecutionHistory)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	var response dto.TaskExecutionHistoryResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Equal(t, []string{"activity", "discussion", "artifact"}, historyKinds(response.Items))
}

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

	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/announcements", `{"intent":"routine","body":"Progress"}`)
	api.HandleWorkspaceScopedJSON[dto.TaskAnnouncementRequest]((&ParityHandler{}).CreateAnnouncement)(ctx)
	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, "invalid announcement intent", jsonError(t, recorder))

	recorder, ctx = newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/announcements", `{"intent":"milestone","body":" "}`)
	api.HandleWorkspaceScopedJSON[dto.TaskAnnouncementRequest]((&ParityHandler{}).CreateAnnouncement)(ctx)
	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, "body is required", jsonError(t, recorder))
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{ID: "tsk_1", TaskScopeID: "scope_task_1", ChannelID: "chn_1", TaskNumber: 7, Title: "Ship it"},
		}},
	}
	parityStore := &fakeTaskParityStore{
		messageResults: []fakeTaskParityMessageResult{
			{
				appMessage: message.AppMessage{ID: "msg_announcement", ChannelID: "chn_1", Content: "Progress"},
				delivery:   message.MessageDelivery{MessageID: "msg_announcement", Seq: 42},
			},
			{
				appMessage: message.AppMessage{
					ID:          "msg_announcement_audit",
					ChannelID:   "scope_task_1",
					ChannelType: "task",
					Content:     "Announcement sent to scope chn_1 (milestone):\n\nProgress",
					CreatedAt:   time.Date(2026, 7, 29, 3, 0, 0, 0, time.UTC),
					AuthorProfile: message.AuthorProfile{
						ActorType:   message.AuthorActorTypeHuman,
						PrincipalID: "user_1",
						MemberID:    "mem_user_1",
						DisplayName: "User 1",
					},
				},
				delivery: message.MessageDelivery{
					MessageID:               "msg_announcement_audit",
					Seq:                     43,
					HumanRecipientsTargeted: true,
					HumanRecipientMemberIDs: []string{"mem_watch"},
				},
			},
		},
	}
	fanout := &fakeTaskMessageFanout{}
	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/announcements", `{"intent":"milestone","body":"Progress"}`)

	api.HandleWorkspaceScopedJSON[dto.TaskAnnouncementRequest]((&ParityHandler{
		taskBoard: board,
		store:     parityStore,
		fanout:    fanout,
	}).CreateAnnouncement)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "workspace_1", parityStore.messageWorkspaceID)
	require.Equal(t, "chn_1", parityStore.messageScopeID)
	require.Equal(t, "Progress", parityStore.messageContent)
	require.Equal(t, []string(nil), parityStore.messageAttachmentIDs)
	require.Equal(t, "milestone", parityStore.messageMetadata["taskAnnouncement"].(map[string]any)["intent"])
	require.Len(t, parityStore.messageCalls, 2)
	require.Equal(t, "scope_task_1", parityStore.messageCalls[1].scopeID)
	require.Contains(t, parityStore.messageCalls[1].content, "scope chn_1")
	auditMetadata := parityStore.messageCalls[1].metadata["taskAnnouncementAudit"].(map[string]any)
	require.Equal(t, "milestone", auditMetadata["intent"])
	require.Equal(t, "chn_1", auditMetadata["sourceTargetScopeId"])
	require.Equal(t, "msg_announcement", auditMetadata["sourceMessageId"])
	require.Len(t, fanout.deliveries, 2)
	require.Equal(t, "msg_announcement", fanout.deliveries[0].messageID)
	require.Equal(t, "msg_announcement_audit", fanout.deliveries[1].messageID)
	require.True(t, fanout.deliveries[1].delivery.HumanRecipientsTargeted)
	require.Equal(t, []string{"mem_watch"}, fanout.deliveries[1].delivery.HumanRecipientMemberIDs)
	var response dto.TaskAnnouncementResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.NotNil(t, response.Entry)
	require.Equal(t, "msg_announcement_audit", response.Entry.ID)
	require.Equal(t, "tsk_1", response.Entry.TaskID)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{ID: "tsk_1", ChannelID: "chn_default", TaskNumber: 7, Title: "Ship it"},
		}},
	}
	parityStore := &fakeTaskParityStore{
		messageResults: []fakeTaskParityMessageResult{
			{
				appMessage: message.AppMessage{ID: "msg_announcement", ChannelID: "thr_review"},
				delivery:   message.MessageDelivery{MessageID: "msg_announcement", Seq: 42},
			},
			{
				appMessage: message.AppMessage{ID: "msg_announcement_audit", ChannelID: "tsk_1", ChannelType: "task"},
				delivery:   message.MessageDelivery{MessageID: "msg_announcement_audit", Seq: 43, HumanRecipientsTargeted: true},
			},
		},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/announcements", `{"intent":"review_request","body":"Please review","targetScopeId":" thr_review "}`)

	api.HandleWorkspaceScopedJSON[dto.TaskAnnouncementRequest]((&ParityHandler{
		taskBoard: board,
		store:     parityStore,
	}).CreateAnnouncement)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "thr_review", parityStore.messageScopeID)
	require.Equal(t, "Please review", parityStore.messageContent)
	require.Len(t, parityStore.messageCalls, 2)
	require.Equal(t, "tsk_1", parityStore.messageCalls[1].scopeID, "Task ID is the rolling fallback when TaskScopeID is absent")
	require.Contains(t, parityStore.messageCalls[1].content, "scope thr_review")
	require.Equal(t, "thr_review", parityStore.messageCalls[1].metadata["taskAnnouncementAudit"].(map[string]any)["sourceTargetScopeId"])
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{ID: "tsk_1", TaskScopeID: "scope_task_1", ChannelID: "chn_1", TaskNumber: 7, Title: "Ship it"},
		}},
	}
	parityStore := &fakeTaskParityStore{
		messageResults: []fakeTaskParityMessageResult{
			{
				appMessage: message.AppMessage{ID: "msg_announcement", ChannelID: "chn_1"},
				delivery:   message.MessageDelivery{MessageID: "msg_announcement", Seq: 42},
			},
			{err: errors.New("atomic Task audit write rolled back")},
		},
	}
	fanout := &fakeTaskMessageFanout{}
	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/announcements", `{"intent":"milestone","body":"Progress"}`)

	api.HandleWorkspaceScopedJSON[dto.TaskAnnouncementRequest]((&ParityHandler{
		taskBoard: board,
		store:     parityStore,
		fanout:    fanout,
	}).CreateAnnouncement)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Len(t, parityStore.messageCalls, 2)
	require.Len(t, fanout.deliveries, 1, "a failed atomic audit write has no committed delivery to fan out")
	var response dto.TaskAnnouncementResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Nil(t, response.Entry)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:          "tsk_1",
				TaskScopeID: "scope_task_1",
				ChannelID:   "chn_parent",
				TaskNumber:  8,
			},
		}},
	}
	parityStore := &fakeTaskParityStore{
		panel: message.ScopeNotificationPanel{
			ScopeID:   "scope_task_1",
			ScopeType: "task",
			Self:      message.ScopeNotificationSelfRow{State: message.HumanScopeSubscriptionStateActive},
		},
	}
	unread := &fakeTaskUnreadRuntime{}
	projector := &fakeTaskConversationPolicyProjector{}
	recorder, ctx := newTaskParityTestContext(http.MethodPut, "/api/tasks/tsk_1/notification-policy/self", `{"state":"active"}`)

	api.HandleWorkspaceScopedJSON[dto.TaskNotificationPolicyRequest]((&ParityHandler{taskBoard: board, store: parityStore, unreadRuntime: unread, conversationPolicyProjector: projector}).SetNotificationSelf)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "workspace_1:scope_task_1:active", parityStore.selfNotificationCall)
	require.Len(t, board.getDetailRequests, 1)
	require.True(t, board.getDetailRequests[0].TaskOnly)
	require.Empty(t, unread.invalidations)
	require.Equal(t, []TaskConversationPolicyProjection{{
		WorkspaceID: "workspace_1",
		UserID:      "user_1",
		MemberID:    "member_1",
		TaskID:      "tsk_1",
		ScopeID:     "scope_task_1",
		State:       message.HumanScopeSubscriptionStateActive,
	}}, projector.projections)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:          "tsk_1",
				TaskScopeID: "scope_task_1",
				ChannelID:   "chn_parent",
				TaskNumber:  8,
				Title:       "Focused task",
				SourceThread: &taskboard.TaskSourceThread{
					ThreadChannelID: "chn_thread",
					ParentChannelID: "chn_parent",
					ParentMessageID: "msg_1",
					Ref:             "#general:abc123",
				},
			},
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/active-target", `{"agentId":"agt_1"}`)
	parityStore := &fakeTaskParityStore{}

	api.HandleWorkspaceScopedJSON[dto.TaskActiveTargetRequest]((&ParityHandler{taskBoard: board, store: parityStore}).SetActiveTarget)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "agent_session:agt_1:tsk_1", parityStore.activeTargetCall)
	var response dto.TaskActiveTargetResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Equal(t, "agent_session", response.Scope)
	require.NotNil(t, response.ActiveTaskTarget)
	require.Equal(t, "agent_session", response.ActiveTaskTarget.Scope)
	require.Equal(t, "agt_1", response.ActiveTaskTarget.AgentID)
	require.Equal(t, "#general:abc123", response.ActiveTaskTarget.ThreadTarget)
	require.Equal(t, "human_selected", response.ActiveTaskTarget.Source)
	require.Equal(t, "user_1:workspace_1:scope_task_1:agt_1", parityStore.activeTargetAgentCheckCall)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:          "tsk_1",
				TaskScopeID: "scope_task_1",
				ChannelID:   "chn_parent",
				TaskNumber:  8,
				Title:       "Focused task",
			},
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/active-target", `{"agentId":"agt_other"}`)
	parityStore := &fakeTaskParityStore{
		activeTargetAgentCheckErr: api.NewHTTPError(http.StatusForbidden, "agent cannot access task"),
	}

	api.HandleWorkspaceScopedJSON[dto.TaskActiveTargetRequest]((&ParityHandler{taskBoard: board, store: parityStore}).SetActiveTarget)(ctx)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.Equal(t, "agent cannot access task", jsonError(t, recorder))
	require.Equal(t, "user_1:workspace_1:scope_task_1:agt_other", parityStore.activeTargetAgentCheckCall)
	require.Empty(t, parityStore.activeTargetCall)
}

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

	board := &fakeTaskParityBoard{
		details: map[string]taskboard.GetTaskDetailResponse{
			"tsk_child": {Detail: taskboard.TaskDetail{
				Task: taskboard.Task{
					ID:            "tsk_child",
					TaskScopeID:   "scope_task_child",
					ChannelID:     "chn_parent",
					TaskNumber:    9,
					Title:         "Worker subtask",
					ParentTaskRef: "tsk_root",
				},
			}},
			"tsk_root": {Detail: taskboard.TaskDetail{
				Task: taskboard.Task{
					ID:         "tsk_root",
					ChannelID:  "chn_parent",
					TaskNumber: 8,
					Title:      "Planner root",
					SourceThread: &taskboard.TaskSourceThread{
						ThreadChannelID: "chn_thread",
						ParentChannelID: "chn_parent",
						ParentMessageID: "msg_1",
						Ref:             "#general:abc123",
					},
				},
			}},
		},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_child/active-target", `{"agentId":"agt_1","role":"worker"}`)
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_child"}}
	parityStore := &fakeTaskParityStore{}

	api.HandleWorkspaceScopedJSON[dto.TaskActiveTargetRequest]((&ParityHandler{taskBoard: board, store: parityStore}).SetActiveTarget)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "agent_session:agt_1:tsk_child", parityStore.activeTargetCall)
	var response dto.TaskActiveTargetResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.NotNil(t, response.ActiveTaskTarget)
	require.Equal(t, int64(9), response.ActiveTaskTarget.TaskNumber)
	require.Equal(t, int64(8), response.ActiveTaskTarget.RootTaskNumber)
	require.Equal(t, "tsk_root", response.ActiveTaskTarget.ParentTaskRef)
	require.Equal(t, "#general:abc123", response.ActiveTaskTarget.ThreadTarget)
	require.Equal(t, "worker", response.ActiveTaskTarget.Role)
	require.Len(t, board.getDetailRequests, 2)
	require.Equal(t, "tsk_child", board.getDetailRequests[0].Task.TaskID)
	require.Equal(t, "tsk_root", board.getDetailRequests[1].Task.TaskID)
	require.Equal(t, "user_1:workspace_1:scope_task_child:agt_1", parityStore.activeTargetAgentCheckCall)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:         "tsk_root",
				ChannelID:  "chn_parent",
				TaskNumber: 8,
				Title:      "Planner root",
			},
			Subtasks: []taskboard.Task{{
				ID:         "tsk_child",
				ChannelID:  "chn_parent",
				TaskNumber: 9,
				Title:      "Worker subtask",
			}},
		}},
	}
	parityStore := &fakeTaskParityStore{
		activeTargetRecords: []message.TaskActiveTargetRecord{{
			Scope:          "thread_focus",
			TargetKey:      "chn_thread",
			TaskID:         "tsk_root",
			TaskChannelID:  "chn_parent",
			TaskNumber:     8,
			TaskTitle:      "Planner root",
			Source:         "human_selected",
			RootTaskNumber: 8,
		}, {
			Scope:          "agent_session",
			TargetKey:      "agt_1",
			TaskID:         "tsk_child",
			TaskChannelID:  "chn_parent",
			TaskNumber:     9,
			TaskTitle:      "Worker subtask",
			Source:         "human_selected",
			AgentID:        "agt_1",
			Role:           "worker",
			RootTaskNumber: 8,
			ParentTaskRef:  "tsk_root",
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodGet, "/api/v1/workspaces/wsp_1/tasks/tsk_root/active-targets?includeSubtasks=true", "")
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_root"}}

	api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board, store: parityStore}).ListActiveTargets)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, []string{"tsk_root", "tsk_child"}, parityStore.activeTargetTaskIDs)
	var response dto.TaskActiveTargetsResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Len(t, response.ActiveTaskTargets, 2)
	require.Equal(t, "thread_focus", response.ActiveTaskTargets[0].Scope)
	require.Equal(t, int64(8), response.ActiveTaskTargets[0].TaskNumber)
	require.Equal(t, "agent_session", response.ActiveTaskTargets[1].Scope)
	require.Equal(t, "agt_1", response.ActiveTaskTargets[1].AgentID)
	require.Equal(t, "worker", response.ActiveTaskTargets[1].Role)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:         "tsk_1",
				ChannelID:  "chn_parent",
				TaskNumber: 8,
				Title:      "Focused task",
				SourceThread: &taskboard.TaskSourceThread{
					ThreadChannelID: "chn_thread",
					ParentChannelID: "chn_parent",
					ParentMessageID: "msg_1",
					Ref:             "#general:abc123",
				},
			},
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodDelete, "/api/v1/workspaces/wsp_1/tasks/tsk_1/active-target?scope=thread_focus", "")
	parityStore := &fakeTaskParityStore{}

	api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board, store: parityStore}).ClearActiveTarget)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "clear:thread_focus:chn_thread", parityStore.activeTargetCall)
	var response dto.TaskActiveTargetResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.True(t, response.Cleared)
	require.Equal(t, "thread_focus", response.Scope)
	require.Empty(t, response.AgentID)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:          "tsk_1",
				TaskScopeID: "scope_task_1",
				ChannelID:   "chn_parent",
				TaskNumber:  8,
				Title:       "Focused task",
			},
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodDelete, "/api/v1/workspaces/wsp_1/tasks/tsk_1/active-target?scope=agent_session&agentId=agt_1", "")
	parityStore := &fakeTaskParityStore{}

	api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board, store: parityStore}).ClearActiveTarget)(ctx)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "user_1:workspace_1:scope_task_1:agt_1", parityStore.activeTargetAgentCheckCall)
	require.Equal(t, "clear:agent_session:agt_1", parityStore.activeTargetCall)
	var response dto.TaskActiveTargetResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.True(t, response.Cleared)
	require.Equal(t, "agent_session", response.Scope)
	require.Equal(t, "agt_1", response.AgentID)
}

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

	board := &fakeTaskParityBoard{
		detail: taskboard.GetTaskDetailResponse{Detail: taskboard.TaskDetail{
			Task: taskboard.Task{
				ID:          "tsk_1",
				TaskScopeID: "scope_task_1",
				ChannelID:   "chn_parent",
				TaskNumber:  8,
				Title:       "Focused task",
			},
		}},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodDelete, "/api/v1/workspaces/wsp_1/tasks/tsk_1/active-target?scope=agent_session&agentId=agt_other", "")
	parityStore := &fakeTaskParityStore{
		activeTargetAgentCheckErr: api.NewHTTPError(http.StatusForbidden, "agent cannot access task"),
	}

	api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board, store: parityStore}).ClearActiveTarget)(ctx)

	require.Equal(t, http.StatusForbidden, recorder.Code)
	require.Equal(t, "agent cannot access task", jsonError(t, recorder))
	require.Equal(t, "user_1:workspace_1:scope_task_1:agt_other", parityStore.activeTargetAgentCheckCall)
	require.Empty(t, parityStore.activeTargetCall)
}

func newTaskParityTestContext(method string, target string, body string) (*httptest.ResponseRecorder, *gin.Context) {
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	if body == "" {
		ctx.Request = httptest.NewRequest(method, target, nil)
	} else {
		ctx.Request = httptest.NewRequest(method, target, bytes.NewBufferString(body))
		ctx.Request.Header.Set("Content-Type", "application/json")
	}
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)
	return recorder, ctx
}

func historyKinds(items []dto.TaskExecutionHistoryItem) []string {
	kinds := make([]string, 0, len(items))
	for _, item := range items {
		kinds = append(kinds, item.Kind)
	}
	return kinds
}

type fakeTaskParityBoard struct {
	detail                taskboard.GetTaskDetailResponse
	details               map[string]taskboard.GetTaskDetailResponse
	getDetailRequests     []taskboard.GetTaskDetailRequest
	readableScopeRequests []taskboard.GetReadableTaskScopeRequest
	updateRequests        []taskboard.UpdateTaskRequest
	updateResponse        taskboard.UpdateTaskResponse
	getDetailErr          error
	updateErr             error
}

func (fake *fakeTaskParityBoard) GetTaskDetail(_ context.Context, request taskboard.GetTaskDetailRequest) (taskboard.GetTaskDetailResponse, error) {
	fake.getDetailRequests = append(fake.getDetailRequests, request)
	if fake.getDetailErr != nil {
		return taskboard.GetTaskDetailResponse{}, fake.getDetailErr
	}
	if fake.details != nil {
		if detail, ok := fake.details[request.Task.TaskID]; ok {
			return detail, nil
		}
	}
	return fake.detail, nil
}

func (fake *fakeTaskParityBoard) GetReadableTaskScope(_ context.Context, request taskboard.GetReadableTaskScopeRequest) (taskboard.GetReadableTaskScopeResponse, error) {
	fake.readableScopeRequests = append(fake.readableScopeRequests, request)
	if fake.getDetailErr != nil {
		return taskboard.GetReadableTaskScopeResponse{}, fake.getDetailErr
	}
	detail := fake.detail
	if fake.details != nil {
		if selected, ok := fake.details[request.Task.TaskID]; ok {
			detail = selected
		}
	}
	return taskboard.GetReadableTaskScopeResponse{
		TaskID:      detail.Detail.Task.ID,
		TaskScopeID: detail.Detail.Task.TaskScopeID,
	}, nil
}

func (fake *fakeTaskParityBoard) GetTaskTimeline(context.Context, taskboard.GetTaskTimelineRequest) (taskboard.GetTaskTimelineResponse, error) {
	return taskboard.GetTaskTimelineResponse{}, nil
}

func (fake *fakeTaskParityBoard) UpdateTask(_ context.Context, request taskboard.UpdateTaskRequest) (taskboard.UpdateTaskResponse, error) {
	fake.updateRequests = append(fake.updateRequests, request)
	if fake.updateErr != nil {
		return taskboard.UpdateTaskResponse{}, fake.updateErr
	}
	return fake.updateResponse, nil
}

type fakeTaskParityMessageCall struct {
	workspaceID   string
	scopeID       string
	content       string
	attachmentIDs []string
	metadata      map[string]any
}

type fakeTaskParityMessageResult struct {
	appMessage message.AppMessage
	delivery   message.MessageDelivery
	err        error
}

type fakeTaskParityStore struct {
	panel                      message.ScopeNotificationPanel
	selfNotificationCall       string
	agentWakeCall              string
	activeTargetAgentCheckCall string
	activeTargetAgentCheckErr  error
	activeTargetCall           string
	activeTargetRecords        []message.TaskActiveTargetRecord
	activeTargetTaskIDs        []string
	appMessage                 message.AppMessage
	delivery                   message.MessageDelivery
	messageWorkspaceID         string
	messageScopeID             string
	messageContent             string
	messageAttachmentIDs       []string
	messageMetadata            map[string]any
	messageCalls               []fakeTaskParityMessageCall
	messageResults             []fakeTaskParityMessageResult
	taskWatcherPage            message.TaskWatcherPage
	taskWatcherCall            string
}

func (fake *fakeTaskParityStore) SendMessageForUserWithMetadata(_ context.Context, _ model.User, workspaceID string, scopeID string, content string, attachmentIDs []string, metadata map[string]any) (message.AppMessage, message.MessageDelivery, error) {
	call := fakeTaskParityMessageCall{
		workspaceID:   workspaceID,
		scopeID:       scopeID,
		content:       content,
		attachmentIDs: append([]string(nil), attachmentIDs...),
		metadata:      metadata,
	}
	fake.messageCalls = append(fake.messageCalls, call)
	if len(fake.messageCalls) == 1 {
		fake.messageWorkspaceID = workspaceID
		fake.messageScopeID = scopeID
		fake.messageContent = content
		fake.messageAttachmentIDs = append([]string(nil), attachmentIDs...)
		fake.messageMetadata = metadata
	}
	resultIndex := len(fake.messageCalls) - 1
	if resultIndex < len(fake.messageResults) {
		result := fake.messageResults[resultIndex]
		return result.appMessage, result.delivery, result.err
	}
	return fake.appMessage, fake.delivery, nil
}

type fakeTaskFanoutDelivery struct {
	workspaceID string
	messageID   string
	delivery    message.MessageDelivery
}

type fakeTaskMessageFanout struct {
	calls       int
	workspaceID string
	messageID   string
	deliveryID  string
	deliveries  []fakeTaskFanoutDelivery
}

type fakeTaskConversationPolicyProjector struct {
	projections []TaskConversationPolicyProjection
}

func (fake *fakeTaskConversationPolicyProjector) ProjectTaskConversationPolicy(_ context.Context, projection TaskConversationPolicyProjection) error {
	fake.projections = append(fake.projections, projection)
	return nil
}

func (fake *fakeTaskMessageFanout) EmitCommittedMessage(_ context.Context, workspaceID string, appMessage message.AppMessage, delivery message.MessageDelivery) {
	fake.calls++
	fake.workspaceID = workspaceID
	fake.messageID = appMessage.ID
	fake.deliveryID = delivery.MessageID
	fake.deliveries = append(fake.deliveries, fakeTaskFanoutDelivery{
		workspaceID: workspaceID,
		messageID:   appMessage.ID,
		delivery:    delivery,
	})
}

func (fake *fakeTaskParityStore) GetChannelNotificationPanelForUser(context.Context, string, string, string, string, int) (message.ScopeNotificationPanel, error) {
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetChannelNotificationSelfStateForUser(context.Context, string, string, string, string) (message.ScopeNotificationPanel, error) {
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetChannelNotificationAgentWakeStateForUser(_ context.Context, _ string, workspaceID string, channelID string, agentID string, state string) (message.ScopeNotificationPanel, error) {
	fake.agentWakeCall = "channel:" + workspaceID + ":" + channelID + ":" + agentID + ":" + state
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) GetThreadNotificationPanelForUser(context.Context, string, string, string, string, int) (message.ScopeNotificationPanel, error) {
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetThreadNotificationSelfStateForUser(context.Context, string, string, string, string) (message.ScopeNotificationPanel, error) {
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetThreadNotificationAgentWakeStateForUser(_ context.Context, _ string, workspaceID string, threadChannelID string, agentID string, state string) (message.ScopeNotificationPanel, error) {
	fake.agentWakeCall = "thread:" + workspaceID + ":" + threadChannelID + ":" + agentID + ":" + state
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) GetTaskNotificationPanelForUser(context.Context, string, string, string) (message.ScopeNotificationPanel, error) {
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetTaskNotificationSelfStateForUser(_ context.Context, _ string, workspaceID string, taskScopeID string, state string) (message.ScopeNotificationPanel, error) {
	fake.selfNotificationCall = workspaceID + ":" + taskScopeID + ":" + state
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetTaskNotificationAgentWakeStateForUser(_ context.Context, _ string, workspaceID string, taskScopeID string, agentID string, state string) (message.ScopeNotificationPanel, error) {
	fake.agentWakeCall = "task:" + workspaceID + ":" + taskScopeID + ":" + agentID + ":" + state
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) SetTaskNotificationAgentsWakeStateForUser(_ context.Context, _ string, workspaceID string, taskScopeID string, state string) (message.ScopeNotificationPanel, error) {
	fake.agentWakeCall = "task:" + workspaceID + ":" + taskScopeID + ":all:" + state
	return fake.panel, nil
}

func (fake *fakeTaskParityStore) GetTaskWatchersForUser(_ context.Context, _ string, workspaceID string, taskScopeID string, cursor string, limit int) (message.TaskWatcherPage, error) {
	fake.taskWatcherCall = fmt.Sprintf("%s:%s:%s:%d", workspaceID, taskScopeID, cursor, limit)
	return fake.taskWatcherPage, nil
}

func (fake *fakeTaskParityStore) RequireTaskActiveTargetAgentForUser(_ context.Context, userID string, workspaceID string, taskScopeID string, agentID string) error {
	fake.activeTargetAgentCheckCall = userID + ":" + workspaceID + ":" + taskScopeID + ":" + agentID
	return fake.activeTargetAgentCheckErr
}

func (fake *fakeTaskParityStore) SetTaskActiveTargetForUser(_ context.Context, _ string, _ string, target message.TaskActiveTargetRecord) (message.TaskActiveTargetRecord, error) {
	fake.activeTargetCall = target.Scope + ":" + target.TargetKey + ":" + target.TaskID
	return target, nil
}

func (fake *fakeTaskParityStore) ListTaskActiveTargetsForUser(_ context.Context, _ string, _ string, taskIDs []string) ([]message.TaskActiveTargetRecord, error) {
	fake.activeTargetTaskIDs = append([]string(nil), taskIDs...)
	return fake.activeTargetRecords, nil
}

func (fake *fakeTaskParityStore) ClearTaskActiveTargetForUser(_ context.Context, _ string, _ string, scope string, targetKey string) error {
	fake.activeTargetCall = "clear:" + scope + ":" + targetKey
	return nil
}
