package task

import (
	"context"
	"net/http"
	"net/http/httptest"
	"sort"
	"strings"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type taskRouteSessionAuth struct {
	token string
	user  identity.AuthenticatedUser
}

func (auth taskRouteSessionAuth) AuthenticateSession(_ context.Context, token string) (identity.AuthenticatedUser, error) {
	if token != auth.token {
		return identity.AuthenticatedUser{}, api.ErrUnauthorized
	}
	return auth.user, nil
}

type taskRouteWorkspaceResolver struct {
	userID      string
	workspaceID string
}

func (resolver taskRouteWorkspaceResolver) GetWorkspaceForUser(_ context.Context, userID string, workspaceID string) (workspace.Summary, error) {
	if userID != resolver.userID || workspaceID != resolver.workspaceID {
		return workspace.Summary{}, api.ErrForbidden
	}
	return workspace.Summary{WorkspaceID: workspaceID, MemberID: "mem_human"}, nil
}

func taskRouteHumanUser() identity.AuthenticatedUser {
	return identity.AuthenticatedUser{User: model.User{ID: "usr_human", Name: "human", DisplayName: "Human"}}
}

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

	router := gin.New()
	RegisterRoutes(router, Dependencies{
		AuthStore: taskRouteSessionAuth{
			token: "human-token",
			user: identity.AuthenticatedUser{User: model.User{
				ID:          "usr_human",
				Name:        "human",
				DisplayName: "Human",
			}},
		},
		WorkspaceStore: taskRouteWorkspaceResolver{userID: "usr_human", workspaceID: "wsp_test"},
	})

	// A whitespace-only channel reaches the resource handler and yields its
	// local 400 without requiring a TaskBoard fixture, proving the human
	// session passed all three authentication/workspace middlewares.
	human := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_test/channels/%20/tasks", nil)
	human.Header.Set("Authorization", "Bearer human-token")
	humanResponse := httptest.NewRecorder()
	router.ServeHTTP(humanResponse, human)
	require.Equal(t, http.StatusBadRequest, humanResponse.Code)

	agent := httptest.NewRequest(http.MethodGet, "/api/v1/workspaces/wsp_test/channels/%20/tasks", nil)
	agent.Header.Set("Authorization", "Bearer agent-token")
	agent.Header.Set("X-AgentTeam-Agent-Id", "agt_test")
	agentResponse := httptest.NewRecorder()
	router.ServeHTTP(agentResponse, agent)
	require.Equal(t, http.StatusUnauthorized, agentResponse.Code)
}

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

	router := gin.New()
	RegisterRoutes(router, Dependencies{})

	counts := httputil.RouteCounts(router)
	for _, route := range taskRouteKeys() {
		require.Equal(t, 1, counts[route], route)
	}
}

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

	router := gin.New()
	RegisterRoutes(router, Dependencies{})

	counts := httputil.RouteCounts(router)
	const p = "/api/v1/workspaces/:workspaceId"
	want := []string{
		"GET " + p + "/channels/:channelId/tasks",
		"POST " + p + "/channels/:channelId/tasks",
		"GET " + p + "/channels/:channelId/tasks/:taskNumber",
		"GET " + p + "/tasks/:taskId",
		"PATCH " + p + "/tasks/:taskId",
		"POST " + p + "/tasks/:taskId/checkpoints",
	}
	for _, route := range want {
		require.Equal(t, 1, counts[route], route)
	}
}

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

	router := gin.New()
	RegisterRoutes(router, Dependencies{})

	for _, route := range router.Routes() {
		require.NotContains(t, route.Path, "collaborator-boundaries")
		require.NotContains(t, route.Path, "task-collaborator-boundaries")
	}
}

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

	router := gin.New()
	RegisterRoutes(router, Dependencies{})

	for _, route := range router.Routes() {
		require.NotContains(t, route.Path, "private-agent-wake-access")
	}
}

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

	router := gin.New()
	RegisterRoutes(router, Dependencies{})

	for _, route := range router.Routes() {
		require.NotEqual(t, "PATCH /api/v1/workspaces/:workspaceId/tasks/:taskId/status", route.Method+" "+route.Path)
	}
}

// TestRegisterRoutesOwnsFullTaskRouteSet asserts the v1 Task route set is
// exactly the registered target. The registered routes must equal the
// inventory below, so adding or removing a Task route requires updating
// the contract. Status and field mutations live behind the generic task PATCH,
// so neither status-only PATCH routes nor separate task-scoped review mutation
// routes belong on the Task registrar.
func TestRegisterRoutesOwnsFullTaskRouteSet(t *testing.T) {
	t.Parallel()

	router := gin.New()
	RegisterRoutes(router, Dependencies{})

	want := taskRouteKeys()
	got := make(map[string]struct{}, len(router.Routes()))
	for _, route := range router.Routes() {
		got[route.Method+" "+route.Path] = struct{}{}
	}

	registered := make(map[string]struct{}, len(want))
	for _, route := range want {
		registered[route] = struct{}{}
	}

	var extra []string
	for route := range got {
		method := strings.SplitN(route, " ", 2)[0]
		switch method {
		case "GET", "POST", "PATCH", "PUT", "DELETE":
		default:
			continue
		}
		if !strings.Contains(route, "/api/v1/workspaces/") {
			continue
		}
		if _, ok := registered[route]; !ok {
			extra = append(extra, route)
		}
	}
	sort.Strings(extra)
	require.Empty(t, extra,
		"Task registrar target has untracked routes — add them to taskRouteKeys() or scope them out of /api/v1/workspaces/*: %v", extra)

	for _, route := range want {
		_, present := got[route]
		require.True(t, present, "Task target route %s is registered nowhere — taskRouteKeys() must match RegisterRoutes", route)
	}
}

func taskRouteKeys() []string {
	const p = "/api/v1/workspaces/:workspaceId"
	return []string{
		"GET " + p + "/task-board/options",
		"POST " + p + "/task-board/options",
		"PATCH " + p + "/task-board/options/:optionId",
		"POST " + p + "/task-board/options/:optionId/reorder",
		"POST " + p + "/task-board/options/:optionId/deactivate",
		"POST " + p + "/task-board/options/:optionId/reactivate",
		"GET " + p + "/task-board/saved-views",
		"POST " + p + "/task-board/saved-views",
		"PATCH " + p + "/task-board/saved-views/:savedViewId",
		"DELETE " + p + "/task-board/saved-views/:savedViewId",
		"GET " + p + "/tasks/filter-options",
		"GET " + p + "/tasks/related/thread/:threadChannelId",
		"GET " + p + "/tasks/related/thread/:threadChannelId/summary",
		"GET " + p + "/tasks/related/agents/:agentId/execution/summary",
		"GET " + p + "/tasks/related/agents/:agentId/execution",
		"POST " + p + "/tasks/workspace/browse",
		"POST " + p + "/tasks/workspace/board/browse",
		"GET " + p + "/tasks/channel/:channelId/view-state",
		"PUT " + p + "/tasks/channel/:channelId/view-state",
		"POST " + p + "/tasks/channel/:channelId/browse",
		"POST " + p + "/tasks/channel/:channelId/board/browse",
		"GET " + p + "/channels/:channelId/tasks",
		"POST " + p + "/channels/:channelId/tasks",
		"GET " + p + "/channels/:channelId/tasks/:taskNumber",
		"GET " + p + "/tasks/:taskId",
		"GET " + p + "/tasks/:taskId/review-contexts",
		"GET " + p + "/tasks/:taskId/timeline",
		"POST " + p + "/tasks/:taskId/read-state",
		"PATCH " + p + "/tasks/:taskId",
		"PATCH " + p + "/tasks/:taskId/claim",
		"PATCH " + p + "/tasks/:taskId/unclaim",
		"PATCH " + p + "/tasks/:taskId/block",
		"PATCH " + p + "/tasks/:taskId/unblock",
		"POST " + p + "/tasks/:taskId/subtasks",
		"GET " + p + "/tasks/:taskId/subtasks",
		"POST " + p + "/tasks/:taskId/checkpoints",
		"GET " + p + "/tasks/:taskId/collaborators",
		"POST " + p + "/tasks/:taskId/collaborators",
		"DELETE " + p + "/tasks/:taskId/collaborators/:memberId",
		"POST " + p + "/tasks/:taskId/collaborators/leave",
		"GET " + p + "/tasks/:taskId/effective-collaborators",
		"POST " + p + "/tasks/:taskId/private-agent-context-state",
		"GET " + p + "/tasks/:taskId/private-agent-contribution-capability",
		"GET " + p + "/tasks/:taskId/private-agent-sender-capability",
		"GET " + p + "/threads/:threadChannelId/task-collaborators",
		"GET " + p + "/threads/:threadChannelId/task-collaborators/effective",
		"POST " + p + "/threads/:threadChannelId/task-collaborators",
		"DELETE " + p + "/threads/:threadChannelId/task-collaborators/:memberId",
		"POST " + p + "/threads/:threadChannelId/task-collaborators/leave",
		"POST " + p + "/threads/:threadChannelId/private-agent-context-state",
		"GET " + p + "/threads/:threadChannelId/private-agent-contribution-capability",
		"GET " + p + "/threads/:threadChannelId/private-agent-sender-capability",
		"GET " + p + "/threads/:threadChannelId/private-agent-mention-candidates",
		"GET " + p + "/channels/:channelId/task-collaborators",
		"POST " + p + "/channels/:channelId/task-collaborators",
		"DELETE " + p + "/channels/:channelId/task-collaborators/:memberId",
		"POST " + p + "/channels/:channelId/task-collaborators/leave",
		"POST " + p + "/channels/:channelId/private-agent-context-state",
		"GET " + p + "/channels/:channelId/private-agent-contribution-capability",
		"GET " + p + "/channels/:channelId/private-agent-sender-capability",
		"GET " + p + "/channels/:channelId/private-agent-mention-candidates",
		"GET " + p + "/agent-collaborators",
		"GET " + p + "/collaborator-requests",
		"POST " + p + "/collaborator-requests",
		"POST " + p + "/collaborator-requests/:approvalId/cancel",
		"GET " + p + "/tasks/:taskId/execution-history",
		"POST " + p + "/tasks/:taskId/announcements",
		"GET " + p + "/tasks/:taskId/notification-policy",
		"GET " + p + "/tasks/:taskId/watchers",
		"PUT " + p + "/tasks/:taskId/notification-policy/self",
		"PUT " + p + "/tasks/:taskId/notification-policy/agents/:agentId",
		"PUT " + p + "/tasks/:taskId/notification-policy/agents/all",
		"GET " + p + "/tasks/:taskId/active-targets",
		"POST " + p + "/tasks/:taskId/active-target",
		"DELETE " + p + "/tasks/:taskId/active-target",
		"DELETE " + p + "/tasks/:taskId",
	}
}
