package handlers

import (
	"encoding/json"
	"fmt"
	"net/http"
	"testing"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/treytartt/honeydue-api/internal/dto/requests"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/repositories"
	"github.com/treytartt/honeydue-api/internal/services"
	"github.com/treytartt/honeydue-api/internal/testutil"
	"gorm.io/gorm"
)

func setupTaskHandler(t *testing.T) (*TaskHandler, *echo.Echo, *gorm.DB) {
	db := testutil.SetupTestDB(t)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	taskService := services.NewTaskService(taskRepo, residenceRepo)
	handler := NewTaskHandler(taskService, nil)
	e := testutil.SetupTestRouter()
	return handler, e, db
}

func TestTaskHandler_BulkCreateTasks(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	tmpl := models.TaskTemplate{Title: "Change HVAC Filter", IsActive: true}
	require.NoError(t, db.Create(&tmpl).Error)

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/bulk/", handler.BulkCreateTasks)

	t.Run("creates all tasks and returns 201", func(t *testing.T) {
		req := requests.BulkCreateTasksRequest{
			ResidenceID: residence.ID,
			Tasks: []requests.CreateTaskRequest{
				{ResidenceID: residence.ID, Title: "Bulk A", TemplateID: &tmpl.ID},
				{ResidenceID: residence.ID, Title: "Bulk B"},
			},
		}
		w := testutil.MakeRequest(e, "POST", "/api/tasks/bulk/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusCreated)

		var response map[string]interface{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response))
		assert.EqualValues(t, 2, response["created_count"])
		tasks := response["tasks"].([]interface{})
		require.Len(t, tasks, 2)
		first := tasks[0].(map[string]interface{})
		require.NotNil(t, first["template_id"])
		assert.EqualValues(t, tmpl.ID, first["template_id"])
	})

	t.Run("empty task list returns 400", func(t *testing.T) {
		req := requests.BulkCreateTasksRequest{
			ResidenceID: residence.ID,
			Tasks:       []requests.CreateTaskRequest{},
		}
		w := testutil.MakeRequest(e, "POST", "/api/tasks/bulk/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("more than 50 tasks rejected by validator", func(t *testing.T) {
		big := make([]requests.CreateTaskRequest, 51)
		for i := range big {
			big[i] = requests.CreateTaskRequest{ResidenceID: residence.ID, Title: "n"}
		}
		req := requests.BulkCreateTasksRequest{ResidenceID: residence.ID, Tasks: big}
		w := testutil.MakeRequest(e, "POST", "/api/tasks/bulk/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("foreign residence returns 403", func(t *testing.T) {
		foreigner := testutil.CreateTestUser(t, db, "intruder", "intruder@test.com", "password")
		foreignerResidence := testutil.CreateTestResidence(t, db, foreigner.ID, "Not Yours")

		req := requests.BulkCreateTasksRequest{
			ResidenceID: foreignerResidence.ID,
			Tasks: []requests.CreateTaskRequest{
				{ResidenceID: foreignerResidence.ID, Title: "Nope"},
			},
		}
		w := testutil.MakeRequest(e, "POST", "/api/tasks/bulk/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusForbidden)
	})
}

func TestTaskHandler_CreateTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateTask)

	t.Run("successful task creation", func(t *testing.T) {
		req := requests.CreateTaskRequest{
			ResidenceID: residence.ID,
			Title:       "Fix leaky faucet",
			Description: "Kitchen faucet is dripping",
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusCreated)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, "Fix leaky faucet", taskData["title"])
		assert.Equal(t, "Kitchen faucet is dripping", taskData["description"])
		assert.Equal(t, float64(residence.ID), taskData["residence_id"])
		assert.Equal(t, false, taskData["is_cancelled"])
		assert.Equal(t, false, taskData["is_archived"])
	})

	t.Run("task creation with optional fields", func(t *testing.T) {
		var category models.TaskCategory
		db.First(&category)
		var priority models.TaskPriority
		db.First(&priority)

		dueDate := requests.FlexibleDate{Time: time.Now().AddDate(0, 0, 7)}
		estimatedCost := decimal.NewFromFloat(150.50)

		req := requests.CreateTaskRequest{
			ResidenceID:   residence.ID,
			Title:         "Install new lights",
			Description:   "Replace old light fixtures",
			CategoryID:    &category.ID,
			PriorityID:    &priority.ID,
			DueDate:       &dueDate,
			EstimatedCost: &estimatedCost,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusCreated)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, "Install new lights", taskData["title"])
		// Note: Category and Priority are no longer preloaded for performance
		// Client resolves from cache using category_id and priority_id
		assert.NotNil(t, taskData["category_id"], "category_id should be set")
		assert.NotNil(t, taskData["priority_id"], "priority_id should be set")
		assert.Equal(t, "150.5", taskData["estimated_cost"]) // Decimal serializes as string
	})

	t.Run("task creation without residence access", func(t *testing.T) {
		otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
		otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House")

		req := requests.CreateTaskRequest{
			ResidenceID: otherResidence.ID,
			Title:       "Unauthorized Task",
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusForbidden)
	})
}

func TestTaskHandler_GetTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/:id/", handler.GetTask)

	otherGroup := e.Group("/api/other-tasks")
	otherGroup.Use(testutil.MockAuthMiddleware(otherUser))
	otherGroup.GET("/:id/", handler.GetTask)

	t.Run("get own task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/%d/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Equal(t, "Test Task", response["title"])
		assert.Equal(t, float64(task.ID), response["id"])
	})

	t.Run("get non-existent task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/tasks/9999/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusNotFound)
	})

	t.Run("access denied for other user", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/other-tasks/%d/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusForbidden)
	})
}

func TestTaskHandler_ListTasks(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 1")
	testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 2")
	testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 3")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/", handler.ListTasks)

	t.Run("list tasks", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/tasks/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		// ListTasks returns a kanban board object, not an array
		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Verify kanban structure
		assert.Contains(t, response, "columns")
		assert.Contains(t, response, "days_threshold")

		// Count total tasks across all columns
		columns := response["columns"].([]interface{})
		totalTasks := 0
		for _, col := range columns {
			column := col.(map[string]interface{})
			totalTasks += int(column["count"].(float64))
		}
		assert.Equal(t, 3, totalTasks)
	})
}

func TestTaskHandler_GetTasksByResidence(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Create tasks with different states
	testutil.CreateTestTask(t, db, residence.ID, user.ID, "Active Task")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/by-residence/:residence_id/", handler.GetTasksByResidence)

	t.Run("get kanban columns", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", residence.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Contains(t, response, "columns")
		assert.Contains(t, response, "days_threshold")
		assert.Contains(t, response, "residence_id")

		columns := response["columns"].([]interface{})
		assert.Len(t, columns, 5) // 5 visible kanban columns (cancelled/archived hidden)
	})

	t.Run("kanban column structure", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", residence.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		columns := response["columns"].([]interface{})
		firstColumn := columns[0].(map[string]interface{})

		// Verify column structure
		assert.Contains(t, firstColumn, "name")
		assert.Contains(t, firstColumn, "display_name")
		assert.Contains(t, firstColumn, "tasks")
		assert.Contains(t, firstColumn, "count")
		assert.Contains(t, firstColumn, "color")
		assert.Contains(t, firstColumn, "icons")
		assert.Contains(t, firstColumn, "button_types")
	})
}

func TestTaskHandler_UpdateTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Original Title")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.PUT("/:id/", handler.UpdateTask)

	t.Run("update task", func(t *testing.T) {
		newTitle := "Updated Title"
		newDesc := "Updated description"
		req := requests.UpdateTaskRequest{
			Title:       &newTitle,
			Description: &newDesc,
		}

		w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/tasks/%d/", task.ID), req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, "Updated Title", taskData["title"])
		assert.Equal(t, "Updated description", taskData["description"])
	})
}

func TestTaskHandler_DeleteTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Delete")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.DELETE("/:id/", handler.DeleteTask)

	t.Run("delete task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/tasks/%d/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")
		assert.Contains(t, response["data"], "deleted")
	})
}

func TestTaskHandler_CancelTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Cancel")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/:id/cancel/", handler.CancelTask)

	t.Run("cancel task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/cancel/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, true, taskData["is_cancelled"])
	})

	t.Run("cancel already cancelled task", func(t *testing.T) {
		// Already cancelled from previous test
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/cancel/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})
}

func TestTaskHandler_UncancelTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Uncancel")

	// Cancel first
	taskRepo := repositories.NewTaskRepository(db)
	taskRepo.Cancel(task.ID, task.Version)

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/:id/uncancel/", handler.UncancelTask)

	t.Run("uncancel task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/uncancel/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, false, taskData["is_cancelled"])
	})
}

func TestTaskHandler_ArchiveTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Archive")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/:id/archive/", handler.ArchiveTask)

	t.Run("archive task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/archive/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, true, taskData["is_archived"])
	})
}

func TestTaskHandler_UnarchiveTask(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Unarchive")

	// Archive first
	taskRepo := repositories.NewTaskRepository(db)
	taskRepo.Archive(task.ID, task.Version)

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/:id/unarchive/", handler.UnarchiveTask)

	t.Run("unarchive task", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/unarchive/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})
		assert.Equal(t, false, taskData["is_archived"])
	})
}

func TestTaskHandler_MarkInProgress(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Start")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/:id/mark-in-progress/", handler.MarkInProgress)

	t.Run("mark in progress", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/mark-in-progress/", task.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")
		assert.NotNil(t, response["data"])
	})
}

func TestTaskHandler_CreateCompletion(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Complete")

	authGroup := e.Group("/api/task-completions")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateCompletion)

	t.Run("create completion", func(t *testing.T) {
		completedAt := time.Now().UTC()
		req := requests.CreateTaskCompletionRequest{
			TaskID:      task.ID,
			CompletedAt: &completedAt,
			Notes:       "Completed successfully",
		}

		w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusCreated)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		completionData := response["data"].(map[string]interface{})
		testutil.AssertJSONFieldExists(t, completionData, "id")
		assert.Equal(t, float64(task.ID), completionData["task_id"])
		assert.Equal(t, "Completed successfully", completionData["notes"])
	})
}

func TestTaskHandler_CreateCompletion_Rating6_Returns400(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Rate Me")

	authGroup := e.Group("/api/task-completions")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateCompletion)

	t.Run("rating out of bounds rejected", func(t *testing.T) {
		rating := 6
		req := requests.CreateTaskCompletionRequest{
			TaskID: task.ID,
			Rating: &rating,
		}

		w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("rating zero rejected", func(t *testing.T) {
		rating := 0
		req := requests.CreateTaskCompletionRequest{
			TaskID: task.ID,
			Rating: &rating,
		}

		w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("rating 5 accepted", func(t *testing.T) {
		rating := 5
		completedAt := time.Now().UTC()
		req := requests.CreateTaskCompletionRequest{
			TaskID:      task.ID,
			CompletedAt: &completedAt,
			Rating:      &rating,
		}

		w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusCreated)
	})
}

func TestTaskHandler_ListCompletions(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")

	// Create completions
	for i := 0; i < 3; i++ {
		db.Create(&models.TaskCompletion{
			TaskID:        task.ID,
			CompletedByID: user.ID,
			CompletedAt:   time.Now().UTC(),
		})
	}

	authGroup := e.Group("/api/task-completions")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/", handler.ListCompletions)

	t.Run("list completions", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/task-completions/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Len(t, response, 3)
	})
}

func TestTaskHandler_GetCompletion(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")

	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
		Notes:         "Test completion",
	}
	db.Create(completion)

	authGroup := e.Group("/api/task-completions")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/:id/", handler.GetCompletion)

	t.Run("get completion", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/task-completions/%d/", completion.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Equal(t, float64(completion.ID), response["id"])
		assert.Equal(t, "Test completion", response["notes"])
	})
}

func TestTaskHandler_DeleteCompletion(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")

	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
	}
	db.Create(completion)

	authGroup := e.Group("/api/task-completions")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.DELETE("/:id/", handler.DeleteCompletion)

	t.Run("delete completion", func(t *testing.T) {
		w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/task-completions/%d/", completion.ID), nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")
		assert.Contains(t, response["data"], "deleted")
	})
}

func TestTaskHandler_CreateTask_EmptyTitle_Returns400(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateTask)

	t.Run("empty body returns 400 with validation errors", func(t *testing.T) {
		w := testutil.MakeRequest(e, "POST", "/api/tasks/", map[string]interface{}{}, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusBadRequest)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Should contain structured validation error
		assert.Contains(t, response, "error")
		assert.Contains(t, response, "fields")

		fields := response["fields"].(map[string]interface{})
		assert.Contains(t, fields, "residence_id", "validation error should reference 'residence_id'")
		assert.Contains(t, fields, "title", "validation error should reference 'title'")
	})

	t.Run("missing title returns 400", func(t *testing.T) {
		req := map[string]interface{}{
			"residence_id": residence.ID,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusBadRequest)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Contains(t, response, "fields")
		fields := response["fields"].(map[string]interface{})
		assert.Contains(t, fields, "title", "validation error should reference 'title'")
	})

	t.Run("missing residence_id returns 400", func(t *testing.T) {
		req := map[string]interface{}{
			"title": "Test Task",
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusBadRequest)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Contains(t, response, "fields")
		fields := response["fields"].(map[string]interface{})
		assert.Contains(t, fields, "residence_id", "validation error should reference 'residence_id'")
	})
}

func TestTaskHandler_GetLookups(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "user", "user@test.com", "password")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.GET("/categories/", handler.GetCategories)
	authGroup.GET("/priorities/", handler.GetPriorities)
	authGroup.GET("/frequencies/", handler.GetFrequencies)

	t.Run("get categories", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/tasks/categories/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Greater(t, len(response), 0)
		assert.Contains(t, response[0], "id")
		assert.Contains(t, response[0], "name")
	})

	t.Run("get priorities", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/tasks/priorities/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Greater(t, len(response), 0)
		assert.Contains(t, response[0], "id")
		assert.Contains(t, response[0], "name")
		assert.Contains(t, response[0], "level")
	})

	t.Run("get frequencies", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/tasks/frequencies/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		var response []map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		assert.Greater(t, len(response), 0)
	})
}

func TestTaskHandler_JSONResponses(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateTask)
	authGroup.GET("/", handler.ListTasks)

	t.Run("task response has correct JSON structure", func(t *testing.T) {
		req := requests.CreateTaskRequest{
			ResidenceID: residence.ID,
			Title:       "JSON Test Task",
			Description: "Testing JSON structure",
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusCreated)

		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be wrapped in WithSummaryResponse
		assert.Contains(t, response, "data")
		assert.Contains(t, response, "summary")

		taskData := response["data"].(map[string]interface{})

		// Required fields in task data
		assert.Contains(t, taskData, "id")
		assert.Contains(t, taskData, "residence_id")
		assert.Contains(t, taskData, "created_by_id")
		assert.Contains(t, taskData, "title")
		assert.Contains(t, taskData, "description")
		assert.Contains(t, taskData, "is_cancelled")
		assert.Contains(t, taskData, "is_archived")
		assert.Contains(t, taskData, "created_at")
		assert.Contains(t, taskData, "updated_at")

		// Type checks
		assert.IsType(t, float64(0), taskData["id"])
		assert.IsType(t, "", taskData["title"])
		assert.IsType(t, false, taskData["is_cancelled"])
		assert.IsType(t, false, taskData["is_archived"])

		// Summary should have expected fields
		summary := response["summary"].(map[string]interface{})
		assert.Contains(t, summary, "total_residences")
		assert.Contains(t, summary, "total_tasks")
		assert.Contains(t, summary, "total_pending")
		assert.Contains(t, summary, "total_overdue")
	})

	t.Run("list response returns kanban board", func(t *testing.T) {
		w := testutil.MakeRequest(e, "GET", "/api/tasks/", nil, "test-token")

		testutil.AssertStatusCode(t, w, http.StatusOK)

		// ListTasks returns a kanban board object with columns
		var response map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)

		// Response should be a kanban board object
		assert.Contains(t, response, "columns")
		assert.Contains(t, response, "days_threshold")
		assert.IsType(t, []interface{}{}, response["columns"])
	})
}

// =============================================================================
// Part 3: Handler-Level Edge Cases (TDD)
// =============================================================================

func TestTaskHandler_CreateCompletion_RatingValidation(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/task-completions")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateCompletion)

	tests := []struct {
		name       string
		rating     int
		wantStatus int
	}{
		{"rating_0_rejected", 0, http.StatusBadRequest},
		{"rating_negative1_rejected", -1, http.StatusBadRequest},
		{"rating_1_accepted", 1, http.StatusCreated},
		{"rating_5_accepted", 5, http.StatusCreated},
		{"rating_6_rejected", 6, http.StatusBadRequest},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a fresh task for each accepted rating (otherwise it's completed)
			task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Rate Me "+tt.name)
			completedAt := time.Now().UTC()
			rating := tt.rating
			req := requests.CreateTaskCompletionRequest{
				TaskID:      task.ID,
				CompletedAt: &completedAt,
				Rating:      &rating,
			}

			w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")
			testutil.AssertStatusCode(t, w, tt.wantStatus)
		})
	}
}

func TestTaskHandler_CreateTask_TitleBoundary(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateTask)

	t.Run("title_exactly_200_chars_accepted", func(t *testing.T) {
		title200 := ""
		for i := 0; i < 200; i++ {
			title200 += "A"
		}
		req := requests.CreateTaskRequest{
			ResidenceID: residence.ID,
			Title:       title200,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusCreated)
	})

	t.Run("title_201_chars_rejected", func(t *testing.T) {
		title201 := ""
		for i := 0; i < 201; i++ {
			title201 += "A"
		}
		req := requests.CreateTaskRequest{
			ResidenceID: residence.ID,
			Title:       title201,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})
}

func TestTaskHandler_CreateTask_DescriptionBoundary(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateTask)

	t.Run("description_exactly_10000_chars_accepted", func(t *testing.T) {
		desc10000 := ""
		for i := 0; i < 10000; i++ {
			desc10000 += "B"
		}
		req := requests.CreateTaskRequest{
			ResidenceID: residence.ID,
			Title:       "Long Desc Task",
			Description: desc10000,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusCreated)
	})
}

func TestTaskHandler_CreateTask_CustomIntervalDaysValidation(t *testing.T) {
	handler, e, db := setupTaskHandler(t)
	testutil.SeedLookupData(t, db)
	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	authGroup := e.Group("/api/tasks")
	authGroup.Use(testutil.MockAuthMiddleware(user))
	authGroup.POST("/", handler.CreateTask)

	t.Run("custom_interval_days_0_rejected", func(t *testing.T) {
		// Validation tag: min=1, so 0 should be rejected
		interval := 0
		req := map[string]interface{}{
			"residence_id":        residence.ID,
			"title":               "Custom Interval Zero",
			"custom_interval_days": interval,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("custom_interval_days_negative1_rejected", func(t *testing.T) {
		interval := -1
		req := map[string]interface{}{
			"residence_id":        residence.ID,
			"title":               "Custom Interval Negative",
			"custom_interval_days": interval,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusBadRequest)
	})

	t.Run("custom_interval_days_1_accepted", func(t *testing.T) {
		interval := 1
		req := requests.CreateTaskRequest{
			ResidenceID:        residence.ID,
			Title:              "Custom Interval One",
			CustomIntervalDays: &interval,
		}

		w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
		testutil.AssertStatusCode(t, w, http.StatusCreated)
	})
}
