package exec

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

func TestTaskHandlerUsesTypedRequestsAndAuthorizedServer(t *testing.T) {
	var createBody map[string]any
	var updateBodies []map[string]any
	var claimValues []bool
	var postedBodies = make(map[string]map[string]any)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		base := "/api/v1/workspaces/workspace-1/agents/server-agent-1/runtime/tasks"
		switch {
		case r.Method == http.MethodGet && r.URL.Path == base:
			require.Equal(t, "#agentteam-in-agentteam", r.URL.Query().Get("channel"))
			_, _ = io.WriteString(w, `{"channelRef":"#agentteam-in-agentteam","tasks":[{"taskScopeId":"task-scope-1","taskNumber":1,"title":"task one","status":"todo","assigneeProfile":{"actorType":"agent","principalId":"agent-1","memberId":"member-1","targetId":"agent-1","displayName":"Implementer","avatarUrl":"https://example.test/implementer.png","serviceKind":"agent","serviceSlug":"implementer","serviceBuiltin":true,"badge":"AI"}}],"limit":20}`)
		case r.Method == http.MethodPost && r.URL.Path == base:
			require.NoError(t, json.NewDecoder(r.Body).Decode(&createBody))
			_, _ = io.WriteString(w, `{"channelRef":"#agentteam-in-agentteam","tasks":[{"taskScopeId":"task-scope-2","taskNumber":2,"title":"created","status":"todo","assigneeProfile":{"actorType":"agent","principalId":"agent-2","memberId":"member-2","displayName":"Creator"},"reused":true}]}`)
		case r.URL.Path == base+"/get":
			var body map[string]any
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			postedBodies["get"] = body
			_, _ = io.WriteString(w, `{"detail":{"task":{"taskScopeId":"task-scope-1","taskNumber":1,"title":"task one","status":"todo","assigneeProfile":{"actorType":"agent","principalId":"agent-1","memberId":"member-1","displayName":"Implementer"}},"parentTask":{"taskScopeId":"task-scope-parent","taskNumber":10,"title":"parent","assigneeProfile":{"actorType":"human","principalId":"user-parent","memberId":"member-parent","displayName":"Parent owner"}},"subtasks":[{"taskScopeId":"task-scope-child","taskNumber":11,"title":"child","assigneeProfile":{"actorType":"agent","principalId":"agent-child","memberId":"member-child","displayName":"Child owner"}}],"pinnedMessageRefs":["msg_1"],"relevantURLs":["https://example.com"],"workspaceRefs":["/workspace"]}}`)
		case r.URL.Path == base+"/update":
			var body map[string]any
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			postedBodies["update"] = body
			updateBodies = append(updateBodies, body)
			if _, explicit := body["assignee"]; explicit {
				_, _ = io.WriteString(w, `{"task":{"taskScopeId":"task-scope-1","taskNumber":1,"title":"updated","status":"in_progress","assigneeProfile":null}}`)
			} else {
				_, _ = io.WriteString(w, `{"task":{"taskScopeId":"task-scope-1","taskNumber":1,"title":"updated","status":"in_progress","assigneeProfile":{"actorType":"agent","principalId":"agent-1","memberId":"member-1","displayName":"Implementer"}}}`)
			}
		case r.URL.Path == base+"/add-checkpoint":
			var body map[string]any
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			postedBodies["checkpoint"] = body
			_, _ = io.WriteString(w, `{"task":{"taskScopeId":"task-scope-1","taskNumber":1,"title":"task one","status":"in_progress","assigneeProfile":{"actorType":"agent","principalId":"agent-1","memberId":"member-1","displayName":"Implementer"}}}`)
		case r.URL.Path == base+"/set-claim":
			var body map[string]any
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			claimValues = append(claimValues, body["claimed"].(bool))
			postedBodies["claim"] = body
			_, _ = io.WriteString(w, `{"claimed":1}`)
		case r.URL.Path == base+"/delete":
			var body map[string]any
			require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
			postedBodies["delete"] = body
			_, _ = io.WriteString(w, `{"deletedTaskNumbers":[1,2],"promotedTaskNumbers":[]}`)
		default:
			http.NotFound(w, r)
		}
	}))
	t.Cleanup(server.Close)

	authorized := runtimeAuthorization{
		Server: &apiclient.Client{
			ServerURL: server.URL, RouteWorkspaceID: "workspace-1",
			AgentID: "server-agent-1", AuthToken: "server-token", HTTPClient: server.Client(),
		},
	}
	handler := taskHandler{}
	listed, err := handler.list(context.Background(), actionScopedJSON[taskListRequest]{
		Authorized: authorized, Body: taskListRequest{Channel: "#agentteam-in-agentteam"},
	})
	require.NoError(t, err)
	require.Equal(t, int64(1), listed.Data.Tasks[0].TaskNumber)
	require.NotNil(t, listed.Data.Tasks[0].AssigneeProfile)
	require.Equal(t, &taskActorProfile{
		ActorType:   "agent",
		PrincipalID: "agent-1",
		MemberID:    "member-1",
		TargetID:    "agent-1",
		DisplayName: "Implementer",
	}, listed.Data.Tasks[0].AssigneeProfile)

	created, err := handler.create(context.Background(), actionScopedJSON[taskCreateRequest]{
		Authorized: authorized,
		Body:       taskCreateRequest{Channel: "#agentteam-in-agentteam", Target: "#agentteam-in-agentteam:thread-1", Tasks: []taskDraft{{Title: "created"}}},
	})
	require.NoError(t, err)
	require.Equal(t, int64(2), created.Data.Tasks[0].TaskNumber)
	require.Equal(t, "todo", created.Data.Tasks[0].Status)
	require.NotNil(t, created.Data.Tasks[0].AssigneeProfile)
	require.Equal(t, "agent-2", created.Data.Tasks[0].AssigneeProfile.PrincipalID)
	require.True(t, created.Data.Tasks[0].Reused)
	require.Equal(t, "#agentteam-in-agentteam:thread-1", createBody["target"])

	created, err = handler.create(context.Background(), actionScopedJSON[taskCreateRequest]{
		Authorized: authorized,
		Body: taskCreateRequest{
			Channel: "#agentteam-in-agentteam",
			Target:  "#agentteam-in-agentteam:explicit-thread",
			Tasks:   []taskDraft{{Title: "explicit target"}},
		},
	})
	require.NoError(t, err)
	require.Equal(t, int64(2), created.Data.Tasks[0].TaskNumber)
	require.Equal(t, "#agentteam-in-agentteam:explicit-thread", createBody["target"])

	got, err := handler.get(context.Background(), actionScopedJSON[taskGetRequest]{
		Authorized: authorized, Body: taskGetRequest{Channel: "#agentteam-in-agentteam", Number: 1},
	})
	require.NoError(t, err)
	require.Equal(t, "task one", got.Data.Detail.Task.Title)
	require.Equal(t, []string{"msg_1"}, got.Data.Detail.Messages)
	require.Equal(t, []string{"https://example.com"}, got.Data.Detail.URLs)
	require.Equal(t, []string{"/workspace"}, got.Data.Detail.Workspaces)
	require.NotNil(t, got.Data.Detail.Task.AssigneeProfile)
	require.Equal(t, "agent-1", got.Data.Detail.Task.AssigneeProfile.PrincipalID)
	require.NotNil(t, got.Data.Detail.ParentTask)
	require.NotNil(t, got.Data.Detail.ParentTask.AssigneeProfile)
	require.Equal(t, "user-parent", got.Data.Detail.ParentTask.AssigneeProfile.PrincipalID)
	require.Len(t, got.Data.Detail.Subtasks, 1)
	require.NotNil(t, got.Data.Detail.Subtasks[0].AssigneeProfile)
	require.Equal(t, "agent-child", got.Data.Detail.Subtasks[0].AssigneeProfile.PrincipalID)

	updated, err := handler.update(context.Background(), actionScopedJSON[taskUpdateRequest]{
		Authorized: authorized,
		Body:       taskUpdateRequest{Channel: "#agentteam-in-agentteam", Number: 1, Status: taskString("in_progress")},
	})
	require.NoError(t, err)
	require.NotNil(t, updated.Data.Task.AssigneeProfile)
	require.Equal(t, "agent-1", updated.Data.Task.AssigneeProfile.PrincipalID)
	require.Len(t, updateBodies, 1)
	require.NotContains(t, updateBodies[0], "assignee")

	unassign := ""
	updated, err = handler.update(context.Background(), actionScopedJSON[taskUpdateRequest]{
		Authorized: authorized,
		Body: taskUpdateRequest{
			Channel:  "#agentteam-in-agentteam",
			Number:   1,
			Assignee: &unassign,
		},
	})
	require.NoError(t, err)
	require.Equal(t, "in_progress", updated.Data.Task.Status)
	require.Nil(t, updated.Data.Task.AssigneeProfile)
	require.Len(t, updateBodies, 2)
	require.Contains(t, updateBodies[1], "assignee")
	require.Equal(t, "", updateBodies[1]["assignee"])
	require.NotContains(t, updateBodies[1], "status")

	checkpointed, err := handler.checkpoint(context.Background(), actionScopedJSON[taskCheckpointRequest]{
		Authorized: authorized,
		Body:       taskCheckpointRequest{Channel: "#agentteam-in-agentteam", Number: 1, Summary: "progress"},
	})
	require.NoError(t, err)
	require.Equal(t, int64(1), checkpointed.Data.Task.TaskNumber)
	require.NotNil(t, checkpointed.Data.Task.AssigneeProfile)
	require.Equal(t, "agent-1", checkpointed.Data.Task.AssigneeProfile.PrincipalID)

	claimed, err := handler.claim(context.Background(), actionScopedJSON[taskClaimRequest]{
		Authorized: authorized,
		Body:       taskClaimRequest{Channel: "#agentteam-in-agentteam", Numbers: []int{1}},
	})
	require.NoError(t, err)
	require.Equal(t, 1, claimed.Data.Claimed)

	unclaimed, err := handler.unclaim(context.Background(), actionScopedJSON[taskClaimRequest]{
		Authorized: authorized,
		Body:       taskClaimRequest{Channel: "#agentteam-in-agentteam", Numbers: []int{1}},
	})
	require.NoError(t, err)
	require.Equal(t, 1, unclaimed.Data.Claimed)
	require.Equal(t, []bool{true, false}, claimValues)
	deleted, err := handler.delete(context.Background(), actionScopedJSON[taskDeleteRequest]{
		Authorized: authorized,
		Body:       taskDeleteRequest{Channel: "#agentteam-in-agentteam", Number: 1, SubtaskMode: "delete"},
	})
	require.NoError(t, err)
	require.Equal(t, []int64{1, 2}, deleted.Data.DeletedTaskNumbers)
	require.Equal(t, float64(1), postedBodies["get"]["number"])
	require.NotContains(t, postedBodies["get"], "task_number")
	require.Equal(t, float64(1), postedBodies["update"]["number"])
	require.Equal(t, float64(1), postedBodies["checkpoint"]["number"])
	require.Equal(t, []any{float64(1)}, postedBodies["claim"]["numbers"])
	require.NotContains(t, postedBodies["claim"], "task_numbers")
	require.Equal(t, float64(1), postedBodies["delete"]["number"])
	require.Equal(t, "delete", postedBodies["delete"]["subtaskMode"])
}

func TestTaskHandlerRejectsIncompleteTypedInput(t *testing.T) {
	handler := taskHandler{}
	_, err := handler.get(context.Background(), actionScopedJSON[taskGetRequest]{})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)

	_, err = handler.claim(context.Background(), actionScopedJSON[taskClaimRequest]{})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)

	_, err = handler.delete(context.Background(), actionScopedJSON[taskDeleteRequest]{
		Body: taskDeleteRequest{Channel: "#agentteam", Number: 1, SubtaskMode: "cascade"},
	})
	require.Error(t, err)
	require.Equal(t, CodeValidation, classify(err).Code)
}

func TestTaskCreateParentScopeTransport(t *testing.T) {
	var body map[string]any
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		_, _ = io.WriteString(w, `{"channelRef":"#agentteam","tasks":[{"taskScopeId":"tsk_child","taskNumber":2,"title":"child","status":"todo"}]}`)
	}))
	t.Cleanup(server.Close)
	client := &apiclient.Client{
		ServerURL: server.URL, RouteWorkspaceID: "workspace-1",
		AgentID: "agent-1", AuthToken: "token", HTTPClient: server.Client(),
	}
	handler := taskHandler{}

	_, err := handler.create(context.Background(), actionScopedJSON[taskCreateRequest]{
		Authorized: runtimeAuthorization{
			Server: client,
		},
		Body: taskCreateRequest{
			Target: "#agentteam:thread-1",
			ParentTaskScope: &domain.MessageScopeValue{
				Kind:    domain.MessageScopeKindTask,
				ScopeID: "tsk_parent_scope",
			},
			Tasks: []taskDraft{{Title: "child"}},
		},
	})
	require.NoError(t, err)
	require.NotContains(t, body, "channel")
	require.NotContains(t, body, "parent")
	require.Equal(t, map[string]any{
		"kind":    "task",
		"scopeId": "tsk_parent_scope",
	}, body["parentTaskScope"])
	require.Equal(t, "#agentteam:thread-1", body["target"])
}

func TestTaskCreateParentScopeRejectsAmbiguousOrInvalidLocator(t *testing.T) {
	handler := taskHandler{}
	client := &apiclient.Client{}
	validScope := &domain.MessageScopeValue{
		Kind:    domain.MessageScopeKindTask,
		ScopeID: "tsk_parent_scope",
	}

	for name, input := range map[string]taskCreateRequest{
		"channel": {
			Channel: "#agentteam", ParentTaskScope: validScope, Tasks: []taskDraft{{Title: "child"}},
		},
		"parent": {
			Parent: 1, ParentTaskScope: validScope, Tasks: []taskDraft{{Title: "child"}},
		},
		"wrong kind": {
			ParentTaskScope: &domain.MessageScopeValue{Kind: domain.MessageScopeKindChannel, ScopeID: "chn_parent"},
			Tasks:           []taskDraft{{Title: "child"}},
		},
		"malformed scope": {
			ParentTaskScope: &domain.MessageScopeValue{Kind: domain.MessageScopeKindTask, ScopeID: "bad scope"},
			Tasks:           []taskDraft{{Title: "child"}},
		},
	} {
		t.Run(name, func(t *testing.T) {
			_, err := handler.create(context.Background(), actionScopedJSON[taskCreateRequest]{
				Authorized: runtimeAuthorization{Server: client},
				Body:       input,
			})
			require.Error(t, err)
			require.Equal(t, CodeValidation, classify(err).Code)
		})
	}
}

func TestTaskCreateForwardsExplicitFields(t *testing.T) {
	var body map[string]any
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		_, _ = io.WriteString(w, `{"channelRef":"#agentteam","tasks":[{"taskScopeId":"tsk_1","taskNumber":1,"title":"one","status":"todo"}]}`)
	}))
	t.Cleanup(server.Close)
	authorized := runtimeAuthorization{Server: &apiclient.Client{
		ServerURL: server.URL, RouteWorkspaceID: "workspace-1",
		AgentID: "agent-1", AuthToken: "token", HTTPClient: server.Client(),
	}}

	_, err := (taskHandler{}).create(context.Background(), actionScopedJSON[taskCreateRequest]{
		Authorized: authorized,
		Body: taskCreateRequest{
			Channel: "#agentteam", Tasks: []taskDraft{{Title: "one"}},
		},
	})
	require.NoError(t, err)
}

type stubTurnTargets struct {
	authorizationID string
	target          string
}

func (s stubTurnTargets) CurrentTurnDeliveryTarget(agentID, authorizationID string) string {
	if s.authorizationID != "" && authorizationID != s.authorizationID {
		return ""
	}
	return s.target
}

func newTaskCreateCaptureServer(t *testing.T) (*httptest.Server, *map[string]any) {
	t.Helper()
	body := make(map[string]any)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		_, _ = io.WriteString(w, `{"channelRef":"#agentteam","tasks":[{"taskScopeId":"tsk_1","taskNumber":1,"title":"a","status":"todo"}]}`)
	}))
	t.Cleanup(server.Close)
	return server, &body
}

func taskCreateScopedJSON(body taskCreateRequest, server *httptest.Server) actionScopedJSON[taskCreateRequest] {
	return actionScopedJSON[taskCreateRequest]{
		Authorized: runtimeAuthorization{AgentID: "agent-1", AuthorizationID: "auth-1", Server: &apiclient.Client{
			ServerURL: server.URL, RouteWorkspaceID: "workspace-1",
			AgentID: "agent-1", AuthToken: "token", HTTPClient: server.Client(),
		}},
		Body: body,
	}
}

func TestTaskCreateDefaultsToCurrentTurnChannel(t *testing.T) {
	server, body := newTaskCreateCaptureServer(t)
	handler := taskHandler{turnTargets: stubTurnTargets{authorizationID: "auth-1", target: "#agentteam-in-agentteam"}}

	_, err := handler.create(context.Background(), taskCreateScopedJSON(taskCreateRequest{
		Tasks: []taskDraft{{Title: "a"}},
	}, server))
	require.NoError(t, err)
	require.Equal(t, "#agentteam-in-agentteam", (*body)["channel"])
	require.NotContains(t, *body, "target")
}

func TestTaskCreateDefaultsToCurrentTurnThreadTarget(t *testing.T) {
	server, body := newTaskCreateCaptureServer(t)
	handler := taskHandler{turnTargets: stubTurnTargets{authorizationID: "auth-1", target: "#agentteam-in-agentteam:0234da66"}}

	_, err := handler.create(context.Background(), taskCreateScopedJSON(taskCreateRequest{
		Tasks: []taskDraft{{Title: "a"}},
	}, server))
	require.NoError(t, err)
	require.Equal(t, "#agentteam-in-agentteam:0234da66", (*body)["target"])
	require.NotContains(t, *body, "channel")
}

func TestInstalledTaskCreateInvokerPreservesCurrentTurnTargetReader(t *testing.T) {
	server, body := newTaskCreateCaptureServer(t)
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	routeAdapter := adapter{catalog: catalog, builtin: make(map[string]builtinInvoker)}
	registrations := builtinRouteRegistrations(
		agentHandler{}, teamHandler{}, channelHandler{}, profileHandler{}, projectHandler{}, reviewHandler{},
		roleHandler{}, skillHandler{}, scheduledMessageHandler{},
		taskHandler{turnTargets: stubTurnTargets{authorizationID: "auth-1", target: "#agentteam-in-agentteam:0234da66"}},
		workflowHandler{}, workspaceHandler{},
	)
	for _, registration := range registrations {
		if registration.Domain == "task" && registration.Action == "create" {
			registration.Install(routeAdapter)
			break
		}
	}
	invoker, ok := routeAdapter.builtin["task create"]
	require.True(t, ok)

	authorized := taskCreateScopedJSON(taskCreateRequest{}, server).Authorized
	_, err = invoker.call(context.Background(), authorized, json.RawMessage(`{"tasks":[{"title":"a"}]}`))
	require.NoError(t, err)
	require.Equal(t, "#agentteam-in-agentteam:0234da66", (*body)["target"])
	require.NotContains(t, *body, "channel")
}

func TestTaskCreateExplicitArgumentsWinOverTurnDefault(t *testing.T) {
	server, body := newTaskCreateCaptureServer(t)
	handler := taskHandler{turnTargets: stubTurnTargets{authorizationID: "auth-1", target: "#other"}}

	_, err := handler.create(context.Background(), taskCreateScopedJSON(taskCreateRequest{
		Channel: "#agentteam",
		Tasks:   []taskDraft{{Title: "a"}},
	}, server))
	require.NoError(t, err)
	require.Equal(t, "#agentteam", (*body)["channel"])
}

func TestTaskCreateFailsClosedWithoutUsableTurnTarget(t *testing.T) {
	cases := []struct {
		name       string
		turnTarget string
		message    string
	}{
		{name: "no turn target", turnTarget: "", message: "channel is required"},
		{name: "dm turn", turnTarget: "dm:@user", message: "direct message"},
		{name: "dm reply thread turn", turnTarget: "dm:@user:0234da66", message: "direct message"},
		{name: "task discussion turn", turnTarget: "#tsk_abc123", message: "task discussion"},
		{name: "team turn", turnTarget: "#team_abcdefghijklmn", message: "does not map to a task board"},
		{name: "workflow turn", turnTarget: "workflow:plan1", message: "does not map to a task board"},
		{name: "malformed turn target", turnTarget: "#Bad_Slug!", message: "channel is required"},
	}
	for _, testCase := range cases {
		t.Run(testCase.name, func(t *testing.T) {
			server, _ := newTaskCreateCaptureServer(t)
			handler := taskHandler{turnTargets: stubTurnTargets{authorizationID: "auth-1", target: testCase.turnTarget}}

			_, err := handler.create(context.Background(), taskCreateScopedJSON(taskCreateRequest{
				Tasks: []taskDraft{{Title: "a"}},
			}, server))
			require.Error(t, err)
			require.Contains(t, err.Error(), testCase.message)
			require.Equal(t, CodeValidation, classify(err).Code)
		})
	}
}

func TestTaskCreateFailsClosedWithoutTurnReader(t *testing.T) {
	server, _ := newTaskCreateCaptureServer(t)
	handler := taskHandler{}

	_, err := handler.create(context.Background(), taskCreateScopedJSON(taskCreateRequest{
		Tasks: []taskDraft{{Title: "a"}},
	}, server))
	require.Error(t, err)
	require.Contains(t, err.Error(), "channel is required")
	require.Equal(t, CodeValidation, classify(err).Code)
}

func TestTaskCreateFailsClosedWhenAuthorizationGenerationReplaced(t *testing.T) {
	server, _ := newTaskCreateCaptureServer(t)
	handler := taskHandler{turnTargets: stubTurnTargets{authorizationID: "auth-new", target: "#new-chan:thread2"}}

	_, err := handler.create(context.Background(), taskCreateScopedJSON(taskCreateRequest{
		Tasks: []taskDraft{{Title: "a"}},
	}, server))
	require.Error(t, err)
	require.Contains(t, err.Error(), "channel is required")
	require.Equal(t, CodeValidation, classify(err).Code)
}
