package repositories

import (
	"testing"
	"time"

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

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

func TestTaskRepository_Create(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Fix leaky faucet",
		Description: "Kitchen faucet is dripping",
	}

	err := repo.Create(task)
	require.NoError(t, err)
	assert.NotZero(t, task.ID)
}

func TestTaskRepository_FindByID(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

	found, err := repo.FindByID(task.ID)
	require.NoError(t, err)
	assert.Equal(t, task.ID, found.ID)
	assert.Equal(t, "Test Task", found.Title)
}

func TestTaskRepository_FindByID_NotFound(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	_, err := repo.FindByID(9999)
	assert.Error(t, err)
}

func TestTaskRepository_FindByResidence(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
	residence2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")

	testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task 1")
	testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task 2")
	testutil.CreateTestTask(t, db, residence2.ID, user.ID, "Task 3")

	tasks, err := repo.FindByResidence(residence1.ID)
	require.NoError(t, err)
	assert.Len(t, tasks, 2)
}

func TestTaskRepository_Update(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Original Title")

	task.Title = "Updated Title"
	task.Description = "Updated description"
	err := repo.Update(task)
	require.NoError(t, err)

	found, err := repo.FindByID(task.ID)
	require.NoError(t, err)
	assert.Equal(t, "Updated Title", found.Title)
	assert.Equal(t, "Updated description", found.Description)
}

func TestTaskRepository_Delete(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

	err := repo.Delete(task.ID)
	require.NoError(t, err)

	_, err = repo.FindByID(task.ID)
	assert.Error(t, err) // Should not be found
}

func TestTaskRepository_Cancel(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

	assert.False(t, task.IsCancelled)

	err := repo.Cancel(task.ID, task.Version)
	require.NoError(t, err)

	found, err := repo.FindByID(task.ID)
	require.NoError(t, err)
	assert.True(t, found.IsCancelled)
}

func TestTaskRepository_Uncancel(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

	repo.Cancel(task.ID, task.Version)
	err := repo.Uncancel(task.ID, task.Version+1) // version incremented by Cancel
	require.NoError(t, err)

	found, err := repo.FindByID(task.ID)
	require.NoError(t, err)
	assert.False(t, found.IsCancelled)
}

func TestTaskRepository_Archive(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

	err := repo.Archive(task.ID, task.Version)
	require.NoError(t, err)

	found, err := repo.FindByID(task.ID)
	require.NoError(t, err)
	assert.True(t, found.IsArchived)
}

func TestTaskRepository_Unarchive(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

	repo.Archive(task.ID, task.Version)
	err := repo.Unarchive(task.ID, task.Version+1) // version incremented by Archive
	require.NoError(t, err)

	found, err := repo.FindByID(task.ID)
	require.NoError(t, err)
	assert.False(t, found.IsArchived)
}

func TestTaskRepository_CreateCompletion(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

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

	err := repo.CreateCompletion(completion)
	require.NoError(t, err)
	assert.NotZero(t, completion.ID)
}

func TestTaskRepository_FindCompletionByID(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

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

	found, err := repo.FindCompletionByID(completion.ID)
	require.NoError(t, err)
	assert.Equal(t, completion.ID, found.ID)
	assert.Equal(t, "Test notes", found.Notes)
}

func TestTaskRepository_FindCompletionsByTask(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

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

	completions, err := repo.FindCompletionsByTask(task.ID)
	require.NoError(t, err)
	assert.Len(t, completions, 3)
}

func TestTaskRepository_DeleteCompletion(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(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, "Test Task")

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

	err := repo.DeleteCompletion(completion.ID)
	require.NoError(t, err)

	_, err = repo.FindCompletionByID(completion.ID)
	assert.Error(t, err)
}

func TestTaskRepository_GetAllCategories(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

	categories, err := repo.GetAllCategories()
	require.NoError(t, err)
	assert.Greater(t, len(categories), 0)
}

func TestTaskRepository_GetAllPriorities(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

	priorities, err := repo.GetAllPriorities()
	require.NoError(t, err)
	assert.Greater(t, len(priorities), 0)
}


func TestTaskRepository_GetAllFrequencies(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

	frequencies, err := repo.GetAllFrequencies()
	require.NoError(t, err)
	assert.Greater(t, len(frequencies), 0)
}

func TestTaskRepository_CountByResidence(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	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")

	count, err := repo.CountByResidence(residence.ID)
	require.NoError(t, err)
	assert.Equal(t, int64(3), count)
}

// === Kanban Board Categorization Tests ===

func TestKanbanBoard_CancelledTasksHiddenFromKanbanBoard(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a cancelled task
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Cancelled Task")
	repo.Cancel(task.ID, task.Version)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	assert.Len(t, board.Columns, 5, "board should have 5 visible columns")
	for _, col := range board.Columns {
		assert.NotEqual(t, "cancelled_tasks", col.Name, "cancelled column must be hidden")
	}

	totalTasks := 0
	for _, col := range board.Columns {
		totalTasks += col.Count
	}
	assert.Equal(t, 0, totalTasks, "cancelled task should be hidden from board")
}

func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task with a completion
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Completed Task")
	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
		Notes:         "Done!",
	}
	err := repo.CreateCompletion(completion)
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find completed column
	var completedColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "completed_tasks" {
			completedColumn = &board.Columns[i]
			break
		}
	}

	require.NotNil(t, completedColumn, "completed_tasks column should exist")
	assert.Equal(t, 1, completedColumn.Count)
	assert.Len(t, completedColumn.Tasks, 1)
	assert.Equal(t, "Completed Task", completedColumn.Tasks[0].Title)

	// Verify button types for completed column (read-only, no buttons)
	assert.ElementsMatch(t, []string{}, completedColumn.ButtonTypes)
}

func TestKanbanBoard_InProgressTasksGoToInProgressColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task with InProgress = true
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "In Progress Task",
		InProgress:  true,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find in_progress column
	var inProgressColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "in_progress_tasks" {
			inProgressColumn = &board.Columns[i]
			break
		}
	}

	require.NotNil(t, inProgressColumn, "in_progress_tasks column should exist")
	assert.Equal(t, 1, inProgressColumn.Count)
	assert.Len(t, inProgressColumn.Tasks, 1)
	assert.Equal(t, "In Progress Task", inProgressColumn.Tasks[0].Title)

	// Verify button types for in-progress column (no mark_in_progress since already in progress)
	assert.ElementsMatch(t, []string{"edit", "complete", "cancel"}, inProgressColumn.ButtonTypes)
}

func TestKanbanBoard_OverdueTasksGoToOverdueColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task with due date in the past
	pastDue := time.Now().UTC().AddDate(0, 0, -5) // 5 days ago
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue Task",
		DueDate:     &pastDue,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find overdue column
	var overdueColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "overdue_tasks" {
			overdueColumn = &board.Columns[i]
			break
		}
	}

	require.NotNil(t, overdueColumn, "overdue_tasks column should exist")
	assert.Equal(t, 1, overdueColumn.Count)
	assert.Len(t, overdueColumn.Tasks, 1)
	assert.Equal(t, "Overdue Task", overdueColumn.Tasks[0].Title)

	// Verify button types for overdue column
	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, overdueColumn.ButtonTypes)
}

func TestKanbanBoard_DueSoonTasksGoToDueSoonColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task with due date within threshold (default 30 days)
	dueSoon := time.Now().UTC().AddDate(0, 0, 15) // 15 days from now
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Due Soon Task",
		DueDate:     &dueSoon,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find due_soon column
	var dueSoonColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "due_soon_tasks" {
			dueSoonColumn = &board.Columns[i]
			break
		}
	}

	require.NotNil(t, dueSoonColumn, "due_soon_tasks column should exist")
	assert.Equal(t, 1, dueSoonColumn.Count)
	assert.Len(t, dueSoonColumn.Tasks, 1)
	assert.Equal(t, "Due Soon Task", dueSoonColumn.Tasks[0].Title)

	// Verify button types for due_soon column
	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, dueSoonColumn.ButtonTypes)
}

func TestKanbanBoard_UpcomingTasksGoToUpcomingColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task with due date beyond threshold (default 30 days)
	upcoming := time.Now().UTC().AddDate(0, 0, 45) // 45 days from now
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Upcoming Task",
		DueDate:     &upcoming,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find upcoming column
	var upcomingColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "upcoming_tasks" {
			upcomingColumn = &board.Columns[i]
			break
		}
	}

	require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
	assert.Equal(t, 1, upcomingColumn.Count)
	assert.Len(t, upcomingColumn.Tasks, 1)
	assert.Equal(t, "Upcoming Task", upcomingColumn.Tasks[0].Title)

	// Verify button types for upcoming column
	assert.ElementsMatch(t, []string{"edit", "complete", "cancel", "mark_in_progress"}, upcomingColumn.ButtonTypes)
}

func TestKanbanBoard_TasksWithNoDueDateGoToUpcomingColumn(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task with no due date
	task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "No Due Date Task")

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find upcoming column
	var upcomingColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "upcoming_tasks" {
			upcomingColumn = &board.Columns[i]
			break
		}
	}

	require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
	assert.Equal(t, 1, upcomingColumn.Count)
	assert.Equal(t, task.ID, upcomingColumn.Tasks[0].ID)
}

func TestKanbanBoard_ArchivedTasksHiddenFromKanbanBoard(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a regular task and an archived task
	testutil.CreateTestTask(t, db, residence.ID, user.ID, "Regular Task")
	archivedTask := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Archived Task")
	repo.Archive(archivedTask.ID, archivedTask.Version)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find the upcoming column and verify archived task is hidden
	var upcomingColumn *models.KanbanColumn
	foundCancelledColumn := false
	for i := range board.Columns {
		if board.Columns[i].Name == "cancelled_tasks" {
			foundCancelledColumn = true
		}
		if board.Columns[i].Name == "upcoming_tasks" {
			upcomingColumn = &board.Columns[i]
		}
	}

	require.NotNil(t, upcomingColumn, "upcoming_tasks column should exist")
	assert.False(t, foundCancelledColumn, "cancelled column must be hidden")

	// Archived task should be hidden from board

	// Regular task should be in upcoming (no due date)
	assert.Equal(t, 1, upcomingColumn.Count, "regular task should be in upcoming column")
	assert.Equal(t, "Regular Task", upcomingColumn.Tasks[0].Title)

	// Total tasks should be 1 (archived task is hidden)
	totalTasks := 0
	for _, col := range board.Columns {
		totalTasks += col.Count
	}
	assert.Equal(t, 1, totalTasks, "archived task should be hidden from board")
}

func TestKanbanBoard_ArchivedOverdueTask_HiddenFromKanbanBoard(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create an archived task that would be overdue if it weren't archived
	archivedOverdueTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Archived Overdue Task",
		DueDate:     &pastDue,
		IsArchived:  true,
	}
	db.Create(archivedOverdueTask)

	board, err := repo.GetKanbanData(residence.ID, 30, now)
	require.NoError(t, err)

	// Find columns
	var overdueColumn *models.KanbanColumn
	foundCancelledColumn := false
	for i := range board.Columns {
		if board.Columns[i].Name == "cancelled_tasks" {
			foundCancelledColumn = true
		}
		if board.Columns[i].Name == "overdue_tasks" {
			overdueColumn = &board.Columns[i]
		}
	}

	require.NotNil(t, overdueColumn)
	assert.False(t, foundCancelledColumn, "cancelled column must be hidden")

	// Archived task should be hidden and NOT overdue
	assert.Equal(t, 0, overdueColumn.Count, "archived task should NOT be in overdue column")
	totalTasks := 0
	for _, col := range board.Columns {
		totalTasks += col.Count
	}
	assert.Equal(t, 0, totalTasks, "archived task should be hidden from board")
}

func TestKanbanBoard_CategoryPriority_CancelledTasksAreHidden(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task that is BOTH overdue AND cancelled
	// Cancelled should take precedence
	pastDue := time.Now().UTC().AddDate(0, 0, -5)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue and Cancelled Task",
		DueDate:     &pastDue,
		IsCancelled: true,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	var overdueColumn *models.KanbanColumn
	foundCancelledColumn := false
	for i := range board.Columns {
		if board.Columns[i].Name == "cancelled_tasks" {
			foundCancelledColumn = true
		}
		if board.Columns[i].Name == "overdue_tasks" {
			overdueColumn = &board.Columns[i]
		}
	}

	// Cancelled task should be hidden and not appear as overdue
	assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
	require.NotNil(t, overdueColumn, "overdue column should exist")
	assert.Equal(t, 0, overdueColumn.Count, "Task should NOT be in overdue column")
	totalTasks := 0
	for _, col := range board.Columns {
		totalTasks += col.Count
	}
	assert.Equal(t, 0, totalTasks, "cancelled task should be hidden from board")
}

func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task that has InProgress = true AND a completion
	// Completed should take precedence
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "In Progress with Completion",
		InProgress:  true,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Add a completion
	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
	}
	err = repo.CreateCompletion(completion)
	require.NoError(t, err)

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Find columns
	var completedColumn, inProgressColumn *models.KanbanColumn
	for i := range board.Columns {
		if board.Columns[i].Name == "completed_tasks" {
			completedColumn = &board.Columns[i]
		}
		if board.Columns[i].Name == "in_progress_tasks" {
			inProgressColumn = &board.Columns[i]
		}
	}

	// Task should be in completed, not in_progress
	assert.Equal(t, 1, completedColumn.Count, "Task should be in completed column")
	assert.Equal(t, 0, inProgressColumn.Count, "Task should NOT be in in_progress column")
}

func TestKanbanBoard_DaysThresholdAffectsCategorization(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	// Create a task due in 10 days
	dueIn10Days := time.Now().UTC().AddDate(0, 0, 10)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Task Due in 10 Days",
		DueDate:     &dueIn10Days,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// With 30-day threshold, should be in "due_soon"
	board30, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	var dueSoon30, upcoming30 *models.KanbanColumn
	for i := range board30.Columns {
		if board30.Columns[i].Name == "due_soon_tasks" {
			dueSoon30 = &board30.Columns[i]
		}
		if board30.Columns[i].Name == "upcoming_tasks" {
			upcoming30 = &board30.Columns[i]
		}
	}
	assert.Equal(t, 1, dueSoon30.Count, "With 30-day threshold, task should be in due_soon")
	assert.Equal(t, 0, upcoming30.Count, "With 30-day threshold, task should NOT be in upcoming")

	// With 5-day threshold, should be in "upcoming"
	board5, err := repo.GetKanbanData(residence.ID, 5, time.Now().UTC())
	require.NoError(t, err)

	var dueSoon5, upcoming5 *models.KanbanColumn
	for i := range board5.Columns {
		if board5.Columns[i].Name == "due_soon_tasks" {
			dueSoon5 = &board5.Columns[i]
		}
		if board5.Columns[i].Name == "upcoming_tasks" {
			upcoming5 = &board5.Columns[i]
		}
	}
	assert.Equal(t, 0, dueSoon5.Count, "With 5-day threshold, task should NOT be in due_soon")
	assert.Equal(t, 1, upcoming5.Count, "With 5-day threshold, task should be in upcoming")
}

func TestKanbanBoard_ColumnMetadata(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
	require.NoError(t, err)

	// Verify all 5 visible columns exist with correct metadata
	expectedColumns := []struct {
		name        string
		displayName string
		color       string
		buttonTypes []string
		iosIcon     string
		androidIcon string
	}{
		{"overdue_tasks", "Overdue", "#FF3B30", []string{"edit", "complete", "cancel", "mark_in_progress"}, "exclamationmark.triangle", "Warning"},
		{"in_progress_tasks", "In Progress", "#5856D6", []string{"edit", "complete", "cancel"}, "hammer", "Build"},
		{"due_soon_tasks", "Due Soon", "#FF9500", []string{"edit", "complete", "cancel", "mark_in_progress"}, "clock", "Schedule"},
		{"upcoming_tasks", "Upcoming", "#007AFF", []string{"edit", "complete", "cancel", "mark_in_progress"}, "calendar", "Event"},
		{"completed_tasks", "Completed", "#34C759", []string{}, "checkmark.circle", "CheckCircle"}, // Completed tasks are read-only (no buttons)
	}

	assert.Len(t, board.Columns, 5, "Board should have 5 visible columns")

	for i, expected := range expectedColumns {
		col := board.Columns[i]
		assert.Equal(t, expected.name, col.Name, "Column %d name mismatch", i)
		assert.Equal(t, expected.displayName, col.DisplayName, "Column %d display name mismatch", i)
		assert.Equal(t, expected.color, col.Color, "Column %d color mismatch", i)
		assert.ElementsMatch(t, expected.buttonTypes, col.ButtonTypes, "Column %d button types mismatch", i)
		assert.Equal(t, expected.iosIcon, col.Icons["ios"], "Column %d iOS icon mismatch", i)
		assert.Equal(t, expected.androidIcon, col.Icons["android"], "Column %d Android icon mismatch", i)
	}

	// Verify days threshold is returned
	assert.Equal(t, 30, board.DaysThreshold)
}

func TestKanbanBoard_MultipleResidences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
	residence2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")

	// Create tasks in both residences
	testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Task in House 1")
	testutil.CreateTestTask(t, db, residence2.ID, user.ID, "Task in House 2")

	// Create a cancelled task in house 1
	cancelledTask := testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Cancelled in House 1")
	repo.Cancel(cancelledTask.ID, cancelledTask.Version)

	board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30, time.Now().UTC())
	require.NoError(t, err)

	// Count total tasks (cancelled task is intentionally hidden from board)
	totalTasks := 0
	for _, col := range board.Columns {
		totalTasks += col.Count
	}
	assert.Equal(t, 2, totalTasks, "Should have 2 visible tasks total across both residences")

	// Find upcoming column and ensure cancelled column is hidden
	var upcomingColumn *models.KanbanColumn
	foundCancelledColumn := false
	for i := range board.Columns {
		if board.Columns[i].Name == "upcoming_tasks" {
			upcomingColumn = &board.Columns[i]
		}
		if board.Columns[i].Name == "cancelled_tasks" {
			foundCancelledColumn = true
		}
	}

	assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
	require.NotNil(t, upcomingColumn, "upcoming column should exist")
	assert.Equal(t, 2, upcomingColumn.Count, "Should have 2 upcoming tasks")
}

// === Single-Purpose Function Tests ===

func TestGetOverdueTasks_Basic(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create an overdue task
	overdueTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue Task",
		DueDate:     &pastDue,
	}
	db.Create(overdueTask)

	// Create a task due in the future (not overdue)
	futureDue := now.AddDate(0, 0, 10)
	futureTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Future Task",
		DueDate:     &futureDue,
	}
	db.Create(futureTask)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Overdue Task", tasks[0].Title)
}

func TestGetOverdueTasks_ExcludesInProgressByDefault(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create an overdue task that is in-progress
	inProgressTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue In Progress",
		DueDate:     &pastDue,
		InProgress:  true,
	}
	db.Create(inProgressTask)

	// Create a regular overdue task
	regularTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue Not In Progress",
		DueDate:     &pastDue,
	}
	db.Create(regularTask)

	// Default: excludes in-progress
	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Overdue Not In Progress", tasks[0].Title)
}

func TestGetOverdueTasks_IncludesInProgressWhenRequested(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create an overdue task that is in-progress
	inProgressTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue In Progress",
		DueDate:     &pastDue,
		InProgress:  true,
	}
	db.Create(inProgressTask)

	// Create a regular overdue task
	regularTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue Not In Progress",
		DueDate:     &pastDue,
	}
	db.Create(regularTask)

	// With IncludeInProgress=true: includes both
	opts := TaskFilterOptions{
		ResidenceID:       residence.ID,
		IncludeInProgress: true,
	}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 2)
}

func TestGetOverdueTasks_TaskDueToday_NotOverdue(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Use start of today for "now"
	now := time.Now().UTC()
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)

	// Create a task due TODAY
	taskDueToday := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Task Due Today",
		DueDate:     &startOfToday,
	}
	db.Create(taskDueToday)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	// Task due TODAY is NOT overdue - it becomes overdue tomorrow
	assert.Len(t, tasks, 0, "Task due today should NOT be overdue")
}

func TestGetDueSoonTasks_Basic(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	dueSoon := now.AddDate(0, 0, 5)
	dueLater := now.AddDate(0, 0, 45)

	// Create a task due soon (within 30 days)
	dueSoonTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Due Soon Task",
		DueDate:     &dueSoon,
	}
	db.Create(dueSoonTask)

	// Create a task due later (beyond 30 days)
	dueLaterTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Due Later Task",
		DueDate:     &dueLater,
	}
	db.Create(dueLaterTask)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetDueSoonTasks(now, 30, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Due Soon Task", tasks[0].Title)
}

func TestGetDueSoonTasks_TaskDueToday_IncludedInDueSoon(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)

	// Create a task due TODAY
	taskDueToday := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Task Due Today",
		DueDate:     &startOfToday,
	}
	db.Create(taskDueToday)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetDueSoonTasks(now, 30, opts)
	require.NoError(t, err)
	// Task due TODAY should be in "due soon" (not overdue)
	assert.Len(t, tasks, 1, "Task due today should be in due_soon")
	assert.Equal(t, "Task Due Today", tasks[0].Title)
}

func TestGetInProgressTasks_Basic(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Create an in-progress task
	inProgressTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "In Progress Task",
		InProgress:  true,
	}
	db.Create(inProgressTask)

	// Create a regular task (not in-progress)
	regularTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Regular Task",
	}
	db.Create(regularTask)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetInProgressTasks(opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "In Progress Task", tasks[0].Title)
}

func TestGetCompletedTasks_Basic(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Create a task with a completion (completed)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Completed Task",
	}
	db.Create(task)
	db.Create(&models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
	})

	// Create a task without completion (not completed)
	notCompletedTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Not Completed Task",
	}
	db.Create(notCompletedTask)

	opts := TaskFilterOptions{ResidenceID: residence.ID, PreloadCompletions: true}
	tasks, err := repo.GetCompletedTasks(opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Completed Task", tasks[0].Title)
}

func TestGetCancelledTasks_Basic(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Create a cancelled task
	cancelledTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Cancelled Task",
		IsCancelled: true,
	}
	db.Create(cancelledTask)

	// Create a regular task
	regularTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Regular Task",
	}
	db.Create(regularTask)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetCancelledTasks(opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Cancelled Task", tasks[0].Title)
}

func TestGetUpcomingTasks_Basic(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	upcomingDue := now.AddDate(0, 0, 45) // Beyond 30-day threshold
	dueSoonDue := now.AddDate(0, 0, 10)  // Within 30-day threshold

	// Create an upcoming task
	upcomingTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Upcoming Task",
		DueDate:     &upcomingDue,
	}
	db.Create(upcomingTask)

	// Create a due soon task
	dueSoonTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Due Soon Task",
		DueDate:     &dueSoonDue,
	}
	db.Create(dueSoonTask)

	// Create a task with no due date (should be upcoming)
	noDueDateTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "No Due Date Task",
	}
	db.Create(noDueDateTask)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetUpcomingTasks(now, 30, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 2) // Upcoming + No due date

	titles := []string{tasks[0].Title, tasks[1].Title}
	assert.Contains(t, titles, "Upcoming Task")
	assert.Contains(t, titles, "No Due Date Task")
}

// === TaskFilterOptions Tests ===

func TestTaskFilterOptions_SingleResidence(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
	residence2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create overdue tasks in both residences
	db.Create(&models.Task{ResidenceID: residence1.ID, CreatedByID: user.ID, Title: "Overdue in House 1", DueDate: &pastDue})
	db.Create(&models.Task{ResidenceID: residence2.ID, CreatedByID: user.ID, Title: "Overdue in House 2", DueDate: &pastDue})

	opts := TaskFilterOptions{ResidenceID: residence1.ID}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Overdue in House 1", tasks[0].Title)
}

func TestTaskFilterOptions_MultipleResidences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
	residence1 := testutil.CreateTestResidence(t, db, user.ID, "House 1")
	residence2 := testutil.CreateTestResidence(t, db, user.ID, "House 2")
	residence3 := testutil.CreateTestResidence(t, db, user.ID, "House 3")

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create overdue tasks in all three residences
	db.Create(&models.Task{ResidenceID: residence1.ID, CreatedByID: user.ID, Title: "Overdue in House 1", DueDate: &pastDue})
	db.Create(&models.Task{ResidenceID: residence2.ID, CreatedByID: user.ID, Title: "Overdue in House 2", DueDate: &pastDue})
	db.Create(&models.Task{ResidenceID: residence3.ID, CreatedByID: user.ID, Title: "Overdue in House 3", DueDate: &pastDue})

	// Filter for residences 1 and 2 only
	opts := TaskFilterOptions{ResidenceIDs: []uint{residence1.ID, residence2.ID}}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 2)
}

func TestTaskFilterOptions_UserIDs(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	user1 := testutil.CreateTestUser(t, db, "owner1", "owner1@test.com", "password")
	user2 := testutil.CreateTestUser(t, db, "owner2", "owner2@test.com", "password")
	residence1 := testutil.CreateTestResidence(t, db, user1.ID, "User1 House")
	residence2 := testutil.CreateTestResidence(t, db, user2.ID, "User2 House")

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create overdue tasks in both users' residences
	db.Create(&models.Task{ResidenceID: residence1.ID, CreatedByID: user1.ID, Title: "User1 Overdue", DueDate: &pastDue})
	db.Create(&models.Task{ResidenceID: residence2.ID, CreatedByID: user2.ID, Title: "User2 Overdue", DueDate: &pastDue})

	// Filter by user1 only
	opts := TaskFilterOptions{UserIDs: []uint{user1.ID}, IncludeInProgress: true}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "User1 Overdue", tasks[0].Title)
}

// TestTaskFilterOptions_UserIDs_ExcludesInactiveResidences verifies that tasks
// from inactive residences are excluded when filtering by user IDs.
// This is a regression test for the bug where users received notifications
// for tasks on residences they had deactivated.
func TestTaskFilterOptions_UserIDs_ExcludesInactiveResidences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")

	// Create an active residence
	activeResidence := testutil.CreateTestResidence(t, db, user.ID, "Active House")

	// Create an inactive residence (manually, since testutil always creates active)
	// Note: Must update after create because GORM's default:true overrides struct value
	inactiveResidence := &models.Residence{
		OwnerID:       user.ID,
		Name:          "Inactive House",
		StreetAddress: "456 Inactive St",
		City:          "Test City",
		StateProvince: "TS",
		PostalCode:    "12345",
		Country:       "USA",
		IsPrimary:     false,
	}
	err := db.Create(inactiveResidence).Error
	require.NoError(t, err)
	// Set to inactive after creation to bypass GORM default
	db.Model(inactiveResidence).Update("is_active", false)

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create overdue tasks in both residences
	db.Create(&models.Task{
		ResidenceID: activeResidence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue in Active Residence",
		DueDate:     &pastDue,
	})
	db.Create(&models.Task{
		ResidenceID: inactiveResidence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue in Inactive Residence",
		DueDate:     &pastDue,
	})

	// Filter by user ID (used by notification system)
	opts := TaskFilterOptions{UserIDs: []uint{user.ID}, IncludeInProgress: true}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)

	// Should only return task from active residence
	assert.Len(t, tasks, 1, "Should only return tasks from active residences")
	assert.Equal(t, "Overdue in Active Residence", tasks[0].Title)
}

// TestTaskFilterOptions_UserIDs_DueSoon_ExcludesInactiveResidences verifies that
// due soon tasks from inactive residences are excluded when filtering by user IDs.
func TestTaskFilterOptions_UserIDs_DueSoon_ExcludesInactiveResidences(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

	user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")

	// Create an active residence
	activeResidence := testutil.CreateTestResidence(t, db, user.ID, "Active House")

	// Create an inactive residence
	// Note: Must update after create because GORM's default:true overrides struct value
	inactiveResidence := &models.Residence{
		OwnerID:       user.ID,
		Name:          "Inactive House",
		StreetAddress: "456 Inactive St",
		City:          "Test City",
		StateProvince: "TS",
		PostalCode:    "12345",
		Country:       "USA",
		IsPrimary:     false,
	}
	err := db.Create(inactiveResidence).Error
	require.NoError(t, err)
	// Set to inactive after creation to bypass GORM default
	db.Model(inactiveResidence).Update("is_active", false)

	now := time.Now().UTC()
	dueSoon := now.AddDate(0, 0, 5) // 5 days from now

	// Create due soon tasks in both residences
	db.Create(&models.Task{
		ResidenceID: activeResidence.ID,
		CreatedByID: user.ID,
		Title:       "Due Soon in Active Residence",
		DueDate:     &dueSoon,
	})
	db.Create(&models.Task{
		ResidenceID: inactiveResidence.ID,
		CreatedByID: user.ID,
		Title:       "Due Soon in Inactive Residence",
		DueDate:     &dueSoon,
	})

	// Filter by user ID (used by notification system)
	opts := TaskFilterOptions{UserIDs: []uint{user.ID}, IncludeInProgress: true}
	tasks, err := repo.GetDueSoonTasks(now, 30, opts)
	require.NoError(t, err)

	// Should only return task from active residence
	assert.Len(t, tasks, 1, "Should only return tasks from active residences")
	assert.Equal(t, "Due Soon in Active Residence", tasks[0].Title)
}

func TestTaskFilterOptions_IncludeArchived(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)

	// Create an overdue task
	activeTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Active Overdue",
		DueDate:     &pastDue,
	}
	db.Create(activeTask)

	// Create an archived overdue task
	archivedTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Archived Overdue",
		DueDate:     &pastDue,
		IsArchived:  true,
	}
	db.Create(archivedTask)

	// Default: excludes archived
	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1)
	assert.Equal(t, "Active Overdue", tasks[0].Title)

	// With IncludeArchived: includes both
	opts = TaskFilterOptions{ResidenceID: residence.ID, IncludeArchived: true}
	tasks, err = repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 2)
}

// === Timezone Tests ===

func TestGetOverdueTasks_Timezone_TaskBecomesOverdueAtMidnight(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Task due date: Dec 15
	dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Test Task",
		DueDate:     &dueDate,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}

	// On Dec 15 at any time: NOT overdue
	dec15Morning := time.Date(2025, 12, 15, 8, 0, 0, 0, time.UTC)
	tasks, err := repo.GetOverdueTasks(dec15Morning, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task should NOT be overdue on Dec 15")

	// On Dec 16 at midnight: IS overdue
	dec16Midnight := time.Date(2025, 12, 16, 0, 0, 0, 0, time.UTC)
	tasks, err = repo.GetOverdueTasks(dec16Midnight, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task should be overdue on Dec 16")
}

func TestGetOverdueTasks_RecurringTask_UsesNextDueDate(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Now().UTC()
	originalDue := now.AddDate(0, 0, -30) // Original due date: 30 days ago
	nextDue := now.AddDate(0, 0, 5)       // Next due date: 5 days from now

	// Create a recurring task that was completed, with NextDueDate in the future
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Recurring Task",
		DueDate:     &originalDue,
		NextDueDate: &nextDue,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	// Task should NOT be overdue because NextDueDate (effective date) is in the future
	assert.Len(t, tasks, 0, "Recurring task with future NextDueDate should NOT be overdue")
}

// === Consistency Tests ===

func TestKanbanBoardMatchesSinglePurposeFunctions(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)
	testutil.SeedLookupData(t, db)

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

	now := time.Now().UTC()
	pastDue := now.AddDate(0, 0, -5)
	dueSoon := now.AddDate(0, 0, 10)
	upcomingDue := now.AddDate(0, 0, 45)

	// Create various tasks
	db.Create(&models.Task{ResidenceID: residence.ID, CreatedByID: user.ID, Title: "Overdue", DueDate: &pastDue})
	db.Create(&models.Task{ResidenceID: residence.ID, CreatedByID: user.ID, Title: "Due Soon", DueDate: &dueSoon})
	db.Create(&models.Task{ResidenceID: residence.ID, CreatedByID: user.ID, Title: "Upcoming", DueDate: &upcomingDue})
	db.Create(&models.Task{ResidenceID: residence.ID, CreatedByID: user.ID, Title: "In Progress", InProgress: true})
	db.Create(&models.Task{ResidenceID: residence.ID, CreatedByID: user.ID, Title: "Cancelled", IsCancelled: true})

	// Create a completed task
	completedTask := &models.Task{ResidenceID: residence.ID, CreatedByID: user.ID, Title: "Completed"}
	db.Create(completedTask)
	db.Create(&models.TaskCompletion{TaskID: completedTask.ID, CompletedByID: user.ID, CompletedAt: now})

	opts := TaskFilterOptions{ResidenceID: residence.ID, PreloadCompletions: true}

	// Get tasks using single-purpose functions
	overdue, _ := repo.GetOverdueTasks(now, opts)
	dueSoonTasks, _ := repo.GetDueSoonTasks(now, 30, opts)
	inProgress, _ := repo.GetInProgressTasks(opts)
	upcoming, _ := repo.GetUpcomingTasks(now, 30, opts)
	completed, _ := repo.GetCompletedTasks(opts)
	cancelled, _ := repo.GetCancelledTasks(opts)

	// Get kanban board
	board, err := repo.GetKanbanData(residence.ID, 30, now)
	require.NoError(t, err)

	// Compare counts
	var boardOverdue, boardDueSoon, boardInProgress, boardUpcoming, boardCompleted int
	foundCancelledColumn := false
	for _, col := range board.Columns {
		switch col.Name {
		case "overdue_tasks":
			boardOverdue = col.Count
		case "due_soon_tasks":
			boardDueSoon = col.Count
		case "in_progress_tasks":
			boardInProgress = col.Count
		case "upcoming_tasks":
			boardUpcoming = col.Count
		case "completed_tasks":
			boardCompleted = col.Count
		case "cancelled_tasks":
			foundCancelledColumn = true
		}
	}

	assert.Equal(t, len(overdue), boardOverdue, "Overdue count mismatch")
	assert.Equal(t, len(dueSoonTasks), boardDueSoon, "Due Soon count mismatch")
	assert.Equal(t, len(inProgress), boardInProgress, "In Progress count mismatch")
	assert.Equal(t, len(upcoming), boardUpcoming, "Upcoming count mismatch")
	assert.Equal(t, len(completed), boardCompleted, "Completed count mismatch")
	assert.False(t, foundCancelledColumn, "cancelled column must be hidden")
	assert.NotEmpty(t, cancelled, "single-purpose cancelled query should still return cancelled tasks")
}

// === Additional Timezone Tests ===
//
// Note: These tests verify that the `now` parameter's timezone affects
// when "start of day" is calculated. Due dates are stored in UTC, but the
// scope computes startOfDay using the timezone of the `now` parameter.
// This allows users in different timezones to see correct overdue status
// based on their local date.

func TestGetOverdueTasks_Timezone_Tokyo(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	tokyo, _ := time.LoadLocation("Asia/Tokyo")

	// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
	// A task "due on Dec 15" is stored as 2025-12-15 00:00:00 UTC.
	// Timezone handling happens at query time via the `now` parameter.
	dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Test Task",
		DueDate:     &dueDate,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}

	// When it's Dec 15 23:00 in Tokyo, task should NOT be overdue (still Dec 15 in Tokyo)
	// The `now` parameter's date (Dec 15) is compared against the due date (Dec 15)
	tokyoDec15Evening := time.Date(2025, 12, 15, 23, 0, 0, 0, tokyo)
	tasks, err := repo.GetOverdueTasks(tokyoDec15Evening, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task should NOT be overdue on Dec 15 in Tokyo timezone")

	// When it's Dec 16 00:00 in Tokyo, task IS overdue (now Dec 16 in Tokyo, due Dec 15)
	tokyoDec16Midnight := time.Date(2025, 12, 16, 0, 0, 0, 0, tokyo)
	tasks, err = repo.GetOverdueTasks(tokyoDec16Midnight, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task should be overdue on Dec 16 in Tokyo timezone")
}

func TestGetOverdueTasks_Timezone_NewYork(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	newYork, _ := time.LoadLocation("America/New_York")

	// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
	// A task "due on Dec 15" is stored as 2025-12-15 00:00:00 UTC.
	// Timezone handling happens at query time via the `now` parameter.
	dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Test Task",
		DueDate:     &dueDate,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}

	// When it's Dec 15 23:00 in New York, task should NOT be overdue (still Dec 15 in NY)
	nyDec15Evening := time.Date(2025, 12, 15, 23, 0, 0, 0, newYork)
	tasks, err := repo.GetOverdueTasks(nyDec15Evening, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task should NOT be overdue on Dec 15 in New York timezone")

	// When it's Dec 16 00:00 in New York, task IS overdue (now Dec 16 in NY, due Dec 15)
	nyDec16Midnight := time.Date(2025, 12, 16, 0, 0, 0, 0, newYork)
	tasks, err = repo.GetOverdueTasks(nyDec16Midnight, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task should be overdue on Dec 16 in New York timezone")
}

func TestGetOverdueTasks_Timezone_InternationalDateLine(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Auckland is UTC+13, Honolulu is UTC-10
	// They're 23 hours apart in local time
	auckland, _ := time.LoadLocation("Pacific/Auckland")
	honolulu, _ := time.LoadLocation("Pacific/Honolulu")

	// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
	// Task due on Dec 15 is stored as 2025-12-15 00:00:00 UTC.
	dueDate := time.Date(2025, 12, 15, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Test Task",
		DueDate:     &dueDate,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}

	// From Auckland's perspective on Dec 16, task is overdue (Dec 16 > Dec 15)
	aucklandDec16 := time.Date(2025, 12, 16, 0, 0, 0, 0, auckland)
	tasks, err := repo.GetOverdueTasks(aucklandDec16, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task should be overdue on Dec 16 in Auckland")

	// From Honolulu's perspective on Dec 14, task is NOT overdue (Dec 14 < Dec 15)
	honoluluDec14 := time.Date(2025, 12, 14, 5, 0, 0, 0, honolulu)
	tasks, err = repo.GetOverdueTasks(honoluluDec14, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task should NOT be overdue on Dec 14 in Honolulu")
}

func TestGetDueSoonTasks_Timezone_DST(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	newYork, _ := time.LoadLocation("America/New_York")

	// IMPORTANT: Due dates are stored as midnight UTC representing calendar dates.
	// 2025 DST ends Nov 2: clocks fall back from 2:00 AM to 1:00 AM
	// Task due on Nov 5 is stored as 2025-11-05 00:00:00 UTC
	dueDate := time.Date(2025, 11, 5, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Post-DST Task",
		DueDate:     &dueDate,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}

	// On Nov 3 (day after DST ends), with 7-day threshold, task should be due soon
	// Nov 5 is 2 days from Nov 3, which is within 7-day threshold
	nov3NY := time.Date(2025, 11, 3, 10, 0, 0, 0, newYork)
	tasks, err := repo.GetDueSoonTasks(nov3NY, 7, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task due Nov 5 should be 'due soon' when checking from Nov 3 with 7-day threshold")

	// With 2-day threshold: Nov 3 + 2 days = Nov 5 (at threshold boundary, exclusive)
	// Task due Nov 5 is exactly AT the threshold, so should be EXCLUDED
	tasks, err = repo.GetDueSoonTasks(nov3NY, 2, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task due Nov 5 should NOT be 'due soon' when checking from Nov 3 with 2-day threshold (at boundary)")

	// With 3-day threshold: Nov 3 + 3 days = Nov 6 (task is before this)
	// Task due Nov 5 should be INCLUDED
	tasks, err = repo.GetDueSoonTasks(nov3NY, 3, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task due Nov 5 should be 'due soon' when checking from Nov 3 with 3-day threshold")
}

// === Additional Edge Case Tests ===

func TestGetDueSoonTasks_TaskDueAtThreshold_Excluded(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Fixed reference time
	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)
	threshold := 7 // 7 days

	// Task due exactly at threshold boundary (Dec 23 = Dec 16 + 7 days)
	// The threshold is exclusive: due_soon means < (today + threshold)
	// So Dec 23 is NOT included in due_soon when threshold is 7
	atThreshold := time.Date(2025, 12, 23, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "At Threshold",
		DueDate:     &atThreshold,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetDueSoonTasks(now, threshold, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task due exactly at threshold boundary should be EXCLUDED (threshold is exclusive)")
}

func TestGetDueSoonTasks_TaskDueBeforeThreshold_Included(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Fixed reference time
	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)
	threshold := 7 // 7 days

	// Task due one day before threshold (Dec 22 = Dec 16 + 6 days)
	beforeThreshold := time.Date(2025, 12, 22, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Before Threshold",
		DueDate:     &beforeThreshold,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetDueSoonTasks(now, threshold, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 1, "Task due before threshold boundary should be INCLUDED")
	assert.Equal(t, "Before Threshold", tasks[0].Title)
}

func TestGetDueSoonTasks_TaskDuePastThreshold_Excluded(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	// Fixed reference time
	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)
	threshold := 7 // 7 days

	// Task due after threshold (Dec 25 = Dec 16 + 9 days)
	pastThreshold := time.Date(2025, 12, 25, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Past Threshold",
		DueDate:     &pastThreshold,
	}
	db.Create(task)

	opts := TaskFilterOptions{ResidenceID: residence.ID}
	tasks, err := repo.GetDueSoonTasks(now, threshold, opts)
	require.NoError(t, err)
	assert.Len(t, tasks, 0, "Task due past threshold should be EXCLUDED from due soon")

	// Verify it goes to upcoming instead
	upcomingTasks, err := repo.GetUpcomingTasks(now, threshold, opts)
	require.NoError(t, err)
	assert.Len(t, upcomingTasks, 1, "Task due past threshold should be in UPCOMING")
}

func TestGetOverdueTasks_CompletedRecurring_NotOverdue(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)

	// Create a recurring task that was due Dec 10 (overdue), but has been completed
	// and NextDueDate is set to Dec 20 (future)
	originalDue := time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC)
	nextDue := time.Date(2025, 12, 20, 0, 0, 0, 0, time.UTC)

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Recurring Task",
		DueDate:     &originalDue,
		NextDueDate: &nextDue, // Future date means it uses this for effective date
	}
	db.Create(task)

	// Add a completion record
	db.Create(&models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Date(2025, 12, 12, 10, 0, 0, 0, time.UTC),
	})

	opts := TaskFilterOptions{ResidenceID: residence.ID, PreloadCompletions: true}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	// Task should NOT be overdue because NextDueDate (Dec 20) is in the future
	assert.Len(t, tasks, 0, "Recurring task with future NextDueDate should NOT be overdue")

	// Verify it's in due soon instead (Dec 20 is within 30 days)
	dueSoonTasks, err := repo.GetDueSoonTasks(now, 30, opts)
	require.NoError(t, err)
	assert.Len(t, dueSoonTasks, 1, "Recurring task with future NextDueDate should be in DUE SOON")
}

func TestGetOverdueTasks_CompletedOneTime_NotOverdue(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)

	// Create a one-time task that was due Dec 10 (overdue) but has been completed
	// NextDueDate is nil (one-time task)
	originalDue := time.Date(2025, 12, 10, 0, 0, 0, 0, time.UTC)

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Completed One-Time Task",
		DueDate:     &originalDue,
		NextDueDate: nil, // One-time task
	}
	db.Create(task)

	// Add a completion record
	db.Create(&models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Date(2025, 12, 12, 10, 0, 0, 0, time.UTC),
	})

	opts := TaskFilterOptions{ResidenceID: residence.ID, PreloadCompletions: true}
	tasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)
	// Task should NOT be overdue because it's completed (NextDueDate nil + has completion)
	assert.Len(t, tasks, 0, "Completed one-time task should NOT be overdue")

	// Verify it's in completed column
	completedTasks, err := repo.GetCompletedTasks(opts)
	require.NoError(t, err)
	assert.Len(t, completedTasks, 1, "Completed one-time task should be in COMPLETED")
}

// === Consistency Tests ===

func TestConsistency_DueSoonPredicateVsScopeVsRepo(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)
	threshold := 30

	// Create tasks with various due dates
	testCases := []struct {
		title       string
		daysFromNow int
		expectDue   bool
	}{
		{"Due Today", 0, true},
		{"Due Tomorrow", 1, true},
		{"Due in 15 days", 15, true},
		{"Due in 29 days", 29, true},
		{"Due at threshold (30 days)", 30, false},  // Threshold is exclusive
		{"Due past threshold (31 days)", 31, false},
		{"Overdue (-1 day)", -1, false},
	}

	for _, tc := range testCases {
		dueDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, tc.daysFromNow)
		task := &models.Task{
			ResidenceID: residence.ID,
			CreatedByID: user.ID,
			Title:       tc.title,
			DueDate:     &dueDate,
		}
		db.Create(task)
	}

	opts := TaskFilterOptions{ResidenceID: residence.ID, PreloadCompletions: true}

	// Get due soon tasks from repository
	repoTasks, err := repo.GetDueSoonTasks(now, threshold, opts)
	require.NoError(t, err)

	// Build map of task titles returned by repo
	repoTaskTitles := make(map[string]bool)
	for _, task := range repoTasks {
		repoTaskTitles[task.Title] = true
	}

	// Verify each test case
	for _, tc := range testCases {
		found := repoTaskTitles[tc.title]
		if tc.expectDue {
			assert.True(t, found, "Task '%s' should be due soon but wasn't returned", tc.title)
		} else {
			assert.False(t, found, "Task '%s' should NOT be due soon but was returned", tc.title)
		}
	}

	// Also verify count matches expected
	expectedCount := 0
	for _, tc := range testCases {
		if tc.expectDue {
			expectedCount++
		}
	}
	assert.Equal(t, expectedCount, len(repoTasks), "Due soon task count mismatch")
}

func TestConsistency_OverduePredicateVsScopeVsRepo(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Date(2025, 12, 16, 10, 0, 0, 0, time.UTC)

	// Create tasks with various states
	testCases := []struct {
		title         string
		daysFromNow   int
		isCancelled   bool
		isArchived    bool
		hasCompletion bool
		nextDueDate   *time.Time
		expectOverdue bool
	}{
		{"Overdue Yesterday", -1, false, false, false, nil, true},
		{"Overdue Week Ago", -7, false, false, false, nil, true},
		{"Due Today", 0, false, false, false, nil, false},
		{"Due Tomorrow", 1, false, false, false, nil, false},
		{"Overdue but Cancelled", -5, true, false, false, nil, false},
		{"Overdue but Archived", -5, false, true, false, nil, false},
	}

	for _, tc := range testCases {
		dueDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, tc.daysFromNow)
		task := &models.Task{
			ResidenceID: residence.ID,
			CreatedByID: user.ID,
			Title:       tc.title,
			DueDate:     &dueDate,
			IsCancelled: tc.isCancelled,
			IsArchived:  tc.isArchived,
			NextDueDate: tc.nextDueDate,
		}
		db.Create(task)

		if tc.hasCompletion {
			db.Create(&models.TaskCompletion{
				TaskID:        task.ID,
				CompletedByID: user.ID,
				CompletedAt:   now,
			})
		}
	}

	// Also add a completed one-time task (should NOT be overdue)
	completedDue := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -3)
	completedTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Completed One-Time Overdue",
		DueDate:     &completedDue,
		NextDueDate: nil,
	}
	db.Create(completedTask)
	db.Create(&models.TaskCompletion{
		TaskID:        completedTask.ID,
		CompletedByID: user.ID,
		CompletedAt:   now,
	})

	opts := TaskFilterOptions{ResidenceID: residence.ID, PreloadCompletions: true}

	// Get overdue tasks from repository
	repoTasks, err := repo.GetOverdueTasks(now, opts)
	require.NoError(t, err)

	// Build map of task titles returned by repo
	repoTaskTitles := make(map[string]bool)
	for _, task := range repoTasks {
		repoTaskTitles[task.Title] = true
	}

	// Verify each test case
	for _, tc := range testCases {
		found := repoTaskTitles[tc.title]
		if tc.expectOverdue {
			assert.True(t, found, "Task '%s' should be overdue but wasn't returned", tc.title)
		} else {
			assert.False(t, found, "Task '%s' should NOT be overdue but was returned", tc.title)
		}
	}

	// Verify completed one-time task is not overdue
	assert.False(t, repoTaskTitles["Completed One-Time Overdue"], "Completed one-time task should NOT be overdue")

	// Also verify count matches expected
	expectedCount := 0
	for _, tc := range testCases {
		if tc.expectOverdue {
			expectedCount++
		}
	}
	assert.Equal(t, expectedCount, len(repoTasks), "Overdue task count mismatch")
}

// TestGetKanbanData_CategorizesCorrectly verifies the single-query kanban approach
// produces correct column assignments for various task states.
func TestGetKanbanData_CategorizesCorrectly(t *testing.T) {
	db := testutil.SetupTestDB(t)
	repo := NewTaskRepository(db)

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

	now := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
	yesterday := now.AddDate(0, 0, -1)
	tomorrow := now.AddDate(0, 0, 1)
	nextMonth := now.AddDate(0, 1, 0)

	// Create overdue task (due yesterday)
	overdueTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Overdue Task",
		DueDate:     &yesterday,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(overdueTask).Error)

	// Create due-soon task (due tomorrow, within 30-day threshold)
	dueSoonTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Due Soon Task",
		DueDate:     &tomorrow,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(dueSoonTask).Error)

	// Create upcoming task (due next month, outside 30-day threshold)
	upcomingTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Upcoming Task",
		DueDate:     &nextMonth,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(upcomingTask).Error)

	// Create in-progress task
	inProgressTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "In Progress Task",
		DueDate:     &tomorrow,
		InProgress:  true,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(inProgressTask).Error)

	// Create completed task (no next due date, has completion)
	completedTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Completed Task",
		DueDate:     &yesterday,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(completedTask).Error)
	completion := &models.TaskCompletion{
		TaskID:        completedTask.ID,
		CompletedByID: user.ID,
		CompletedAt:   now,
	}
	require.NoError(t, db.Create(completion).Error)

	// Create cancelled task (should NOT appear in kanban columns)
	cancelledTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Cancelled Task",
		DueDate:     &yesterday,
		IsCancelled: true,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(cancelledTask).Error)

	// Create archived task (should NOT appear in active kanban columns)
	archivedTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Archived Task",
		DueDate:     &yesterday,
		IsCancelled: false,
		IsArchived:  true,
		Version:     1,
	}
	require.NoError(t, db.Create(archivedTask).Error)

	// Create no-due-date task (should go to upcoming)
	noDueDateTask := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "No Due Date Task",
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	require.NoError(t, db.Create(noDueDateTask).Error)

	// Execute kanban data retrieval
	board, err := repo.GetKanbanData(residence.ID, 30, now)
	require.NoError(t, err)
	require.NotNil(t, board)
	require.Len(t, board.Columns, 5, "Should have 5 visible columns")

	// Build a map of column name -> task titles for easy assertion
	columnTasks := make(map[string][]string)
	for _, col := range board.Columns {
		var titles []string
		for _, task := range col.Tasks {
			titles = append(titles, task.Title)
		}
		columnTasks[col.Name] = titles
	}

	// Verify overdue column
	assert.Contains(t, columnTasks["overdue_tasks"], "Overdue Task",
		"Overdue task should be in overdue column")

	// Verify in-progress column
	assert.Contains(t, columnTasks["in_progress_tasks"], "In Progress Task",
		"In-progress task should be in in-progress column")

	// Verify due-soon column
	assert.Contains(t, columnTasks["due_soon_tasks"], "Due Soon Task",
		"Due-soon task should be in due-soon column")

	// Verify upcoming column contains both upcoming and no-due-date tasks
	assert.Contains(t, columnTasks["upcoming_tasks"], "No Due Date Task",
		"No-due-date task should be in upcoming column")

	// Verify completed column
	assert.Contains(t, columnTasks["completed_tasks"], "Completed Task",
		"Completed task should be in completed column")

	// Verify cancelled and archived tasks are categorized to the cancelled column
	// (which is present in categorization but hidden from visible kanban columns)
	// The cancelled/archived tasks should NOT appear in any of the 5 visible columns
	allVisibleTitles := make(map[string]bool)
	for _, col := range board.Columns {
		for _, task := range col.Tasks {
			allVisibleTitles[task.Title] = true
		}
	}
	assert.False(t, allVisibleTitles["Cancelled Task"],
		"Cancelled task should not appear in visible kanban columns")
	assert.False(t, allVisibleTitles["Archived Task"],
		"Archived task should not appear in visible kanban columns")
}
