package task

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"gorm.io/gorm"

	baseapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/base"
	taskapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/ticket-manager/task"
	entbase "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/base"
	taskent "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/ticket-manager/task"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/apiserver"
	"github.com/tidbcloud/aisupport/services/ticket-manager/internal/app"
	tasksvcapi "github.com/tidbcloud/aisupport/services/ticket-manager/internal/services/task/api"
)

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

	ctrl := gomock.NewController(t)
	taskService := tasksvcapi.NewMockService(ctrl)

	taskList := &taskent.TaskList{
		Updatable: entbase.Updatable{Base: entbase.Base{ID: 1001}},
		Name:      "incident-list",
		Owner:     "owner@example.com",
	}
	updatedName := "incident-list-updated"
	updatedOwner := "new-owner@example.com"
	updated := &taskent.TaskList{
		Updatable: entbase.Updatable{Base: entbase.Base{ID: 1001}},
		Name:      updatedName,
		Owner:     updatedOwner,
	}

	taskService.EXPECT().ListTaskLists(gomock.Any(), gomock.AssignableToTypeOf(tasksvcapi.ListTaskListsParam{})).
		DoAndReturn(func(_ context.Context, param tasksvcapi.ListTaskListsParam) ([]*taskent.TaskList, int64, error) {
			require.NotNil(t, param.OwnerIs)
			require.Equal(t, "owner@example.com", *param.OwnerIs)
			require.NotNil(t, param.NameIs)
			require.Equal(t, "incident-list", *param.NameIs)
			require.NotNil(t, param.Page)
			require.Equal(t, 2, param.Page.Page)
			require.Equal(t, 10, param.Page.PageSize)
			return []*taskent.TaskList{taskList}, int64(1), nil
		})
	taskService.EXPECT().CreateTaskList(gomock.Any(), tasksvcapi.CreateTaskListParam{
		Name:  "incident-list",
		Owner: "owner@example.com",
	}).Return(taskList, nil)
	taskService.EXPECT().GetTaskList(gomock.Any(), uint64(1001)).Return(taskList, nil)
	taskService.EXPECT().UpdateTaskList(gomock.Any(), uint64(1001), gomock.AssignableToTypeOf(tasksvcapi.UpdateTaskListParam{})).
		DoAndReturn(func(_ context.Context, id uint64, param tasksvcapi.UpdateTaskListParam) (*taskent.TaskList, error) {
			require.Equal(t, uint64(1001), id)
			require.NotNil(t, param.Name)
			require.Equal(t, updatedName, *param.Name)
			require.NotNil(t, param.Owner)
			require.Equal(t, updatedOwner, *param.Owner)
			return updated, nil
		})
	taskService.EXPECT().DeleteTaskList(gomock.Any(), uint64(1001)).Return(nil)

	handler := newTestHandler(taskService)

	rec := serveRequest(t, handler, http.MethodGet, "/api/v1/ticketmanager/task/lists?owner=owner@example.com&name=incident-list&page=2&page_size=10", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var listResp taskapi.ListTaskListsResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&listResp))
	require.NotNil(t, listResp.BaseResponse)
	require.Equal(t, http.StatusOK, listResp.ErrCode)
	require.Equal(t, int64(1), listResp.Total)
	require.Len(t, listResp.TaskLists, 1)
	require.Equal(t, "incident-list", listResp.TaskLists[0].Name)

	rec = serveRequest(t, handler, http.MethodPost, "/api/v1/ticketmanager/task/lists", mustMarshal(t, taskapi.CreateTaskListRequest{
		Name:  "incident-list",
		Owner: "owner@example.com",
	}))
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var createResp taskapi.CreateTaskListResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&createResp))
	require.NotNil(t, createResp.TaskList)
	require.Equal(t, uint64(1001), createResp.TaskList.ID)

	rec = serveRequest(t, handler, http.MethodGet, "/api/v1/ticketmanager/task/lists/1001", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var getResp taskapi.GetTaskListResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&getResp))
	require.NotNil(t, getResp.TaskList)
	require.Equal(t, "owner@example.com", getResp.TaskList.Owner)

	rec = serveRequest(t, handler, http.MethodPut, "/api/v1/ticketmanager/task/lists/1001", mustMarshal(t, taskapi.UpdateTaskListRequest{
		Name:  &updatedName,
		Owner: &updatedOwner,
	}))
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var updateResp taskapi.UpdateTaskListResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&updateResp))
	require.NotNil(t, updateResp.TaskList)
	require.Equal(t, updatedName, updateResp.TaskList.Name)
	require.Equal(t, updatedOwner, updateResp.TaskList.Owner)

	rec = serveRequest(t, handler, http.MethodDelete, "/api/v1/ticketmanager/task/lists/1001", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var deleteResp baseapi.BaseResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&deleteResp))
	require.Equal(t, http.StatusOK, deleteResp.ErrCode)
}

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

	ctrl := gomock.NewController(t)
	taskService := tasksvcapi.NewMockService(ctrl)

	dueAt := int64(100)
	startedAt := int64(200)
	completedAt := int64(300)
	group := taskent.TaskGroup("incident")
	var priority taskent.TaskPriority = taskent.TaskPriorityP1
	member := taskent.TaskMember{
		TaskID:      2001,
		MemberEmail: "assignee@example.com",
		Role:        taskent.TaskMemberRoleOperator,
	}
	task := &taskent.Task{
		Updatable:    entbase.Updatable{Base: entbase.Base{ID: 2001}},
		Title:        "Investigate alert",
		Description:  "check task",
		Priority:     priority,
		DueAt:        &dueAt,
		Creator:      "creator@example.com",
		StartedAt:    &startedAt,
		ParentTaskID: 3001,
		TaskListID:   2001,
		Group:        group,
		Members:      []taskent.TaskMember{member},
	}
	updatedTitle := "Investigate alert deeply"
	updated := *task
	updated.Title = updatedTitle
	completed := updated
	completed.CompletedAt = &completedAt

	taskService.EXPECT().ListTasks(gomock.Any(), gomock.AssignableToTypeOf(tasksvcapi.ListTasksParam{})).
		DoAndReturn(func(_ context.Context, param tasksvcapi.ListTasksParam) ([]*taskent.Task, int64, error) {
			require.NotNil(t, param.TaskListIDIs)
			require.Equal(t, uint64(2001), *param.TaskListIDIs)
			require.NotNil(t, param.ParentTaskIDIs)
			require.Equal(t, uint64(3001), *param.ParentTaskIDIs)
			require.NotNil(t, param.CreatorIs)
			require.Equal(t, "creator@example.com", *param.CreatorIs)
			require.Equal(t, []taskent.TaskPriority{taskent.TaskPriorityP0, taskent.TaskPriorityP1}, param.PriorityIn)
			require.NotNil(t, param.GroupIs)
			require.Equal(t, group, *param.GroupIs)
			require.NotNil(t, param.Page)
			require.Equal(t, 2, param.Page.Page)
			require.Equal(t, 10, param.Page.PageSize)
			return []*taskent.Task{task}, int64(1), nil
		})
	taskService.EXPECT().CreateTask(gomock.Any(), gomock.AssignableToTypeOf(tasksvcapi.CreateTaskParam{})).
		DoAndReturn(func(_ context.Context, param tasksvcapi.CreateTaskParam) (*taskent.Task, error) {
			require.Equal(t, task.Title, param.Title)
			require.Equal(t, task.Description, param.Description)
			require.Equal(t, priority, param.Priority)
			require.Equal(t, &dueAt, param.DueAt)
			require.Equal(t, task.Creator, param.Creator)
			require.Equal(t, &startedAt, param.StartedAt)
			require.Equal(t, task.ParentTaskID, param.ParentTaskID)
			require.Equal(t, task.TaskListID, param.TaskListID)
			require.Equal(t, group, param.Group)
			require.Equal(t, task.Members, param.Members)
			return task, nil
		})
	taskService.EXPECT().GetTask(gomock.Any(), uint64(2001)).Return(task, nil)
	taskService.EXPECT().UpdateTask(gomock.Any(), uint64(2001), gomock.AssignableToTypeOf(tasksvcapi.UpdateTaskParam{})).
		DoAndReturn(func(_ context.Context, id uint64, param tasksvcapi.UpdateTaskParam) (*taskent.Task, error) {
			require.Equal(t, uint64(2001), id)
			require.NotNil(t, param.Title)
			require.Equal(t, updatedTitle, *param.Title)
			require.Nil(t, param.Description)
			require.NotNil(t, param.Priority)
			require.Equal(t, priority, *param.Priority)
			require.NotNil(t, param.DueAt)
			require.Equal(t, dueAt, *param.DueAt)
			require.NotNil(t, param.Creator)
			require.Equal(t, task.Creator, *param.Creator)
			require.NotNil(t, param.StartedAt)
			require.Equal(t, startedAt, *param.StartedAt)
			require.NotNil(t, param.ParentTaskID)
			require.Equal(t, task.ParentTaskID, *param.ParentTaskID)
			require.NotNil(t, param.Group)
			require.Equal(t, group, *param.Group)
			require.NotNil(t, param.Members)
			require.Equal(t, task.Members, *param.Members)
			return &updated, nil
		})
	taskService.EXPECT().CompleteTask(gomock.Any(), uint64(2001)).Return(&completed, nil)
	taskService.EXPECT().DeleteTask(gomock.Any(), uint64(2001)).Return(nil)

	handler := newTestHandler(taskService)

	rec := serveRequest(t, handler, http.MethodGet, "/api/v1/ticketmanager/task/tasks?task_list_id=2001&parent_task_id=3001&creator=creator@example.com&priority=p0&priority=p1&group=incident&page=2&page_size=10", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var listResp taskapi.ListTasksResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&listResp))
	require.NotNil(t, listResp.BaseResponse)
	require.Equal(t, int64(1), listResp.Total)
	require.Len(t, listResp.Tasks, 1)
	require.Equal(t, uint64(2001), listResp.Tasks[0].ID)

	rec = serveRequest(t, handler, http.MethodPost, "/api/v1/ticketmanager/task/tasks", mustMarshal(t, taskapi.CreateTaskRequest{
		Title:        task.Title,
		Description:  task.Description,
		Priority:     priority,
		DueAt:        &dueAt,
		Creator:      task.Creator,
		StartedAt:    &startedAt,
		ParentTaskID: task.ParentTaskID,
		TaskListID:   task.TaskListID,
		Group:        group,
		Members:      task.Members,
	}))
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var createResp taskapi.CreateTaskResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&createResp))
	require.NotNil(t, createResp.Task)
	require.Equal(t, "Investigate alert", createResp.Task.Title)

	rec = serveRequest(t, handler, http.MethodGet, "/api/v1/ticketmanager/task/tasks/2001", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var getResp taskapi.GetTaskResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&getResp))
	require.NotNil(t, getResp.Task)
	require.Equal(t, uint64(2001), getResp.Task.ID)

	rec = serveRequest(t, handler, http.MethodPut, "/api/v1/ticketmanager/task/tasks/2001", mustMarshal(t, taskapi.UpdateTaskRequest{
		Title:        &updatedTitle,
		Priority:     &priority,
		DueAt:        &dueAt,
		Creator:      &task.Creator,
		StartedAt:    &startedAt,
		ParentTaskID: &task.ParentTaskID,
		Group:        &group,
		Members:      &task.Members,
	}))
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var updateResp taskapi.UpdateTaskResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&updateResp))
	require.NotNil(t, updateResp.Task)
	require.Equal(t, updatedTitle, updateResp.Task.Title)

	rec = serveRequest(t, handler, http.MethodPost, "/api/v1/ticketmanager/task/tasks/2001/complete", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var completeResp taskapi.CompleteTaskResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&completeResp))
	require.NotNil(t, completeResp.Task)
	require.Equal(t, &completedAt, completeResp.Task.CompletedAt)

	rec = serveRequest(t, handler, http.MethodDelete, "/api/v1/ticketmanager/task/tasks/2001", nil)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
}

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

	handler := newTestHandler(tasksvcapi.NewMockService(gomock.NewController(t)))

	tests := []struct {
		name   string
		method string
		path   string
		body   []byte
		msg    string
	}{
		{
			name:   "invalid task id",
			method: http.MethodGet,
			path:   "/api/v1/ticketmanager/task/tasks/not-a-number",
			msg:    "invalid syntax",
		},
		{
			name:   "invalid list id",
			method: http.MethodGet,
			path:   "/api/v1/ticketmanager/task/lists/not-a-number",
			msg:    "invalid syntax",
		},
		{
			name:   "invalid task query",
			method: http.MethodGet,
			path:   "/api/v1/ticketmanager/task/tasks?page=not-a-number",
			msg:    "invalid syntax",
		},
		{
			name:   "invalid list query",
			method: http.MethodGet,
			path:   "/api/v1/ticketmanager/task/lists?page_size=not-a-number",
			msg:    "invalid syntax",
		},
		{
			name:   "missing task fields",
			method: http.MethodPost,
			path:   "/api/v1/ticketmanager/task/tasks",
			body:   mustMarshal(t, taskapi.CreateTaskRequest{}),
			msg:    "invalid tasks query",
		},
		{
			name:   "missing task list fields",
			method: http.MethodPost,
			path:   "/api/v1/ticketmanager/task/lists",
			body:   mustMarshal(t, taskapi.CreateTaskListRequest{}),
			msg:    "Name",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			rec := serveRequest(t, handler, tt.method, tt.path, tt.body)
			require.Equal(t, http.StatusBadRequest, rec.Code, rec.Body.String())
			require.Contains(t, rec.Body.String(), tt.msg)
		})
	}
}

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

	ctrl := gomock.NewController(t)
	taskService := tasksvcapi.NewMockService(ctrl)
	taskService.EXPECT().GetTask(gomock.Any(), uint64(2001)).Return(nil, gorm.ErrRecordNotFound)
	taskService.EXPECT().UpdateTaskList(gomock.Any(), uint64(1001), gomock.Any()).Return(nil, gorm.ErrRecordNotFound)
	taskService.EXPECT().DeleteTask(gomock.Any(), uint64(2002)).Return(gorm.ErrRecordNotFound)

	handler := newTestHandler(taskService)

	rec := serveRequest(t, handler, http.MethodGet, "/api/v1/ticketmanager/task/tasks/2001", nil)
	require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "record not found")

	rec = serveRequest(t, handler, http.MethodPut, "/api/v1/ticketmanager/task/lists/1001", mustMarshal(t, taskapi.UpdateTaskListRequest{}))
	require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "record not found")

	rec = serveRequest(t, handler, http.MethodDelete, "/api/v1/ticketmanager/task/tasks/2002", nil)
	require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "record not found")
}

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

	ctrl := gomock.NewController(t)
	taskService := tasksvcapi.NewMockService(ctrl)
	taskService.EXPECT().ListTasks(gomock.Any(), gomock.Any()).Return(nil, int64(0), errors.New("database unavailable"))
	taskService.EXPECT().CreateTaskList(gomock.Any(), gomock.Any()).Return(nil, errors.New("database unavailable"))
	taskService.EXPECT().UpdateTask(gomock.Any(), uint64(2001), gomock.Any()).Return(nil, errors.New("database unavailable"))
	taskService.EXPECT().CompleteTask(gomock.Any(), uint64(2001)).Return(nil, errors.New("database unavailable"))

	handler := newTestHandler(taskService)

	rec := serveRequest(t, handler, http.MethodGet, "/api/v1/ticketmanager/task/tasks", nil)
	require.Equal(t, http.StatusInternalServerError, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "database unavailable")

	rec = serveRequest(t, handler, http.MethodPost, "/api/v1/ticketmanager/task/lists", mustMarshal(t, taskapi.CreateTaskListRequest{
		Name:  "incident-list",
		Owner: "owner@example.com",
	}))
	require.Equal(t, http.StatusInternalServerError, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "database unavailable")

	rec = serveRequest(t, handler, http.MethodPut, "/api/v1/ticketmanager/task/tasks/2001", mustMarshal(t, taskapi.UpdateTaskRequest{}))
	require.Equal(t, http.StatusInternalServerError, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "database unavailable")

	rec = serveRequest(t, handler, http.MethodPost, "/api/v1/ticketmanager/task/tasks/2001/complete", nil)
	require.Equal(t, http.StatusInternalServerError, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "database unavailable")
}

func serveRequest(t *testing.T, handler apiserver.Handler, method, path string, body []byte) *httptest.ResponseRecorder {
	t.Helper()

	var reader *bytes.Reader
	if body == nil {
		reader = bytes.NewReader(nil)
	} else {
		reader = bytes.NewReader(body)
	}

	req := httptest.NewRequest(method, path, reader)
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	gin.SetMode(gin.TestMode)
	router := gin.New()
	err := handler.Register(map[apiserver.RoutePath]gin.IRouter{
		app.APIRoutePath: router.Group(string(app.APIRoutePath)),
	})
	require.NoError(t, err)

	router.ServeHTTP(rec, req)
	return rec
}

func newTestHandler(task tasksvcapi.Service) *Handler {
	return &Handler{Task: task}
}

func mustMarshal(t *testing.T, value any) []byte {
	t.Helper()

	data, err := json.Marshal(value)
	require.NoError(t, err)
	return data
}
