package task

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

	"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/taskboard"

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

func TestTaskParityNotificationPolicyDelegatesToTaskScope(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,
				SourceThread: &taskboard.TaskSourceThread{
					ThreadChannelID: "chn_thread",
					ParentChannelID: "chn_parent",
					ParentMessageID: "msg_1",
				},
			},
		}},
	}
	parityStore := &fakeTaskParityStore{
		panel: message.ScopeNotificationPanel{
			ScopeID:   "scope_task_1",
			ScopeType: "task",
			TaskWatcherPreview: []message.AuthorProfile{
				{
					ActorType:   message.AuthorActorTypeAgent,
					PrincipalID: "agt_1",
					MemberID:    "mem_agt_1",
					TargetID:    "alpha-agent",
					DisplayName: "Alpha Agent",
					AvatarURL:   "https://example.com/alpha.png",
				},
				{
					ActorType:   message.AuthorActorTypeHuman,
					PrincipalID: "usr_1",
					MemberID:    "mem_1",
					TargetID:    "mem_1",
					DisplayName: "Owner",
					AvatarURL:   "https://example.com/owner.png",
				},
			},
			TaskWatcherCount: 4,
			TaskMemberCount:  9,
		},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodPut, "/api/v1/workspaces/wsp_1/tasks/tsk_1/notification-policy/agents/agt_1", `{"wakeState":"auto-wake"}`)
	ctx.Params = append(ctx.Params, gin.Param{Key: "agentId", Value: "agt_1"})

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

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "task:workspace_1:scope_task_1:agt_1:auto-wake", parityStore.agentWakeCall)

	var response dto.TaskNotificationPolicyResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Equal(t, "task", response.ScopeType)
	require.Equal(t, "scope_task_1", response.ScopeID)
	require.Equal(t, 4, response.WatcherCount)
	require.Equal(t, 9, response.MemberCount)
	require.Equal(t, []dto.AuthorProfile{
		{
			ActorType:   message.AuthorActorTypeAgent,
			PrincipalID: "agt_1",
			MemberID:    "mem_agt_1",
			TargetID:    "alpha-agent",
			DisplayName: "Alpha Agent",
			AvatarURL:   "https://example.com/alpha.png",
		},
		{
			ActorType:   message.AuthorActorTypeHuman,
			PrincipalID: "usr_1",
			MemberID:    "mem_1",
			TargetID:    "mem_1",
			DisplayName: "Owner",
			AvatarURL:   "https://example.com/owner.png",
		},
	}, response.WatcherPreview)
}

func TestTaskParityBulkNotificationPolicyDelegatesToTaskScope(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",
			TaskWatcherCount: 2,
			TaskMemberCount:  3,
		},
	}
	recorder, ctx := newTaskParityTestContext(
		http.MethodPut,
		"/api/v1/workspaces/wsp_1/tasks/tsk_1/notification-policy/agents/all",
		`{"wakeState":"no-wake"}`,
	)

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

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(
		t,
		"task:workspace_1:scope_task_1:all:no-wake",
		parityStore.agentWakeCall,
	)
	var response dto.TaskNotificationPolicyResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Equal(t, "task", response.ScopeType)
	require.Equal(t, "scope_task_1", response.ScopeID)
	require.Equal(t, 2, response.WatcherCount)
	require.Equal(t, 3, response.MemberCount)
}

func TestTaskParityListWatchersDelegatesBoundedPage(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{
		taskWatcherPage: message.TaskWatcherPage{
			Watchers: []message.AuthorProfile{{
				ActorType:   message.AuthorActorTypeHuman,
				PrincipalID: "usr_1",
				MemberID:    "mem_1",
				TargetID:    "mem_1",
				DisplayName: "Owner",
			}},
			WatcherCount: 3,
			HasMore:      true,
			NextCursor:   "next_opaque",
		},
	}
	recorder, ctx := newTaskParityTestContext(http.MethodGet, "/api/v1/workspaces/wsp_1/tasks/tsk_1/watchers?limit=1&cursor=opaque", "")
	ctx.Request.URL.RawQuery = "limit=1&cursor=opaque"

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

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "workspace_1:scope_task_1:opaque:1", parityStore.taskWatcherCall)
	require.Empty(t, board.getDetailRequests, "watcher pagination must not load the Task detail graph")
	require.Len(t, board.readableScopeRequests, 1)
	var response dto.TaskWatcherPageResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
	require.Equal(t, 3, response.WatcherCount)
	require.True(t, response.HasMore)
	require.Equal(t, "next_opaque", response.NextCursor)
	require.Len(t, response.Watchers, 1)
	require.Equal(t, "mem_1", response.Watchers[0].MemberID)

	defaultStore := &fakeTaskParityStore{}
	defaultRecorder, defaultContext := newTaskParityTestContext(http.MethodGet, "/api/v1/workspaces/wsp_1/tasks/tsk_1/watchers", "")
	api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board, store: defaultStore}).ListWatchers)(defaultContext)
	require.Equal(t, http.StatusOK, defaultRecorder.Code)
	require.Equal(t, "workspace_1:scope_task_1::50", defaultStore.taskWatcherCall)
}

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

	for _, rawQuery := range []string{
		"limit=0",
		"limit=101",
		"limit=not-a-number",
		"cursor=" + strings.Repeat("x", maxTaskWatcherCursorBytes+1),
	} {
		t.Run(rawQuery[:min(len(rawQuery), 32)], func(t *testing.T) {
			board := &fakeTaskParityBoard{}
			recorder, ctx := newTaskParityTestContext(http.MethodGet, "/api/v1/workspaces/wsp_1/tasks/tsk_1/watchers?"+rawQuery, "")
			ctx.Request.URL.RawQuery = rawQuery

			api.HandleWorkspaceScoped((&ParityHandler{taskBoard: board, store: &fakeTaskParityStore{}}).ListWatchers)(ctx)

			require.Equal(t, http.StatusBadRequest, recorder.Code)
			require.Empty(t, board.readableScopeRequests)
		})
	}
}
