package agent

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

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

type expectedTaskBoardService struct {
	listRequest            ListTasksRequest
	getRequest             GetTaskDetailRequest
	createRequest          CreateTasksRequest
	createResponse         CreateTasksResponse
	createSubtasksRequest  CreateSubtasksRequest
	createSubtasksResponse CreateSubtasksResponse
	updateRequest          UpdateTaskRequest
	checkpointRequest      AddTaskCheckpointRequest
	claimRequest           ClaimTasksRequest
	claimResults           []ClaimResult
	unclaimRequests        []UnclaimTaskRequest
	unclaimErr             error
	statusRequest          UpdateTaskStatusRequest
	deleteRequest          DeleteTaskRequest
	deleteResponse         DeleteTaskResponse
}

func (service *expectedTaskBoardService) ListTasks(_ context.Context, request ListTasksRequest) (ListTasksResponse, error) {
	service.listRequest = request
	return ListTasksResponse{ChannelID: "chn_1", TaskViews: []TaskView{}}, nil
}

func (*expectedTaskBoardService) ListTaskOptions(context.Context, ListTaskOptionsRequest) (ListTaskOptionsResponse, error) {
	panic("not used")
}

func (service *expectedTaskBoardService) CreateTasks(_ context.Context, request CreateTasksRequest) (CreateTasksResponse, error) {
	service.createRequest = request
	response := service.createResponse
	if response.ChannelID == "" {
		response.ChannelID = "chn_1"
	}
	return response, nil
}

func (service *expectedTaskBoardService) CreateSubtasks(_ context.Context, request CreateSubtasksRequest) (CreateSubtasksResponse, error) {
	service.createSubtasksRequest = request
	response := service.createSubtasksResponse
	if response.ChannelID == "" {
		response.ChannelID = "chn_1"
	}
	return response, nil
}

func (service *expectedTaskBoardService) ClaimTasks(_ context.Context, request ClaimTasksRequest) (ClaimTasksResponse, error) {
	service.claimRequest = request
	return ClaimTasksResponse{Results: service.claimResults}, nil
}

func (service *expectedTaskBoardService) UnclaimTask(_ context.Context, request UnclaimTaskRequest) (UnclaimTaskResponse, error) {
	service.unclaimRequests = append(service.unclaimRequests, request)
	if service.unclaimErr != nil {
		return UnclaimTaskResponse{}, service.unclaimErr
	}
	return UnclaimTaskResponse{Task: Task{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: request.Task.TaskNumber}}, nil
}

func (*expectedTaskBoardService) BlockTask(context.Context, BlockTaskRequest) (BlockTaskResponse, error) {
	panic("not used")
}

func (*expectedTaskBoardService) UnblockTask(context.Context, UnblockTaskRequest) (UnblockTaskResponse, error) {
	panic("not used")
}

func (service *expectedTaskBoardService) GetTaskDetail(_ context.Context, request GetTaskDetailRequest) (GetTaskDetailResponse, error) {
	service.getRequest = request
	return GetTaskDetailResponse{Detail: TaskDetail{Task: Task{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: 7, Title: "task"}}}, nil
}

func (service *expectedTaskBoardService) UpdateTask(_ context.Context, request UpdateTaskRequest) (UpdateTaskResponse, error) {
	service.updateRequest = request
	return UpdateTaskResponse{Task: Task{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: 7, Title: "updated"}}, nil
}

func (service *expectedTaskBoardService) UpdateTaskStatus(_ context.Context, request UpdateTaskStatusRequest) (UpdateTaskStatusResponse, error) {
	service.statusRequest = request
	return UpdateTaskStatusResponse{Task: Task{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: request.Task.TaskNumber}}, nil
}

func (service *expectedTaskBoardService) AddTaskCheckpoint(_ context.Context, request AddTaskCheckpointRequest) (AddTaskCheckpointResponse, error) {
	service.checkpointRequest = request
	return AddTaskCheckpointResponse{Task: Task{ID: "tsk_1", ChannelID: "chn_1", TaskNumber: 7, Title: "task"}}, nil
}

func (service *expectedTaskBoardService) DeleteTask(_ context.Context, request DeleteTaskRequest) (DeleteTaskResponse, error) {
	service.deleteRequest = request
	return service.deleteResponse, nil
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodGet, "/tasks?channel=%23agentteam&query=needle&status=active", nil)

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.listRequest.Context.Subject.PrincipalID)
	require.Equal(t, "wsp_test", board.listRequest.Context.WorkspaceID)
	require.Equal(t, "#agentteam", board.listRequest.Board.Channel)
	require.Equal(t, "needle", board.listRequest.SearchQuery)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodGet, "/tasks?channel=%23agentteam&status=active&includeSubtasks=true&quickFilters=mine&quickFilters=blocked", nil)

	require.Equal(t, http.StatusOK, response.Code)
	require.True(t, board.listRequest.IncludeSubtasks)
	require.Equal(t, []string{"mine", "blocked"}, board.listRequest.QuickFilters)
}

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

	for _, status := range []string{"open", "claimed", "completed", "non_done"} {
		board := &expectedTaskBoardService{}
		response := agentTaskRequest(
			t,
			expectedTaskTestRouter(board),
			http.MethodGet,
			"/tasks?channel=%23agentteam&status="+status,
			nil,
		)

		require.Equal(t, http.StatusBadRequest, response.Code)
		require.Empty(t, board.listRequest.Board.Channel)
	}
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/get", map[string]any{
		"channel": "#agentteam", "number": 7,
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.getRequest.Context.Subject.PrincipalID)
	require.Equal(t, "#agentteam", board.getRequest.Board.Channel)
	require.Equal(t, int64(7), board.getRequest.Task.TaskNumber)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"parent":  5,
		"tasks":   []map[string]any{{"title": "child one"}, {"title": "child two"}},
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, int64(5), board.createSubtasksRequest.Task.TaskNumber)
	require.Equal(t, "#agentteam", board.createSubtasksRequest.Board.Channel)
	require.Equal(t, []string{"child one", "child two"}, board.createSubtasksRequest.Titles)
	require.Nil(t, board.createSubtasksRequest.Input, "batch subtask create must not also set Input")
	require.Equal(t, "agt_0000000000test", board.createSubtasksRequest.Context.Subject.PrincipalID)
	// A subtask create must not also fire the top-level CreateTasks path.
	require.Zero(t, board.createRequest.Board.Channel)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam", "parent": 5,
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"tasks":   []map[string]any{{"title": "one"}},
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.createRequest.Context.Subject.PrincipalID)
	require.Equal(t, "#agentteam", board.createRequest.Board.Channel)
	require.Equal(t, []string{"one"}, board.createRequest.Titles)
}

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

	assignee := &AuthorProfile{
		ActorType:   message.AuthorActorTypeAgent,
		PrincipalID: "agt_000000assignee",
		MemberID:    "0000assignee",
		DisplayName: "Expected Assignee",
	}
	board := &expectedTaskBoardService{
		createResponse: CreateTasksResponse{
			ChannelID: "chn_1",
			CreatedTasks: []CreatedTask{{
				TaskScopeID: "scp_top", TaskNumber: 7, Title: "top", Reused: true,
			}},
			Tasks: []Task{{
				TaskScopeID: "scp_top", TaskNumber: 7, Title: "top", Status: "in_progress", AssigneeProfile: assignee,
			}},
		},
		createSubtasksResponse: CreateSubtasksResponse{
			ChannelID: "chn_1",
			CreatedTasks: []CreatedTask{{
				TaskScopeID: "scp_child", TaskNumber: 8, Title: "child",
			}},
			Tasks: []Task{{
				TaskScopeID: "scp_child", TaskNumber: 8, Title: "child", Status: "todo", AssigneeProfile: assignee,
			}},
		},
	}
	router := expectedTaskTestRouter(board)

	topResponse := agentTaskRequest(t, router, http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"tasks":   []map[string]any{{"title": "top", "assignee": "agent:agt_000000assignee"}},
	})
	require.Equal(t, http.StatusOK, topResponse.Code)
	require.NotNil(t, board.createRequest.Task)
	require.Equal(t, "agent:agt_000000assignee", board.createRequest.Task.AssigneeRef)
	var top dto.AgentCreatedTasksResponse
	require.NoError(t, json.Unmarshal(topResponse.Body.Bytes(), &top))
	require.Len(t, top.Tasks, 1)
	require.True(t, top.Tasks[0].Reused)
	require.Equal(t, "in_progress", top.Tasks[0].Status)
	require.NotNil(t, top.Tasks[0].AssigneeProfile)
	require.Equal(t, "agt_000000assignee", top.Tasks[0].AssigneeProfile.PrincipalID)
	require.Equal(t, "0000assignee", top.Tasks[0].AssigneeProfile.MemberID)

	childResponse := agentTaskRequest(t, router, http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"parent":  7,
		"tasks":   []map[string]any{{"title": "child", "assignee": "agent:agt_000000assignee"}},
	})
	require.Equal(t, http.StatusOK, childResponse.Code)
	require.NotNil(t, board.createSubtasksRequest.Input)
	require.Equal(t, "agent:agt_000000assignee", board.createSubtasksRequest.Input.AssigneeRef)
	var child dto.AgentCreatedTasksResponse
	require.NoError(t, json.Unmarshal(childResponse.Body.Bytes(), &child))
	require.Len(t, child.Tasks, 1)
	require.False(t, child.Tasks[0].Reused)
	require.Equal(t, "todo", child.Tasks[0].Status)
	require.NotNil(t, child.Tasks[0].AssigneeProfile)
	require.Equal(t, "agt_000000assignee", child.Tasks[0].AssigneeProfile.PrincipalID)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel": "#agentteam", "number": 7, "title": "updated",
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.updateRequest.Context.Subject.PrincipalID)
	require.Equal(t, "#agentteam", board.updateRequest.Board.Channel)
	require.Equal(t, int64(7), board.updateRequest.Task.TaskNumber)
	require.NotNil(t, board.updateRequest.Patch.Title)
	require.Equal(t, "updated", *board.updateRequest.Patch.Title)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/add-checkpoint", map[string]any{
		"channel": "#agentteam", "number": 7, "summary": "progress",
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.checkpointRequest.Context.Subject.PrincipalID)
	require.Equal(t, "progress", board.checkpointRequest.Checkpoint.Text)
}

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

	board := &expectedTaskBoardService{claimResults: []ClaimResult{
		{TaskNumber: 7, Success: true},
		{TaskNumber: 8, Success: true},
	}}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{7, 8}, "claimed": true,
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.claimRequest.Context.Subject.PrincipalID)
	require.Equal(t, "wsp_test", board.claimRequest.Context.WorkspaceID)
	require.Equal(t, "#agentteam", board.claimRequest.Board.Channel)
	require.Equal(t, []TaskRef{{TaskNumber: 7}, {TaskNumber: 8}}, board.claimRequest.Tasks)

	var result struct {
		Claimed int `json:"claimed"`
		Failed  []struct {
			TaskNumber int64  `json:"taskNumber"`
			Reason     string `json:"reason"`
		} `json:"failed"`
	}
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &result))
	require.Equal(t, 2, result.Claimed)
	require.Empty(t, result.Failed)
}

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

	board := &expectedTaskBoardService{claimResults: []ClaimResult{
		{TaskNumber: 7, Success: true},
		{TaskNumber: 8, Success: false, Reason: "already claimed by another agent"},
	}}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{7, 8}, "claimed": true,
	})

	require.Equal(t, http.StatusOK, response.Code)
	var result struct {
		Claimed int `json:"claimed"`
		Failed  []struct {
			TaskNumber int64  `json:"taskNumber"`
			Reason     string `json:"reason"`
		} `json:"failed"`
	}
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &result))
	require.Equal(t, 1, result.Claimed)
	require.Len(t, result.Failed, 1)
	require.Equal(t, int64(8), result.Failed[0].TaskNumber)
	require.Equal(t, "already claimed by another agent", result.Failed[0].Reason)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{7, 8}, "claimed": false,
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Len(t, board.unclaimRequests, 2)
	require.Equal(t, "agt_0000000000test", board.unclaimRequests[0].Context.Subject.PrincipalID)
	require.Equal(t, "#agentteam", board.unclaimRequests[0].Board.Channel)
	require.Equal(t, []int64{7, 8}, []int64{board.unclaimRequests[0].Task.TaskNumber, board.unclaimRequests[1].Task.TaskNumber})

	var result struct {
		Claimed int `json:"claimed"`
	}
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &result))
	require.Equal(t, 2, result.Claimed)
}

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

	board := &expectedTaskBoardService{unclaimErr: errors.New("only the current assignee can release the task")}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{7}, "claimed": false,
	})

	require.Equal(t, http.StatusOK, response.Code)
	var result struct {
		Claimed int `json:"claimed"`
		Failed  []struct {
			TaskNumber int64  `json:"taskNumber"`
			Reason     string `json:"reason"`
		} `json:"failed"`
	}
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &result))
	require.Zero(t, result.Claimed)
	require.Len(t, result.Failed, 1)
	require.Equal(t, int64(7), result.Failed[0].TaskNumber)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{}, "claimed": true,
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel": "#agentteam", "number": 7, "status": "done",
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "agt_0000000000test", board.statusRequest.Context.Subject.PrincipalID)
	require.Equal(t, "#agentteam", board.statusRequest.Board.Channel)
	require.Equal(t, int64(7), board.statusRequest.Task.TaskNumber)
	require.Equal(t, "done", board.statusRequest.Status)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel": "#agentteam", "number": 7, "status": "archived",
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Empty(t, board.statusRequest.Status)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel": "#agentteam", "number": 7, "title": "updated", "status": "archived",
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
	// Status is validated before any write, so neither operation runs.
	require.Zero(t, board.updateRequest.Task.TaskNumber)
	require.Empty(t, board.statusRequest.Status)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel": "#agentteam", "number": 7, "status": "DONE",
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "done", board.statusRequest.Status)
}

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

	taskNumbers := make([]any, 0, 21)
	for i := 1; i <= 21; i++ {
		taskNumbers = append(taskNumbers, i)
	}
	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": taskNumbers, "claimed": true,
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Empty(t, board.claimRequest.Tasks)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{7, 0}, "claimed": true,
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Empty(t, board.claimRequest.Tasks)
}

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

	board := &expectedTaskBoardService{claimResults: []ClaimResult{
		{TaskNumber: 7, Success: true},
		{TaskNumber: 8, Success: true},
	}}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/set-claim", map[string]any{
		"channel": "#agentteam", "numbers": []any{7, 7, 8}, "claimed": true,
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, []TaskRef{{TaskNumber: 7}, {TaskNumber: 8}}, board.claimRequest.Tasks)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel": "#agentteam", "number": 7, "title": "updated", "status": "done",
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Zero(t, board.updateRequest.Task.TaskNumber)
	require.Empty(t, board.statusRequest.Status)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel":            "#agentteam",
		"number":             7,
		"acceptanceCriteria": []any{},
	})

	// An explicit empty array is a clear, not "no patch": it must not be rejected
	// as missing a patch field.
	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, int64(7), board.updateRequest.Task.TaskNumber)
	require.NotNil(t, board.updateRequest.Patch.AcceptanceCriteria)
	require.Empty(t, board.updateRequest.Patch.AcceptanceCriteria)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel":  "#agentteam",
		"number":   7,
		"assignee": "@alice",
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.NotNil(t, board.updateRequest.Patch.AssigneeRef)
	require.Equal(t, "@alice", *board.updateRequest.Patch.AssigneeRef)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel":          "#agentteam",
		"number":           7,
		"kindOptionId":     "kopt_legacy",
		"priorityOptionId": "popt_legacy",
	})

	// Expected task mutations use a strict JSON decoder: removed fields
	// (kindOptionId/priorityOptionId) are rejected with 400 rather than
	// silently ignored, so the legacy option IDs cannot reach the store.
	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Zero(t, board.updateRequest.Task.TaskNumber)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks/update", map[string]any{
		"channel":              "#agentteam",
		"number":               7,
		"currentAgentRef":      "agt_000000000alice",
		"suggestedAssigneeRef": "@bob",
		"reviewer":             "@reviewer",
	})

	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Zero(t, board.updateRequest.Task.TaskNumber)
}

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

	gin.SetMode(gin.TestMode)
	router := gin.New()
	targetStore := &expectedTaskTargetStore{}
	RegisterRoutes(router, Dependencies{
		Auth:               fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "00000000test"}},
		TaskBoard:          &expectedTaskBoardService{},
		TaskTargetResolver: targetStore,
	})
	response := agentTaskRequest(t, router, http.MethodPost, "/tasks", map[string]any{
		"channel": "#meeting-room",
		"target":  "#meeting-room:f6997aed",
		"tasks":   []map[string]any{{"title": "created from thread"}},
	})

	require.Equal(t, http.StatusOK, response.Code)
	// The explicit thread target reaches the resolver and is validated before the
	// create source metadata is applied.
	require.Equal(t, "#meeting-room:f6997aed", targetStore.lastResolvedTarget)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#other-board",
		"target":  "#meeting-room:f6997aed",
		"tasks":   []map[string]any{{"title": "mismatched"}},
	})

	// The target resolves to board "#meeting-room", which conflicts with the
	// explicit channel "#other-board"; the association is rejected, not forced.
	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Zero(t, board.createRequest.Board.Channel)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"parent":  5,
		"tasks": []map[string]any{
			{"title": "rich subtask", "assignee": "agt_0000000000test"},
		},
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, int64(5), board.createSubtasksRequest.Task.TaskNumber)
	// A rich single subtask must travel as Input only, not Titles+Input (the
	// store rejects both set).
	require.Nil(t, board.createSubtasksRequest.Titles)
	require.NotNil(t, board.createSubtasksRequest.Input)
	require.Equal(t, "rich subtask", board.createSubtasksRequest.Input.Title)
	require.Equal(t, "agt_0000000000test", board.createSubtasksRequest.Input.AssigneeRef)
	require.Zero(t, board.createRequest.Board.Channel)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"parent":  -3,
		"tasks":   []map[string]any{{"title": "x"}},
	})

	// A negative parent must be rejected, not silently degrade to a top-level create.
	require.Equal(t, http.StatusBadRequest, response.Code)
	require.Zero(t, board.createSubtasksRequest.Task.TaskNumber)
	require.Zero(t, board.createRequest.Board.Channel)
}

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

	gin.SetMode(gin.TestMode)
	router := gin.New()
	targetStore := &expectedTaskTargetStore{}
	board := &expectedTaskBoardService{}
	RegisterRoutes(router, Dependencies{
		Auth:               fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "00000000test"}},
		TaskBoard:          board,
		TaskTargetResolver: targetStore,
	})
	response := agentTaskRequest(t, router, http.MethodPost, "/tasks", map[string]any{
		"channel": "#meeting-room",
		"target":  "#meeting-room:f6997aed",
		"tasks":   []map[string]any{{"title": "from thread"}},
	})

	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "#meeting-room:f6997aed", targetStore.lastResolvedTarget)
	require.NotNil(t, board.createRequest.Metadata, "explicit thread target must write source-thread metadata into CreateTasks")
	require.Equal(t, "thr_1", board.createRequest.Metadata.SourceThreadChannelID)
	require.Equal(t, "msg_thread_root", board.createRequest.Metadata.SourceThreadParentMessageID)
	require.Equal(t, "#meeting-room:f6997aed", board.createRequest.Metadata.SourceThreadRef)
}

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

	board := &expectedTaskBoardService{}
	response := agentTaskRequest(t, expectedTaskTestRouter(board), http.MethodPost, "/tasks", map[string]any{
		"channel": "#agentteam",
		"tasks":   []map[string]any{{"title": "plain"}},
	})

	require.Equal(t, http.StatusOK, response.Code)
	// No explicit target ⇒ the task is not auto-associated with any thread.
	require.Nil(t, board.createRequest.Metadata)
}

func expectedTaskTestRouter(board TaskBoardService) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	RegisterRoutes(router, Dependencies{
		Auth:               fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "00000000test"}},
		TaskBoard:          board,
		TaskTargetResolver: &expectedTaskTargetStore{},
	})
	return router
}

type expectedTaskTargetStore struct {
	lastResolvedTarget string
}

// expectedTaskBoardRefForTarget simulates the real resolver mapping a
// reply-thread target ("#channel:threadid") to its parent board channel, so a
// create whose explicit target is a thread can pass the channel-consistency
// check in unit tests.
func expectedTaskBoardRefForTarget(target string) string {
	if strings.HasPrefix(target, "#") {
		if idx := strings.LastIndex(target, ":"); idx > 1 {
			return target[:idx]
		}
	}
	return target
}

// expectedTaskResolvedTarget simulates the real resolver: a reply-thread
// target ("#channel:threadid") resolves to its parent board and is attributed
// to the source thread, so createTaskMetadataFromTarget produces metadata.
func expectedTaskResolvedTarget(target string) message.TaskCommandTarget {
	resolved := message.TaskCommandTarget{TaskChannelID: "chn_1", TaskChannelRef: expectedTaskBoardRefForTarget(target)}
	if strings.HasPrefix(target, "#") {
		if idx := strings.LastIndex(target, ":"); idx > 1 && idx < len(target)-1 {
			resolved.SourceThreadChannelID = "thr_1"
			resolved.SourceThreadParentChannelID = "chn_1"
			resolved.SourceThreadParentMessageID = "msg_thread_root"
			resolved.SourceThreadRef = target
		}
	}
	return resolved
}

func (store *expectedTaskTargetStore) ResolveTaskTargetForAgent(_ context.Context, _ storeagent.AuthenticatedAgent, target string) (message.TaskCommandTarget, error) {
	store.lastResolvedTarget = target
	return expectedTaskResolvedTarget(target), nil
}

func (store *expectedTaskTargetStore) ResolveTaskSourceTargetForAgent(_ context.Context, _ storeagent.AuthenticatedAgent, target string) (message.TaskCommandTarget, error) {
	store.lastResolvedTarget = target
	return expectedTaskResolvedTarget(target), nil
}

func agentTaskRequest(t *testing.T, router http.Handler, method string, suffix string, body any) *httptest.ResponseRecorder {
	t.Helper()

	var encoded []byte
	var err error
	if body != nil {
		encoded, err = json.Marshal(body)
		require.NoError(t, err)
	}
	request := httptest.NewRequest(
		method,
		"/api/v1/workspaces/wsp_test/agents/agt_0000000000test/runtime"+suffix,
		bytes.NewReader(encoded),
	)
	setAgentAuthHeader(request)
	if body != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	response := httptest.NewRecorder()
	router.ServeHTTP(response, request)
	return response
}
