package task

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

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	unreadservice "github.com/tidbcloud/AgentTeam/internal/unread"
)

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

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodPatch, "/api/v1/workspaces/wsp_1/tasks/tsk_1", strings.NewReader("{"))
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)

	serveTaskWorkspaceJSON(ctx, newRouteHandlers(Dependencies{}).detail.Patch)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, "Invalid request body", jsonError(t, recorder))
}

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

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/subtasks", strings.NewReader("{"))
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)

	serveTaskWorkspaceJSON(ctx, newRouteHandlers(Dependencies{}).subtasks.Create)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, "Invalid request body", jsonError(t, recorder))
}

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

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodPost, "/api/v1/workspaces/wsp_1/tasks/tsk_1/checkpoints", strings.NewReader("{"))
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)

	serveTaskWorkspaceJSON(ctx, newRouteHandlers(Dependencies{}).checkpoints.Create)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, "Invalid request body", jsonError(t, recorder))
}

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

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/workspaces/wsp_1/tasks/tsk_1", strings.NewReader("{"))
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)

	serveTaskWorkspace(ctx, newRouteHandlers(Dependencies{}).detail.Delete)

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Equal(t, "Invalid request body", jsonError(t, recorder))
}

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

	board := &fakeDetailTaskBoard{}
	handler := &DetailHandler{
		taskHandlerBase: taskHandlerBase{
			taskBoard: board,
		},
	}

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/workspaces/wsp_1/tasks/tsk_1", strings.NewReader(`{"subtaskMode":"delete"}`))
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)

	serveTaskWorkspace(ctx, handler.Delete)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Len(t, board.deleteRequests, 1)
	require.Equal(t, "tsk_1", board.deleteRequests[0].Task.TaskID)
	require.Equal(t, DeleteTaskSubtaskMode("delete"), board.deleteRequests[0].SubtaskMode)
}

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

	board := &fakeDetailTaskBoard{
		response: MarkTaskReadResponse{
			Task: Task{ID: "tsk_1", TaskScopeID: "task_scope_1"},
			ReadState: TaskReadState{
				UnreadCount:            0,
				LastMessageReadSeq:     12,
				LastActivityReadSeq:    34,
				ManualUnreadGeneration: 56,
			},
		},
	}
	unread := &fakeTaskUnreadRuntime{}
	projector := &fakeTaskConversationReadProjector{}
	handler := &DetailHandler{
		taskHandlerBase: taskHandlerBase{
			taskBoard:                 board,
			unreadRuntime:             unread,
			conversationReadProjector: projector,
		},
	}

	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)
	ctx.Request = httptest.NewRequest(http.MethodPost, "/api/tasks/tsk_1/read-state", nil)
	ctx.Params = gin.Params{{Key: "taskId", Value: "tsk_1"}}
	setTaskAuth(ctx)

	serveTaskWorkspace(ctx, handler.MarkRead)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Len(t, board.requests, 1)
	require.Equal(t, "tsk_1", board.requests[0].Task.TaskID)
	require.Empty(t, unread.invalidations)
	require.Equal(t, []unreadservice.ReadCursorAdvanced{{
		WorkspaceID:                 "workspace_1",
		MemberID:                    "member_1",
		ScopeID:                     "task_scope_1",
		LastReadSeq:                 12,
		ManualUnreadGeneration:      56,
		ManualUnreadGenerationKnown: true,
	}}, unread.readCursors)
	require.Equal(t, []unreadservice.ActivityReadCursorAdvanced{{
		WorkspaceID: "workspace_1",
		MemberID:    "member_1",
		ScopeID:     "task_scope_1",
		LastReadSeq: 34,
	}}, unread.activityReadCursors)
	require.Equal(t, []TaskConversationReadProjection{{
		WorkspaceID:         "workspace_1",
		UserID:              "user_1",
		MemberID:            "member_1",
		TaskID:              "tsk_1",
		ScopeID:             "task_scope_1",
		LastMessageReadSeq:  12,
		LastActivityReadSeq: 34,
	}}, projector.projections)
	var payload dto.MarkTaskReadResponse
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
	require.Equal(t, int64(0), payload.ReadState.UnreadCount)
}

type fakeDetailTaskBoard struct {
	TaskBoardService
	response       MarkTaskReadResponse
	requests       []MarkTaskReadRequest
	deleteRequests []DeleteTaskRequest
	detailResponse GetTaskDetailResponse
	detailRequests []GetTaskDetailRequest
}

func (fake *fakeDetailTaskBoard) GetTaskDetail(_ context.Context, request GetTaskDetailRequest) (GetTaskDetailResponse, error) {
	fake.detailRequests = append(fake.detailRequests, request)
	return fake.detailResponse, nil
}

func (fake *fakeDetailTaskBoard) MarkTaskRead(_ context.Context, request MarkTaskReadRequest) (MarkTaskReadResponse, error) {
	fake.requests = append(fake.requests, request)
	return fake.response, nil
}

func (fake *fakeDetailTaskBoard) DeleteTask(_ context.Context, request DeleteTaskRequest) (DeleteTaskResponse, error) {
	fake.deleteRequests = append(fake.deleteRequests, request)
	return DeleteTaskResponse{ChannelID: "chn_1", DeletedTaskIDs: []string{request.Task.TaskID}}, nil
}

type fakeTaskConversationReadProjector struct {
	projections []TaskConversationReadProjection
}

func (fake *fakeTaskConversationReadProjector) ProjectTaskConversationRead(_ context.Context, projection TaskConversationReadProjection) error {
	fake.projections = append(fake.projections, projection)
	return nil
}

type fakeTaskUnreadRuntime struct {
	states              map[string]unreadservice.ScopeUnreadState
	invalidations       []unreadservice.ScopeInvalidated
	readCursors         []unreadservice.ReadCursorAdvanced
	activityReadCursors []unreadservice.ActivityReadCursorAdvanced
}

func (fake *fakeTaskUnreadRuntime) ScopeStates(_ string, _ string, scopeIDs []string) map[string]unreadservice.ScopeUnreadState {
	if fake.states == nil {
		return map[string]unreadservice.ScopeUnreadState{}
	}
	if len(scopeIDs) == 0 {
		out := make(map[string]unreadservice.ScopeUnreadState, len(fake.states))
		for scopeID, state := range fake.states {
			out[scopeID] = state
		}
		return out
	}
	out := make(map[string]unreadservice.ScopeUnreadState, len(scopeIDs))
	for _, scopeID := range scopeIDs {
		if state, ok := fake.states[scopeID]; ok {
			out[scopeID] = state
		}
	}
	return out
}

func (fake *fakeTaskUnreadRuntime) ApplyReadCursor(event unreadservice.ReadCursorAdvanced) error {
	fake.readCursors = append(fake.readCursors, event)
	return nil
}

func (fake *fakeTaskUnreadRuntime) ApplyActivityReadCursor(event unreadservice.ActivityReadCursorAdvanced) error {
	fake.activityReadCursors = append(fake.activityReadCursors, event)
	return nil
}

func (fake *fakeTaskUnreadRuntime) ApplyScopeInvalidated(event unreadservice.ScopeInvalidated) error {
	fake.invalidations = append(fake.invalidations, event)
	return nil
}
