package services

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/treytartt/honeydue-api/internal/dto/responses"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/repositories"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

/*
================================================================================
TASK CATEGORIZATION LOGIC - COMPREHENSIVE TEST SUITE
================================================================================

This file contains the definitive tests for task categorization logic.
The categorization determines which kanban column a task appears in.

CORE CONCEPTS:
-------------
1. DueDate: The original due date set when task was created
2. NextDueDate: The "effective" due date used for categorization
   - For new tasks: NextDueDate = DueDate
   - For completed one-time tasks: NextDueDate = nil
   - For completed recurring tasks: NextDueDate = CompletedAt + FrequencyDays

KANBAN COLUMNS (in priority order):
----------------------------------
1. CANCELLED: Task.IsCancelled = true
2. COMPLETED: NextDueDate = nil AND has completions (one-time task done)
3. IN_PROGRESS: InProgress = true
4. OVERDUE: NextDueDate < now
5. DUE_SOON: NextDueDate < now + daysThreshold (default 30)
6. UPCOMING: Everything else (NextDueDate >= threshold or no due date)

RECURRING TASK BEHAVIOR:
-----------------------
- Recurring tasks NEVER go to "Completed" column permanently
- After completion, NextDueDate is updated to CompletedAt + FrequencyDays
- They cycle through: Upcoming -> Due Soon -> Overdue -> (complete) -> Upcoming...

ONE-TIME TASK BEHAVIOR:
----------------------
- One-time tasks (frequency=Once or frequency=nil) go to "Completed" when done
- After completion, NextDueDate = nil signals permanent completion
- They stay in "Completed" column forever

================================================================================
*/

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================

// ptr returns a pointer to the given value
func ptr[T any](v T) *T {
	return &v
}

// daysFromNow returns a time.Time that is n days from now
func daysFromNow(n int) time.Time {
	return time.Now().UTC().AddDate(0, 0, n)
}

// daysAgo returns a time.Time that is n days ago
func daysAgo(n int) time.Time {
	return time.Now().UTC().AddDate(0, 0, -n)
}

// isTaskCompleted checks if a task is permanently completed (one-time task done).
// A task is completed when it has completions AND NextDueDate is nil.
func isTaskCompleted(task *models.Task) bool {
	if len(task.Completions) == 0 {
		return false
	}
	return task.NextDueDate == nil
}

// ============================================================================
// isTaskCompleted FUNCTION TESTS
// ============================================================================

func TestIsTaskCompleted_NoCompletions_ReturnsFalse(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(10)),
		Completions: []models.TaskCompletion{},
	}

	assert.False(t, isTaskCompleted(task),
		"Task with no completions should not be considered completed")
}

func TestIsTaskCompleted_HasCompletions_WithNextDueDate_ReturnsFalse(t *testing.T) {
	// This is a RECURRING task - has completions but NextDueDate is set for next occurrence
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(10)),
		Completions: []models.TaskCompletion{
			{CompletedAt: daysAgo(5)},
		},
	}

	assert.False(t, isTaskCompleted(task),
		"Recurring task with NextDueDate should not be considered completed")
}

func TestIsTaskCompleted_HasCompletions_NilNextDueDate_ReturnsTrue(t *testing.T) {
	// This is a completed ONE-TIME task - NextDueDate is nil
	task := &models.Task{
		NextDueDate: nil,
		Completions: []models.TaskCompletion{
			{CompletedAt: daysAgo(5)},
		},
	}

	assert.True(t, isTaskCompleted(task),
		"One-time task with nil NextDueDate and completions should be considered completed")
}

func TestIsTaskCompleted_NoCompletions_NilNextDueDate_ReturnsFalse(t *testing.T) {
	// Task with no due date and no completions - not completed
	task := &models.Task{
		NextDueDate: nil,
		Completions: []models.TaskCompletion{},
	}

	assert.False(t, isTaskCompleted(task),
		"Task with nil NextDueDate but no completions should not be considered completed")
}

// ============================================================================
// GetButtonTypesForTask FUNCTION TESTS
// ============================================================================

func TestGetButtonTypesForTask_CancelledTask(t *testing.T) {
	task := &models.Task{
		IsCancelled: true,
		NextDueDate: ptr(daysAgo(5)), // Even if overdue, cancelled takes precedence
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.ElementsMatch(t, []string{"uncancel", "delete"}, buttons,
		"Cancelled task should only have uncancel and delete buttons")
}

func TestGetButtonTypesForTask_CompletedOneTimeTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: nil,
		Completions: []models.TaskCompletion{
			{CompletedAt: daysAgo(5)},
		},
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.Empty(t, buttons,
		"Completed one-time task should have no buttons (read-only)")
}

func TestGetButtonTypesForTask_InProgressTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(10)),
		InProgress:  true,
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.ElementsMatch(t, []string{"edit", "complete", "cancel"}, buttons,
		"In Progress task should not have mark_in_progress button")
}

func TestGetButtonTypesForTask_OverdueTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysAgo(5)),
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, buttons,
		"Overdue task should have all action buttons")
}

func TestGetButtonTypesForTask_DueSoonTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(15)), // Within 30-day threshold
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, buttons,
		"Due soon task should have all action buttons")
}

func TestGetButtonTypesForTask_UpcomingTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(45)), // Beyond 30-day threshold
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, buttons,
		"Upcoming task should have all action buttons")
}

func TestGetButtonTypesForTask_TaskWithNoDueDate(t *testing.T) {
	task := &models.Task{
		NextDueDate: nil,
		DueDate:     nil,
	}

	buttons := GetButtonTypesForTask(task, 30)

	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, buttons,
		"Task with no due date should have all action buttons")
}

// ============================================================================
// GetIOSCategoryForTask FUNCTION TESTS
// ============================================================================

func TestGetIOSCategoryForTask_CancelledTask(t *testing.T) {
	task := &models.Task{IsCancelled: true}

	category := GetIOSCategoryForTask(task)

	assert.Equal(t, IOSCategoryTaskCancelled, category)
}

func TestGetIOSCategoryForTask_CompletedTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: nil,
		Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
	}

	category := GetIOSCategoryForTask(task)

	assert.Equal(t, IOSCategoryTaskCompleted, category)
}

func TestGetIOSCategoryForTask_InProgressTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(10)),
		InProgress:  true,
	}

	category := GetIOSCategoryForTask(task)

	assert.Equal(t, IOSCategoryTaskInProgress, category)
}

func TestGetIOSCategoryForTask_ActionableTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(10)),
	}

	category := GetIOSCategoryForTask(task)

	assert.Equal(t, IOSCategoryTaskActionable, category)
}

// ============================================================================
// DetermineKanbanColumn FUNCTION TESTS
// ============================================================================

func TestDetermineKanbanColumn_CancelledTask(t *testing.T) {
	task := &models.Task{
		IsCancelled: true,
		NextDueDate: ptr(daysAgo(5)), // Even overdue
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "cancelled_tasks", column,
		"Cancelled takes precedence over everything")
}

func TestDetermineKanbanColumn_CompletedOneTimeTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: nil,
		Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "completed_tasks", column)
}

func TestDetermineKanbanColumn_InProgressTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysAgo(5)), // Even overdue
		InProgress:  true,
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "in_progress_tasks", column,
		"In Progress takes precedence over date-based columns")
}

func TestDetermineKanbanColumn_OverdueTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysAgo(5)),
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "overdue_tasks", column)
}

func TestDetermineKanbanColumn_DueSoonTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(15)),
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "due_soon_tasks", column)
}

func TestDetermineKanbanColumn_UpcomingTask(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(45)),
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "upcoming_tasks", column)
}

func TestDetermineKanbanColumn_NoDueDate(t *testing.T) {
	task := &models.Task{
		NextDueDate: nil,
		DueDate:     nil,
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "upcoming_tasks", column,
		"Tasks with no due date go to upcoming")
}

func TestDetermineKanbanColumn_FallbackToDueDate(t *testing.T) {
	// Task with DueDate set but NextDueDate nil (legacy data)
	task := &models.Task{
		NextDueDate: nil,
		DueDate:     ptr(daysAgo(5)),
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "overdue_tasks", column,
		"Should fallback to DueDate when NextDueDate is nil")
}

// ============================================================================
// ONE-TIME TASK LIFECYCLE TESTS (Integration)
// ============================================================================

func TestOneTimeTask_Lifecycle_Creation(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	_ = repositories.NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Once" frequency
	var onceFreq models.TaskFrequency
	db.Where("name = ?", "Once").First(&onceFreq)

	dueDate := daysFromNow(10)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "One-Time Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &onceFreq.ID,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Reload with frequency
	task, err = taskRepo.FindByID(task.ID)
	require.NoError(t, err)

	// Verify initial state
	assert.NotNil(t, task.NextDueDate, "NextDueDate should be set on creation")
	assert.Equal(t, task.DueDate.Unix(), task.NextDueDate.Unix(),
		"NextDueDate should equal DueDate on creation")

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "due_soon_tasks", column,
		"Task due in 10 days should be in due_soon")
}

func TestOneTimeTask_Lifecycle_Completion(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	_ = repositories.NewResidenceRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Once" frequency
	var onceFreq models.TaskFrequency
	db.Where("name = ?", "Once").First(&onceFreq)

	dueDate := daysFromNow(10)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "One-Time Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &onceFreq.ID,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Simulate completion (what task_service.CreateCompletion does)
	completedAt := time.Now().UTC()
	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   completedAt,
	}
	err = taskRepo.CreateCompletion(completion)
	require.NoError(t, err)

	// Reload task to get frequency
	task, err = taskRepo.FindByID(task.ID)
	require.NoError(t, err)

	// Simulate the next_due_date update (from task_service.CreateCompletion)
	// For one-time task: NextDueDate becomes nil
	if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
		task.NextDueDate = nil
	}
	err = taskRepo.Update(task)
	require.NoError(t, err)

	// Reload and verify
	task, err = taskRepo.FindByID(task.ID)
	require.NoError(t, err)

	assert.Nil(t, task.NextDueDate,
		"NextDueDate should be nil after one-time task completion")

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "completed_tasks", column,
		"Completed one-time task should be in completed_tasks")
}

// ============================================================================
// RECURRING TASK LIFECYCLE TESTS (Integration)
// ============================================================================

func TestRecurringTask_Lifecycle_Creation(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Weekly" frequency
	var weeklyFreq models.TaskFrequency
	db.Where("name = ?", "Weekly").First(&weeklyFreq)

	dueDate := daysFromNow(3) // Due in 3 days
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Weekly Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &weeklyFreq.ID,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Reload with frequency
	task, err = taskRepo.FindByID(task.ID)
	require.NoError(t, err)

	assert.NotNil(t, task.NextDueDate)
	assert.Equal(t, task.DueDate.Unix(), task.NextDueDate.Unix())

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "due_soon_tasks", column)
}

func TestRecurringTask_Lifecycle_FirstCompletion(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Weekly" frequency (7 days)
	var weeklyFreq models.TaskFrequency
	db.Where("name = ?", "Weekly").First(&weeklyFreq)
	require.NotNil(t, weeklyFreq.Days)

	dueDate := daysFromNow(3)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Weekly Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &weeklyFreq.ID,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Complete the task
	completedAt := time.Now().UTC()
	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   completedAt,
	}
	err = taskRepo.CreateCompletion(completion)
	require.NoError(t, err)

	// Reload task
	task, err = taskRepo.FindByID(task.ID)
	require.NoError(t, err)

	// Simulate the next_due_date update (from task_service.CreateCompletion)
	// For recurring task: NextDueDate = CompletedAt + FrequencyDays
	// Note: Frequency is no longer preloaded for performance, use the weeklyFreq we already have
	nextDue := completedAt.AddDate(0, 0, *weeklyFreq.Days)
	task.NextDueDate = &nextDue
	err = taskRepo.Update(task)
	require.NoError(t, err)

	// Reload and verify
	task, err = taskRepo.FindByID(task.ID)
	require.NoError(t, err)

	assert.NotNil(t, task.NextDueDate,
		"NextDueDate should NOT be nil for recurring task")

	// NextDueDate should be ~7 days from completion
	expectedNextDue := completedAt.AddDate(0, 0, 7)
	assert.WithinDuration(t, expectedNextDue, *task.NextDueDate, time.Hour,
		"NextDueDate should be 7 days after completion")

	// Task should NOT be in completed column
	column := responses.DetermineKanbanColumn(task, 30)
	assert.NotEqual(t, "completed_tasks", column,
		"Recurring task should NOT be in completed_tasks after completion")
	assert.Equal(t, "due_soon_tasks", column,
		"Recurring task should be in due_soon (next due in 7 days)")
}

func TestRecurringTask_Lifecycle_MultipleCompletions(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Weekly" frequency
	var weeklyFreq models.TaskFrequency
	db.Where("name = ?", "Weekly").First(&weeklyFreq)

	// Create task due 3 days ago (overdue)
	dueDate := daysAgo(3)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Weekly Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &weeklyFreq.ID,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Initially overdue
	task, _ = taskRepo.FindByID(task.ID)
	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "overdue_tasks", column, "Task should start as overdue")

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

	task, _ = taskRepo.FindByID(task.ID)
	// Note: Frequency is no longer preloaded for performance, use weeklyFreq we already have
	nextDue1 := completedAt1.AddDate(0, 0, *weeklyFreq.Days)
	task.NextDueDate = &nextDue1
	taskRepo.Update(task)

	// After first completion, should be due_soon
	task, _ = taskRepo.FindByID(task.ID)
	column = responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "due_soon_tasks", column,
		"After completion, task should move to due_soon")

	// Simulate waiting until overdue again
	overdueNextDue := daysAgo(2)
	task.NextDueDate = &overdueNextDue
	taskRepo.Update(task)

	task, _ = taskRepo.FindByID(task.ID)
	column = responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "overdue_tasks", column,
		"Task should be overdue again when NextDueDate passes")

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

	task, _ = taskRepo.FindByID(task.ID)
	// Note: Frequency is no longer preloaded for performance, use weeklyFreq we already have
	nextDue2 := completedAt2.AddDate(0, 0, *weeklyFreq.Days)
	task.NextDueDate = &nextDue2
	taskRepo.Update(task)

	// After second completion
	task, _ = taskRepo.FindByID(task.ID)
	column = responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "due_soon_tasks", column,
		"After second completion, task moves back to due_soon")

	// Verify we have 2 completions but task is NOT completed
	assert.Len(t, task.Completions, 2)
	assert.NotNil(t, task.NextDueDate,
		"NextDueDate should never be nil for recurring task")
}

func TestRecurringTask_NeverInCompletedColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Weekly" frequency
	var weeklyFreq models.TaskFrequency
	db.Where("name = ?", "Weekly").First(&weeklyFreq)

	dueDate := daysFromNow(3)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Weekly Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &weeklyFreq.ID,
	}
	db.Create(task)

	// Add 10 completions
	for i := 0; i < 10; i++ {
		db.Create(&models.TaskCompletion{
			TaskID:        task.ID,
			CompletedByID: user.ID,
			CompletedAt:   daysAgo(i * 7), // One per week
		})
	}

	// Update NextDueDate as if properly maintained
	task, _ = taskRepo.FindByID(task.ID)
	nextDue := daysFromNow(4)
	task.NextDueDate = &nextDue
	taskRepo.Update(task)

	task, _ = taskRepo.FindByID(task.ID)

	// Despite having 10 completions, should NOT be in completed column
	assert.Len(t, task.Completions, 10)
	assert.NotNil(t, task.NextDueDate)

	column := responses.DetermineKanbanColumn(task, 30)
	assert.NotEqual(t, "completed_tasks", column,
		"Recurring task should NEVER be in completed_tasks regardless of completion count")
}

// ============================================================================
// KANBAN BOARD INTEGRATION TESTS
// ============================================================================

func TestKanbanBoard_OneTimeTaskCompletion_MovesToCompleted(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Once" frequency
	var onceFreq models.TaskFrequency
	db.Where("name = ?", "Once").First(&onceFreq)

	dueDate := daysFromNow(10)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "One-Time Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &onceFreq.ID,
	}
	db.Create(task)

	// Before completion - should be in due_soon
	board, err := taskRepo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	dueSoonCount := 0
	completedCount := 0
	for _, col := range board.Columns {
		if col.Name == "due_soon_tasks" {
			dueSoonCount = col.Count
		}
		if col.Name == "completed_tasks" {
			completedCount = col.Count
		}
	}
	assert.Equal(t, 1, dueSoonCount, "Task should be in due_soon before completion")
	assert.Equal(t, 0, completedCount, "Completed should be empty before completion")

	// Complete the task
	db.Create(&models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
	})
	task.NextDueDate = nil // One-time task
	db.Save(task)

	// After completion - should be in completed
	board, err = taskRepo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	for _, col := range board.Columns {
		if col.Name == "due_soon_tasks" {
			dueSoonCount = col.Count
		}
		if col.Name == "completed_tasks" {
			completedCount = col.Count
		}
	}
	assert.Equal(t, 0, dueSoonCount, "due_soon should be empty after completion")
	assert.Equal(t, 1, completedCount, "Task should be in completed after completion")
}

func TestKanbanBoard_RecurringTaskCompletion_StaysActionable(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Weekly" frequency
	var weeklyFreq models.TaskFrequency
	db.Where("name = ?", "Weekly").First(&weeklyFreq)

	dueDate := daysAgo(1) // Overdue by 1 day
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Weekly Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &weeklyFreq.ID,
	}
	db.Create(task)

	// Before completion - should be overdue
	board, err := taskRepo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	var overdueCount, completedCount, dueSoonCount int
	for _, col := range board.Columns {
		switch col.Name {
		case "overdue_tasks":
			overdueCount = col.Count
		case "completed_tasks":
			completedCount = col.Count
		case "due_soon_tasks":
			dueSoonCount = col.Count
		}
	}
	assert.Equal(t, 1, overdueCount, "Task should be overdue before completion")
	assert.Equal(t, 0, completedCount, "Completed should be empty")

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

	// Update NextDueDate for recurring task
	nextDue := completedAt.AddDate(0, 0, 7)
	task.NextDueDate = &nextDue
	db.Save(task)

	// After completion - should be in due_soon, NOT completed
	board, err = taskRepo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	for _, col := range board.Columns {
		switch col.Name {
		case "overdue_tasks":
			overdueCount = col.Count
		case "completed_tasks":
			completedCount = col.Count
		case "due_soon_tasks":
			dueSoonCount = col.Count
		}
	}
	assert.Equal(t, 0, overdueCount, "Should no longer be overdue")
	assert.Equal(t, 0, completedCount, "Should NOT be in completed (recurring task)")
	assert.Equal(t, 1, dueSoonCount, "Should be in due_soon (next occurrence)")
}

// ============================================================================
// EDGE CASE TESTS
// ============================================================================

func TestEdgeCase_TaskDueExactlyAtThreshold(t *testing.T) {
	// Test with 29 days which is clearly within threshold (not at boundary)
	// to avoid timing-dependent boundary behavior
	task := &models.Task{
		NextDueDate: ptr(daysFromNow(29)), // 29 days from now, within 30-day threshold
	}

	column := responses.DetermineKanbanColumn(task, 30)

	// 29 days is clearly within the 30-day threshold, so should be "due_soon"
	assert.Equal(t, "due_soon_tasks", column,
		"Task due within threshold should be in due_soon")

	// Also test that 31 days (clearly beyond threshold) is upcoming
	taskBeyond := &models.Task{
		NextDueDate: ptr(daysFromNow(31)), // 31 days from now, beyond 30-day threshold
	}
	columnBeyond := responses.DetermineKanbanColumn(taskBeyond, 30)
	assert.Equal(t, "upcoming_tasks", columnBeyond,
		"Task due beyond threshold should be upcoming")
}

func TestEdgeCase_TaskDueJustBeforeThreshold(t *testing.T) {
	// Task due 29 days from today's start of day should be "due_soon"
	// (within the 30-day threshold)
	now := time.Now().UTC()
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	dueDate := startOfToday.AddDate(0, 0, 29) // 29 days from start of today

	task := &models.Task{
		NextDueDate: &dueDate,
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "due_soon_tasks", column,
		"Task due 29 days from today should be in due_soon (within 30-day threshold)")
}

func TestEdgeCase_TaskDueInPast_ButHasCompletionAfter(t *testing.T) {
	// One-time task that was overdue but got completed late
	// NextDueDate should be nil after completion
	task := &models.Task{
		DueDate:     ptr(daysAgo(5)),
		NextDueDate: nil, // Set to nil after completion
		Completions: []models.TaskCompletion{
			{CompletedAt: daysAgo(1)}, // Completed 4 days late
		},
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "completed_tasks", column,
		"Late-completed one-time task should be in completed")
}

func TestEdgeCase_CancelledAndOverdue(t *testing.T) {
	task := &models.Task{
		IsCancelled: true,
		NextDueDate: ptr(daysAgo(10)),
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "cancelled_tasks", column,
		"Cancelled takes precedence over overdue")
}

func TestEdgeCase_InProgressAndOverdue(t *testing.T) {
	task := &models.Task{
		NextDueDate: ptr(daysAgo(5)),
		InProgress:  true,
	}

	column := responses.DetermineKanbanColumn(task, 30)

	assert.Equal(t, "in_progress_tasks", column,
		"In Progress takes precedence over overdue")
}

func TestEdgeCase_MonthlyRecurringTask(t *testing.T) {
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")

	// Get "Monthly" frequency (30 days)
	var monthlyFreq models.TaskFrequency
	db.Where("name = ?", "Monthly").First(&monthlyFreq)
	require.NotNil(t, monthlyFreq.Days)
	require.Equal(t, 30, *monthlyFreq.Days)

	dueDate := daysAgo(2) // Overdue
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Monthly Task",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		FrequencyID: &monthlyFreq.ID,
	}
	db.Create(task)

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

	// Update NextDueDate - set to 29 days from today (within 30-day threshold)
	task, _ = taskRepo.FindByID(task.ID)
	now := time.Now().UTC()
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	nextDue := startOfToday.AddDate(0, 0, 29) // 29 days from start of today (within threshold)
	task.NextDueDate = &nextDue
	db.Save(task)

	task, _ = taskRepo.FindByID(task.ID)

	// With day-based comparisons:
	// - Threshold = start of today + 30 days
	// - A task due on day 29 is Before(threshold), so it's "due_soon"
	// - A task due on day 30+ is NOT Before(threshold), so it's "upcoming"
	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "due_soon_tasks", column,
		"Monthly task within 30-day threshold should be due_soon")
}

func TestEdgeCase_ZeroDayFrequency_TreatedAsOneTime(t *testing.T) {
	// Some systems might have frequency.Days = 0 instead of nil for one-time
	zeroDays := 0
	task := &models.Task{
		NextDueDate: nil,
		Frequency:   &models.TaskFrequency{Days: &zeroDays},
		Completions: []models.TaskCompletion{
			{CompletedAt: daysAgo(1)},
		},
	}

	// Should be treated as completed (one-time task logic)
	assert.True(t, isTaskCompleted(task),
		"Task with 0-day frequency and nil NextDueDate should be completed")

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "completed_tasks", column)
}

// ============================================================================
// BUTTON TYPES CONSISTENCY TESTS
// ============================================================================

func TestButtonTypes_ConsistencyWithKanbanColumn(t *testing.T) {
	tests := []struct {
		name           string
		task           *models.Task
		expectedColumn string
		expectedButtons []string
	}{
		{
			name: "Cancelled task",
			task: &models.Task{
				IsCancelled: true,
			},
			expectedColumn:  "cancelled_tasks",
			expectedButtons: []string{"uncancel", "delete"},
		},
		{
			name: "Completed one-time task",
			task: &models.Task{
				NextDueDate: nil,
				Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
			},
			expectedColumn:  "completed_tasks",
			expectedButtons: []string{}, // Read-only
		},
		{
			name: "In Progress task",
			task: &models.Task{
				NextDueDate: ptr(daysFromNow(10)),
				InProgress:  true,
			},
			expectedColumn:  "in_progress_tasks",
			expectedButtons: []string{"edit", "complete", "cancel"},
		},
		{
			name: "Overdue task",
			task: &models.Task{
				NextDueDate: ptr(daysAgo(5)),
			},
			expectedColumn:  "overdue_tasks",
			expectedButtons: []string{"edit", "complete", "cancel", "mark_in_progress"},
		},
		{
			name: "Due soon task",
			task: &models.Task{
				NextDueDate: ptr(daysFromNow(15)),
			},
			expectedColumn:  "due_soon_tasks",
			expectedButtons: []string{"edit", "complete", "cancel", "mark_in_progress"},
		},
		{
			name: "Upcoming task",
			task: &models.Task{
				NextDueDate: ptr(daysFromNow(45)),
			},
			expectedColumn:  "upcoming_tasks",
			expectedButtons: []string{"edit", "complete", "cancel", "mark_in_progress"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			column := responses.DetermineKanbanColumn(tt.task, 30)
			buttons := GetButtonTypesForTask(tt.task, 30)

			assert.Equal(t, tt.expectedColumn, column, "Column mismatch")
			assert.ElementsMatch(t, tt.expectedButtons, buttons, "Button types mismatch")
		})
	}
}

// ============================================================================
// PRIORITY ORDER TESTS
// ============================================================================

func TestPriorityOrder_CancelledBeatsEverything(t *testing.T) {
	// Task that is cancelled, overdue, in progress, with completions
	task := &models.Task{
		IsCancelled: true,
		NextDueDate: ptr(daysAgo(10)),
		InProgress:  true,
		Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
	}

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "cancelled_tasks", column)
}

func TestPriorityOrder_CompletedBeatsInProgress(t *testing.T) {
	// One-time task with In Progress status but completed
	task := &models.Task{
		NextDueDate: nil,
		InProgress:  true,
		Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
	}

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "completed_tasks", column)
}

func TestPriorityOrder_InProgressBeatsDateBased(t *testing.T) {
	// Overdue task that's in progress
	task := &models.Task{
		NextDueDate: ptr(daysAgo(10)),
		InProgress:  true,
	}

	column := responses.DetermineKanbanColumn(task, 30)
	assert.Equal(t, "in_progress_tasks", column)
}
