package agent

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

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

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

// expectedTaskTargetNotFoundStore simulates the resolver refusing a
// syntactically-valid but unknown/inaccessible #channel with a no-leak
// not-found — which must NOT carry the invalid-channel-target code.
type expectedTaskTargetNotFoundStore struct{}

func (expectedTaskTargetNotFoundStore) ResolveTaskTargetForAgent(context.Context, storeagent.AuthenticatedAgent, string) (message.TaskCommandTarget, error) {
	return message.TaskCommandTarget{}, taskboard.ErrNotFound
}

func (expectedTaskTargetNotFoundStore) ResolveTaskSourceTargetForAgent(context.Context, storeagent.AuthenticatedAgent, string) (message.TaskCommandTarget, error) {
	return message.TaskCommandTarget{}, taskboard.ErrNotFound
}

// expectedTaskTargetRawScopeForbiddenStore simulates the canonical resolver
// refusing a raw task-scope target with one uniform ErrForbidden for both a
// missing/unresolvable Task and an existing but unauthorized Task. The server
// must map both to identical transport behavior (403, no-leak), so a caller
// cannot distinguish "Task does not exist" from "Task exists but I lack the
// board Channel membership".
type expectedTaskTargetRawScopeForbiddenStore struct{}

func (expectedTaskTargetRawScopeForbiddenStore) ResolveTaskTargetForAgent(context.Context, storeagent.AuthenticatedAgent, string) (message.TaskCommandTarget, error) {
	return message.TaskCommandTarget{}, taskboard.ErrForbidden
}

func (expectedTaskTargetRawScopeForbiddenStore) ResolveTaskSourceTargetForAgent(context.Context, storeagent.AuthenticatedAgent, string) (message.TaskCommandTarget, error) {
	return message.TaskCommandTarget{}, taskboard.ErrForbidden
}

// TestAgentTaskChannelTargetDiagnostic exercises the real canonical
// route for every Task action: a syntactically non-usable channel is rejected
// with the stable task_channel_target_invalid code before the resolver runs.
// Representative valid #channel / reply-thread targets resolve normally, and an
// unknown but syntactically valid #channel stays a no-leak not-found without
// that code.
func TestAgentTaskChannelTargetDiagnostic(t *testing.T) {
	t.Parallel()
	gin.SetMode(gin.TestMode)

	auth := fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{AgentID: "agt_0000000000test", WorkspaceID: "wsp_test", MemberID: "00000000test"}}
	targetStore := &expectedTaskTargetStore{}
	successRouter := func() *gin.Engine {
		router := gin.New()
		RegisterRoutes(router, Dependencies{Auth: auth, TaskBoard: &expectedTaskBoardService{}, TaskTargetResolver: targetStore})
		return router
	}

	const invalidChannel = "agentteam-in-agentteam"
	routeTable := []struct {
		name   string
		method string
		path   string
		body   any
	}{
		{"list", http.MethodGet, "/tasks?channel=" + invalidChannel, nil},
		{"create", http.MethodPost, "/tasks", map[string]any{"channel": invalidChannel, "tasks": []map[string]any{{"title": "x"}}}},
		{"get", http.MethodPost, "/tasks/get", map[string]any{"channel": invalidChannel, "number": 1}},
		{"update", http.MethodPost, "/tasks/update", map[string]any{"channel": invalidChannel, "number": 1, "title": "x"}},
		{"add-checkpoint", http.MethodPost, "/tasks/add-checkpoint", map[string]any{"channel": invalidChannel, "number": 1, "summary": "x"}},
		{"set-claim", http.MethodPost, "/tasks/set-claim", map[string]any{"channel": invalidChannel, "numbers": []any{1}, "claimed": true}},
	}
	for _, tc := range routeTable {
		t.Run(tc.name+"_invalid_channel", func(t *testing.T) {
			targetStore.lastResolvedTarget = ""
			resp := agentTaskRequest(t, successRouter(), tc.method, tc.path, tc.body)
			require.Equalf(t, http.StatusBadRequest, resp.Code, "%s: body=%s", tc.name, resp.Body.String())
			var body struct {
				Code string `json:"code"`
			}
			require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body))
			require.Equalf(t, taskboard.InvalidChannelTargetCode, body.Code, "%s", tc.name)
			require.Emptyf(t, targetStore.lastResolvedTarget, "%s: resolver must not be reached", tc.name)
		})
	}

	post := func(router http.Handler, channel string) (int, string) {
		resp := agentTaskRequest(t, router, http.MethodPost, "/tasks/get", map[string]any{"channel": channel, "number": 1})
		var body struct {
			Code string `json:"code"`
		}
		_ = json.Unmarshal(resp.Body.Bytes(), &body)
		return resp.Code, body.Code
	}

	// Invalid-target variants on the real /tasks/get envelope: bare name, raw
	// channel id, DM, and DM reply thread all map to the stable code before the
	// resolver runs.
	for _, channel := range []string{"agentteam-in-agentteam", "chn_abc", "dm:@alice", "dm:@alice:AbCd1234"} {
		targetStore.lastResolvedTarget = ""
		status, code := post(successRouter(), channel)
		require.Equalf(t, http.StatusBadRequest, status, "invalid variant %q", channel)
		require.Equalf(t, taskboard.InvalidChannelTargetCode, code, "invalid variant %q", channel)
		require.Emptyf(t, targetStore.lastResolvedTarget, "invalid variant %q: resolver must not be reached", channel)
	}

	for _, channel := range []string{"#agentteam", "#agentteam:09b93379"} {
		targetStore.lastResolvedTarget = ""
		status, _ := post(successRouter(), channel)
		require.Equalf(t, http.StatusOK, status, "channel %q", channel)
		require.NotEmptyf(t, targetStore.lastResolvedTarget, "resolver must run for %q", channel)
	}

	notFoundRouter := gin.New()
	RegisterRoutes(notFoundRouter, Dependencies{Auth: auth, TaskBoard: &expectedTaskBoardService{}, TaskTargetResolver: expectedTaskTargetNotFoundStore{}})
	status, code := post(notFoundRouter, "#unknown-channel")
	require.Equal(t, http.StatusNotFound, status)
	require.NotEqual(t, taskboard.InvalidChannelTargetCode, code)
}

// TestAgentRawTaskScopeOracleProvesNonEnumerating403 proves the raw
// task-scope resolution oracle: a missing/unresolvable Task and an existing but
// unauthorized Task are both refused with one identical 403 transport response,
// so a caller cannot learn Task existence from the HTTP status or body.
