package services

import (
	"testing"
	"time"

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

	"github.com/treytartt/honeydue-api/internal/dto/requests"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/repositories"
	"github.com/treytartt/honeydue-api/internal/task/categorization"
	"github.com/treytartt/honeydue-api/internal/testutil"
)

// =============================================================================
// STATE TRANSITION TESTS (TDD)
// =============================================================================

func TestTaskService_CompleteToCancel_OneTimeTask(t *testing.T) {
	// Complete a one-time task (NextDueDate=nil after completion), then cancel it.
	// Verify IsCancelled=true and kanban column is "cancelled".
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Complete Then Cancel",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Complete the task
	completionReq := &requests.CreateTaskCompletionRequest{
		TaskID: task.ID,
		Notes:  "Completed",
	}
	_, err = service.CreateCompletion(completionReq, user.ID, now)
	require.NoError(t, err)

	// Verify task is completed (NextDueDate=nil)
	var taskAfterComplete models.Task
	db.Preload("Completions").First(&taskAfterComplete, task.ID)
	assert.Nil(t, taskAfterComplete.NextDueDate, "One-time task NextDueDate should be nil after completion")
	assert.True(t, len(taskAfterComplete.Completions) > 0, "Task should have completions")

	// Verify kanban column is "completed" before cancelling
	column := categorization.DetermineKanbanColumnWithTime(&taskAfterComplete, 30, now)
	assert.Equal(t, "completed_tasks", column, "Completed one-time task should be in completed column")

	// Step 2: Cancel the completed task
	cancelResp, err := service.CancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, cancelResp.Data.IsCancelled, "Task should be cancelled")

	// Reload and verify kanban column is "cancelled"
	var taskAfterCancel models.Task
	db.Preload("Completions").First(&taskAfterCancel, task.ID)
	column = categorization.DetermineKanbanColumnWithTime(&taskAfterCancel, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Cancelled task should be in cancelled column regardless of completion state")
}

func TestTaskService_CancelToComplete(t *testing.T) {
	// Cancel a task, then complete the cancelled task.
	// Verify completion is created and IsCancelled remains true.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Cancel Then Complete",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Cancel the task
	cancelResp, err := service.CancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, cancelResp.Data.IsCancelled)

	// Step 2: Complete the cancelled task (service does not prevent this)
	completionReq := &requests.CreateTaskCompletionRequest{
		TaskID: task.ID,
		Notes:  "Completed even though cancelled",
	}
	completionResp, err := service.CreateCompletion(completionReq, user.ID, now)
	require.NoError(t, err)
	assert.NotZero(t, completionResp.Data.ID, "Completion should be created")

	// Reload and verify state
	var reloaded models.Task
	db.Preload("Completions").First(&reloaded, task.ID)
	assert.True(t, reloaded.IsCancelled, "IsCancelled should remain true after completion")
	assert.Nil(t, reloaded.NextDueDate, "NextDueDate should be nil for completed one-time task")
	assert.True(t, len(reloaded.Completions) > 0, "Task should have a completion")

	// Kanban column should be "cancelled" because cancelled takes priority
	column := categorization.DetermineKanbanColumnWithTime(&reloaded, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Cancelled takes priority over completed in kanban")
}

func TestTaskService_ArchiveToComplete(t *testing.T) {
	// Archive a task, then complete the archived task.
	// Verify completion is created and IsArchived remains true.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Archive Then Complete",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Archive the task
	archiveResp, err := service.ArchiveTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, archiveResp.Data.IsArchived)

	// Step 2: Complete the archived task
	completionReq := &requests.CreateTaskCompletionRequest{
		TaskID: task.ID,
		Notes:  "Completed even though archived",
	}
	completionResp, err := service.CreateCompletion(completionReq, user.ID, now)
	require.NoError(t, err)
	assert.NotZero(t, completionResp.Data.ID, "Completion should be created")

	// Reload and verify state
	var reloaded models.Task
	db.Preload("Completions").First(&reloaded, task.ID)
	assert.True(t, reloaded.IsArchived, "IsArchived should remain true after completion")
	assert.Nil(t, reloaded.NextDueDate, "NextDueDate should be nil for completed one-time task")
	assert.True(t, len(reloaded.Completions) > 0, "Task should have a completion")

	// Kanban column: archived maps to cancelled column (both are "inactive" states)
	column := categorization.DetermineKanbanColumnWithTime(&reloaded, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Archived task should show in cancelled column")
}

func TestTaskService_CompleteToArchive_OneTimeTask(t *testing.T) {
	// Complete a one-time task, then archive it.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Complete Then Archive",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Complete the task
	completionReq := &requests.CreateTaskCompletionRequest{
		TaskID: task.ID,
		Notes:  "Done",
	}
	_, err = service.CreateCompletion(completionReq, user.ID, now)
	require.NoError(t, err)

	// Step 2: Archive the completed task
	archiveResp, err := service.ArchiveTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, archiveResp.Data.IsArchived, "Task should be archived")

	// Reload and verify
	var reloaded models.Task
	db.Preload("Completions").First(&reloaded, task.ID)
	assert.True(t, reloaded.IsArchived)
	assert.Nil(t, reloaded.NextDueDate, "NextDueDate should remain nil")
	assert.True(t, len(reloaded.Completions) > 0, "Completions should be preserved")

	// Kanban column: archived maps to cancelled
	column := categorization.DetermineKanbanColumnWithTime(&reloaded, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Archived task should be in cancelled column")
}

func TestTaskService_InProgressToCancelToUncancel(t *testing.T) {
	// Mark task in progress, cancel it, then uncancel it.
	// Verify InProgress is preserved through cancel/uncancel cycle.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 15, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "InProgress Cancel Uncancel",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Mark in progress
	inProgressResp, err := service.MarkInProgress(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, inProgressResp.Data.InProgress)

	// Verify kanban column is in_progress
	var taskAfterIP models.Task
	db.First(&taskAfterIP, task.ID)
	column := categorization.DetermineKanbanColumnWithTime(&taskAfterIP, 30, now)
	assert.Equal(t, "in_progress_tasks", column, "Task should be in in_progress column")

	// Step 2: Cancel the in-progress task
	cancelResp, err := service.CancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, cancelResp.Data.IsCancelled)

	// Cancel only sets is_cancelled=true, it does NOT reset in_progress
	var taskAfterCancel models.Task
	db.First(&taskAfterCancel, task.ID)
	assert.True(t, taskAfterCancel.IsCancelled)
	assert.True(t, taskAfterCancel.InProgress, "InProgress should still be true after cancel (cancel only sets is_cancelled)")

	// Kanban column should be cancelled (highest priority)
	column = categorization.DetermineKanbanColumnWithTime(&taskAfterCancel, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Cancelled task should be in cancelled column")

	// Step 3: Uncancel the task
	uncancelResp, err := service.UncancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.False(t, uncancelResp.Data.IsCancelled)

	// InProgress should still be true after uncancel
	var taskAfterUncancel models.Task
	db.First(&taskAfterUncancel, task.ID)
	assert.False(t, taskAfterUncancel.IsCancelled)
	assert.True(t, taskAfterUncancel.InProgress, "InProgress should still be true after uncancel")

	// Task should return to in_progress kanban column
	column = categorization.DetermineKanbanColumnWithTime(&taskAfterUncancel, 30, now)
	assert.Equal(t, "in_progress_tasks", column, "Uncancelled in-progress task should return to in_progress column")
}

func TestTaskService_MultipleCancelUncancelCycles(t *testing.T) {
	// Cancel -> Uncancel -> Cancel -> Uncancel
	// Verify final state is uncancelled.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 10, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Multiple Cancel Uncancel",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Cycle 1: Cancel -> Uncancel
	_, err = service.CancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	_, err = service.UncancelTask(task.ID, user.ID, now)
	require.NoError(t, err)

	// Cycle 2: Cancel -> Uncancel
	_, err = service.CancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	uncancelResp, err := service.UncancelTask(task.ID, user.ID, now)
	require.NoError(t, err)

	// Verify final state
	assert.False(t, uncancelResp.Data.IsCancelled, "Task should be uncancelled after two cycles")

	// Reload from DB to confirm
	var reloaded models.Task
	db.First(&reloaded, task.ID)
	assert.False(t, reloaded.IsCancelled, "Database should confirm task is uncancelled")

	// Kanban column should be based on dates (due_soon since due date is within 30 days)
	column := categorization.DetermineKanbanColumnWithTime(&reloaded, 30, now)
	assert.Equal(t, "due_soon_tasks", column, "Uncancelled task with due date within 30 days should be due_soon")
}

func TestTaskService_CompleteToMarkInProgress_OneTimeTask(t *testing.T) {
	// Complete a one-time task (NextDueDate=nil), then mark it in progress.
	// InProgress is set to true but task is still "completed" per predicates (chain priority).
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	dueDate := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Complete Then InProgress",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Complete the task (sets NextDueDate=nil, InProgress=false for one-time)
	completionReq := &requests.CreateTaskCompletionRequest{
		TaskID: task.ID,
		Notes:  "Done",
	}
	_, err = service.CreateCompletion(completionReq, user.ID, now)
	require.NoError(t, err)

	// Verify completed state
	var taskAfterComplete models.Task
	db.Preload("Completions").First(&taskAfterComplete, task.ID)
	assert.Nil(t, taskAfterComplete.NextDueDate)

	// Step 2: Mark in progress
	ipResp, err := service.MarkInProgress(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, ipResp.Data.InProgress, "InProgress should be true")

	// Verify NextDueDate is still nil
	var taskAfterIP models.Task
	db.Preload("Completions").First(&taskAfterIP, task.ID)
	assert.Nil(t, taskAfterIP.NextDueDate, "NextDueDate should still be nil")
	assert.True(t, taskAfterIP.InProgress)

	// Kanban column: completed handler (priority 3) is checked before in_progress (priority 4)
	// So the task is still "completed" since NextDueDate=nil and has completions
	column := categorization.DetermineKanbanColumnWithTime(&taskAfterIP, 30, now)
	assert.Equal(t, "completed_tasks", column, "Completed takes priority over in_progress in the chain")
}

func TestTaskService_RecurringTaskStateCycle(t *testing.T) {
	// Full cycle for a recurring weekly task:
	// Create -> Mark in progress -> Complete (recalculates NextDueDate) ->
	// Mark in progress again -> Complete again (recalculates again)
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

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

	var weeklyFrequency models.TaskFrequency
	db.Where("name = ?", "Weekly").First(&weeklyFrequency)

	dueDate := time.Date(2026, 3, 28, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Weekly Recurring Task",
		FrequencyID: &weeklyFrequency.ID,
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Step 1: Mark in progress
	ipResp, err := service.MarkInProgress(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, ipResp.Data.InProgress)

	// Verify kanban column
	var taskAfterIP models.Task
	db.First(&taskAfterIP, task.ID)
	column := categorization.DetermineKanbanColumnWithTime(&taskAfterIP, 30, now)
	assert.Equal(t, "in_progress_tasks", column)

	// Step 2: Complete (first cycle)
	firstCompletedAt := time.Date(2026, 3, 28, 10, 0, 0, 0, time.UTC)
	completionReq1 := &requests.CreateTaskCompletionRequest{
		TaskID:      task.ID,
		Notes:       "Week 1 done",
		CompletedAt: &firstCompletedAt,
	}
	_, err = service.CreateCompletion(completionReq1, user.ID, now)
	require.NoError(t, err)

	// Verify: InProgress reset to false, NextDueDate recalculated
	var taskAfterComplete1 models.Task
	db.First(&taskAfterComplete1, task.ID)
	assert.False(t, taskAfterComplete1.InProgress, "InProgress should be reset after completion")
	require.NotNil(t, taskAfterComplete1.NextDueDate, "Recurring task should have NextDueDate")
	expectedNextDue1 := firstCompletedAt.AddDate(0, 0, 7) // weekly = 7 days
	assert.Equal(t, expectedNextDue1.Year(), taskAfterComplete1.NextDueDate.Year())
	assert.Equal(t, expectedNextDue1.Month(), taskAfterComplete1.NextDueDate.Month())
	assert.Equal(t, expectedNextDue1.Day(), taskAfterComplete1.NextDueDate.Day())

	// Kanban column should not be "completed" since NextDueDate is set for recurring
	column = categorization.DetermineKanbanColumnWithTime(&taskAfterComplete1, 30, now)
	assert.NotEqual(t, "completed_tasks", column, "Recurring task should not be in completed column")

	// Step 3: Mark in progress again for next cycle
	ipResp2, err := service.MarkInProgress(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, ipResp2.Data.InProgress)

	// Step 4: Complete (second cycle)
	secondCompletedAt := time.Date(2026, 4, 4, 10, 0, 0, 0, time.UTC)
	completionReq2 := &requests.CreateTaskCompletionRequest{
		TaskID:      task.ID,
		Notes:       "Week 2 done",
		CompletedAt: &secondCompletedAt,
	}
	_, err = service.CreateCompletion(completionReq2, user.ID, now)
	require.NoError(t, err)

	// Verify: InProgress reset again, NextDueDate recalculated from second completion
	var taskAfterComplete2 models.Task
	db.First(&taskAfterComplete2, task.ID)
	assert.False(t, taskAfterComplete2.InProgress, "InProgress should be reset after second completion")
	require.NotNil(t, taskAfterComplete2.NextDueDate, "Recurring task should have NextDueDate after second completion")
	expectedNextDue2 := secondCompletedAt.AddDate(0, 0, 7)
	assert.Equal(t, expectedNextDue2.Year(), taskAfterComplete2.NextDueDate.Year())
	assert.Equal(t, expectedNextDue2.Month(), taskAfterComplete2.NextDueDate.Month())
	assert.Equal(t, expectedNextDue2.Day(), taskAfterComplete2.NextDueDate.Day())
}

func TestTaskService_ArchiveToUnarchive(t *testing.T) {
	// Archive a task, then unarchive it.
	// Verify the task returns to the correct kanban column based on dates.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

	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 5 days from now (should be "due_soon")
	dueDate := time.Date(2026, 3, 31, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Archive Unarchive",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Verify initial kanban column
	column := categorization.DetermineKanbanColumnWithTime(task, 30, now)
	assert.Equal(t, "due_soon_tasks", column, "Task due in 5 days should be due_soon")

	// Step 1: Archive
	archiveResp, err := service.ArchiveTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, archiveResp.Data.IsArchived)

	var taskAfterArchive models.Task
	db.First(&taskAfterArchive, task.ID)
	column = categorization.DetermineKanbanColumnWithTime(&taskAfterArchive, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Archived task should be in cancelled column")

	// Step 2: Unarchive
	unarchiveResp, err := service.UnarchiveTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.False(t, unarchiveResp.Data.IsArchived)

	var taskAfterUnarchive models.Task
	db.First(&taskAfterUnarchive, task.ID)
	assert.False(t, taskAfterUnarchive.IsArchived)

	// Task should return to its correct kanban column based on dates
	column = categorization.DetermineKanbanColumnWithTime(&taskAfterUnarchive, 30, now)
	assert.Equal(t, "due_soon_tasks", column, "Unarchived task should return to due_soon column based on dates")
}

// =============================================================================
// KANBAN COLUMN PRIORITY VERIFICATION TESTS
// =============================================================================

func TestKanbanColumn_CancelledAlwaysTakesPriority(t *testing.T) {
	// Cancelled task always shows as "cancelled" regardless of other state flags.
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Cancelled + InProgress
	task1 := &models.Task{
		IsCancelled: true,
		InProgress:  true,
	}
	column := categorization.DetermineKanbanColumnWithTime(task1, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Cancelled+InProgress should be cancelled")

	// Cancelled + Completed (NextDueDate=nil with completions)
	task2 := &models.Task{
		IsCancelled: true,
		NextDueDate: nil,
		Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
	}
	column = categorization.DetermineKanbanColumnWithTime(task2, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Cancelled+Completed should be cancelled")

	// Cancelled + Overdue
	overdueDate := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
	task3 := &models.Task{
		IsCancelled: true,
		NextDueDate: &overdueDate,
	}
	column = categorization.DetermineKanbanColumnWithTime(task3, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Cancelled+Overdue should be cancelled")
}

func TestKanbanColumn_ArchivedAlwaysShowsCancelled(t *testing.T) {
	// Archived task always shows as "cancelled" (archived maps to cancelled column).
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	// Archived + InProgress
	task1 := &models.Task{
		IsArchived: true,
		InProgress: true,
	}
	column := categorization.DetermineKanbanColumnWithTime(task1, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Archived+InProgress should be cancelled")

	// Archived + Overdue date
	overdueDate := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
	task2 := &models.Task{
		IsArchived:  true,
		NextDueDate: &overdueDate,
	}
	column = categorization.DetermineKanbanColumnWithTime(task2, 30, now)
	assert.Equal(t, "cancelled_tasks", column, "Archived+Overdue should be cancelled")
}

func TestKanbanColumn_CompletedTaskPriority(t *testing.T) {
	// Completed task (NextDueDate=nil + completions) shows as "completed".
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	task := &models.Task{
		NextDueDate: nil,
		Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
		IsCancelled: false,
		IsArchived:  false,
	}
	column := categorization.DetermineKanbanColumnWithTime(task, 30, now)
	assert.Equal(t, "completed_tasks", column)
}

func TestKanbanColumn_InProgressTaskPriority(t *testing.T) {
	// InProgress task shows as "in_progress" (when not cancelled/archived/completed).
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	dueDate := time.Date(2026, 4, 15, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		InProgress:  true,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
	}
	column := categorization.DetermineKanbanColumnWithTime(task, 30, now)
	assert.Equal(t, "in_progress_tasks", column)
}

func TestKanbanColumn_OverdueTaskPriority(t *testing.T) {
	// Overdue task shows as "overdue".
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	overdueDate := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		NextDueDate: &overdueDate,
		IsCancelled: false,
		IsArchived:  false,
		InProgress:  false,
	}
	column := categorization.DetermineKanbanColumnWithTime(task, 30, now)
	assert.Equal(t, "overdue_tasks", column)
}

func TestKanbanColumn_DueSoonTaskPriority(t *testing.T) {
	// Due soon task (within threshold) shows as "due_soon".
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	dueSoonDate := time.Date(2026, 4, 10, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		NextDueDate: &dueSoonDate,
		IsCancelled: false,
		IsArchived:  false,
		InProgress:  false,
	}
	column := categorization.DetermineKanbanColumnWithTime(task, 30, now)
	assert.Equal(t, "due_soon_tasks", column)
}

func TestKanbanColumn_UpcomingTaskDefault(t *testing.T) {
	// Default task (no special state, far-future due date) shows as "upcoming".
	now := time.Date(2026, 3, 26, 0, 0, 0, 0, time.UTC)

	farFuture := time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		NextDueDate: &farFuture,
		IsCancelled: false,
		IsArchived:  false,
		InProgress:  false,
	}
	column := categorization.DetermineKanbanColumnWithTime(task, 30, now)
	assert.Equal(t, "upcoming_tasks", column)

	// Also verify a task with no due date and no completions is upcoming
	taskNoDue := &models.Task{
		NextDueDate: nil,
		DueDate:     nil,
		IsCancelled: false,
		IsArchived:  false,
		InProgress:  false,
	}
	column = categorization.DetermineKanbanColumnWithTime(taskNoDue, 30, now)
	assert.Equal(t, "upcoming_tasks", column, "Task with no due date and no completions should be upcoming")
}

// =============================================================================
// OPTIMISTIC LOCKING TESTS
// =============================================================================

func TestTaskService_OptimisticLocking_UpdateWithCorrectVersion(t *testing.T) {
	// Update task with correct version -> success, version incremented.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

	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, "Version Test")

	// Verify initial version
	var initialTask models.Task
	db.First(&initialTask, task.ID)
	initialVersion := initialTask.Version

	// Update task via service
	newTitle := "Updated Title"
	req := &requests.UpdateTaskRequest{
		Title: &newTitle,
	}
	now := time.Now().UTC()
	resp, err := service.UpdateTask(task.ID, user.ID, req, now)
	require.NoError(t, err)
	assert.Equal(t, "Updated Title", resp.Data.Title)

	// Verify version was incremented
	var updatedTask models.Task
	db.First(&updatedTask, task.ID)
	assert.Equal(t, initialVersion+1, updatedTask.Version, "Version should be incremented after successful update")
}

func TestTaskService_OptimisticLocking_UpdateWithStaleVersion(t *testing.T) {
	// Update task with stale version -> verify ErrVersionConflict returned.
	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")

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Stale Version Test",
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Bump version in DB to simulate concurrent modification
	db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 999)

	// Try to update with stale version (task object still has version=1)
	task.Title = "Should Fail"
	err = taskRepo.Update(task)
	require.Error(t, err)
	assert.ErrorIs(t, err, repositories.ErrVersionConflict, "Should return ErrVersionConflict for stale version")
}

func TestTaskService_OptimisticLocking_CompleteWithStaleVersion(t *testing.T) {
	// Complete task with stale version -> verify conflict handled and completion rolled back.
	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")

	dueDate := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Completion Conflict Test",
		DueDate:     &dueDate,
		NextDueDate: &dueDate,
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Bump version in DB to simulate concurrent modification
	db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 999)

	// Simulate the transactional completion with stale task (version=1, DB has 999)
	task.NextDueDate = nil
	task.InProgress = false

	completion := &models.TaskCompletion{
		TaskID:        task.ID,
		CompletedByID: user.ID,
		CompletedAt:   time.Now().UTC(),
		Notes:         "Should be rolled back",
	}

	txErr := taskRepo.DB().Transaction(func(tx *gorm.DB) error {
		if err := taskRepo.CreateCompletionTx(tx, completion); err != nil {
			return err
		}
		if err := taskRepo.UpdateTx(tx, task); err != nil {
			return err
		}
		return nil
	})

	require.Error(t, txErr, "Transaction should fail due to version conflict")
	assert.ErrorIs(t, txErr, repositories.ErrVersionConflict, "Error should be ErrVersionConflict")

	// Verify the completion was rolled back (transaction atomicity)
	var count int64
	db.Model(&models.TaskCompletion{}).Where("task_id = ?", task.ID).Count(&count)
	assert.Equal(t, int64(0), count, "Completion should be rolled back when version conflict occurs")
}

func TestTaskService_OptimisticLocking_CancelWithStaleVersion(t *testing.T) {
	// Cancel task with stale version -> verify conflict error returned.
	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")

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Cancel Conflict Test",
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Cancel with correct version -> success (version becomes 2)
	err = taskRepo.Cancel(task.ID, 1)
	require.NoError(t, err)

	// Uncancel with correct version 2 -> success (version becomes 3)
	err = taskRepo.Uncancel(task.ID, 2)
	require.NoError(t, err)

	// Try cancel with stale version 2 (current is 3) -> conflict
	err = taskRepo.Cancel(task.ID, 2)
	require.Error(t, err)
	assert.ErrorIs(t, err, repositories.ErrVersionConflict, "Cancel with stale version should return ErrVersionConflict")
}

func TestTaskService_OptimisticLocking_MarkInProgressWithStaleVersion(t *testing.T) {
	// MarkInProgress with stale version -> verify conflict.
	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")

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "InProgress Conflict Test",
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	// Bump version to simulate concurrent modification
	db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 50)

	err = taskRepo.MarkInProgress(task.ID, 1)
	require.Error(t, err)
	assert.ErrorIs(t, err, repositories.ErrVersionConflict)
}

func TestTaskService_OptimisticLocking_ArchiveWithStaleVersion(t *testing.T) {
	// Archive with stale version -> verify conflict.
	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")

	task := &models.Task{
		ResidenceID: residence.ID,
		CreatedByID: user.ID,
		Title:       "Archive Conflict Test",
		IsCancelled: false,
		IsArchived:  false,
		Version:     1,
	}
	err := db.Create(task).Error
	require.NoError(t, err)

	db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 50)

	err = taskRepo.Archive(task.ID, 1)
	require.Error(t, err)
	assert.ErrorIs(t, err, repositories.ErrVersionConflict)
}

func TestTaskService_OptimisticLocking_ServiceCancelConflictAndRecovery(t *testing.T) {
	// Test the repo-level conflict detection, then verify service cancel succeeds
	// when the version is correct.
	db := testutil.SetupTestDB(t)
	testutil.SeedLookupData(t, db)
	taskRepo := repositories.NewTaskRepository(db)
	residenceRepo := repositories.NewResidenceRepository(db)
	service := NewTaskService(taskRepo, residenceRepo)

	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, "Service Conflict Test")

	now := time.Now().UTC()

	// Verify repo-level conflict detection with stale version
	db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 999)
	err := taskRepo.Cancel(task.ID, 1)
	require.Error(t, err)
	assert.ErrorIs(t, err, repositories.ErrVersionConflict)

	// Reset version and verify service cancel succeeds with correct version
	db.Model(&models.Task{}).Where("id = ?", task.ID).Update("version", 1)
	cancelResp, err := service.CancelTask(task.ID, user.ID, now)
	require.NoError(t, err)
	assert.True(t, cancelResp.Data.IsCancelled, "Service cancel should succeed with correct version")
}
