package task

import (
	"testing"
	"time"

	"gorm.io/gorm"

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

var now = time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)

func timePtr(t time.Time) *time.Time { return &t }

func completedTask() *models.Task {
	return &models.Task{
		BaseModel:   models.BaseModel{ID: 1},
		Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
		// NextDueDate is nil → completed
	}
}

func overdueTask() *models.Task {
	past := now.AddDate(0, 0, -5)
	return &models.Task{
		BaseModel: models.BaseModel{ID: 2},
		DueDate:   &past,
	}
}

func dueSoonTask() *models.Task {
	soon := now.AddDate(0, 0, 10)
	return &models.Task{
		BaseModel: models.BaseModel{ID: 3},
		DueDate:   &soon,
	}
}

func activeTask() *models.Task {
	return &models.Task{
		BaseModel: models.BaseModel{ID: 4},
	}
}

// --- Predicate re-exports ---

func TestReExport_IsCompleted(t *testing.T) {
	if !IsCompleted(completedTask()) {
		t.Error("expected completed")
	}
	if IsCompleted(activeTask()) {
		t.Error("expected not completed")
	}
}

func TestReExport_IsOverdue(t *testing.T) {
	if !IsOverdue(overdueTask(), now) {
		t.Error("expected overdue")
	}
	if IsOverdue(dueSoonTask(), now) {
		t.Error("expected not overdue")
	}
}

func TestReExport_IsDueSoon(t *testing.T) {
	if !IsDueSoon(dueSoonTask(), now, 30) {
		t.Error("expected due soon")
	}
}

func TestReExport_IsActive(t *testing.T) {
	if !IsActive(activeTask()) {
		t.Error("expected active")
	}
	cancelled := &models.Task{IsCancelled: true}
	if IsActive(cancelled) {
		t.Error("cancelled should not be active")
	}
}

func TestReExport_IsArchived(t *testing.T) {
	archived := &models.Task{IsArchived: true}
	if !IsArchived(archived) {
		t.Error("expected archived")
	}
}

func TestReExport_IsCancelled(t *testing.T) {
	cancelled := &models.Task{IsCancelled: true}
	if !IsCancelled(cancelled) {
		t.Error("expected cancelled")
	}
}

func TestReExport_IsInProgress(t *testing.T) {
	ip := &models.Task{InProgress: true}
	if !IsInProgress(ip) {
		t.Error("expected in progress")
	}
}

func TestReExport_IsRecurring(t *testing.T) {
	days := 30
	freqID := uint(1)
	recurring := &models.Task{
		FrequencyID: &freqID,
		Frequency:   &models.TaskFrequency{Days: &days},
	}
	if !IsRecurring(recurring) {
		t.Error("expected recurring")
	}
	if IsRecurring(activeTask()) {
		t.Error("expected not recurring")
	}
}

func TestReExport_IsOneTime(t *testing.T) {
	if !IsOneTime(activeTask()) {
		t.Error("expected one-time")
	}
}

func TestReExport_HasCompletions(t *testing.T) {
	if !HasCompletions(completedTask()) {
		t.Error("expected has completions")
	}
	if HasCompletions(activeTask()) {
		t.Error("expected no completions")
	}
}

func TestReExport_GetCompletionCount(t *testing.T) {
	ct := completedTask()
	if GetCompletionCount(ct) != 1 {
		t.Errorf("count = %d, want 1", GetCompletionCount(ct))
	}
}

func TestReExport_EffectiveDate(t *testing.T) {
	task := overdueTask()
	ed := EffectiveDate(task)
	if ed == nil {
		t.Error("expected non-nil effective date")
	}
	// If NextDueDate is set, prefer it
	next := now.AddDate(0, 1, 0)
	task.NextDueDate = &next
	ed = EffectiveDate(task)
	if !ed.Equal(next) {
		t.Errorf("expected NextDueDate, got %v", ed)
	}
}

func TestReExport_IsUpcoming(t *testing.T) {
	far := now.AddDate(0, 6, 0)
	task := &models.Task{DueDate: &far}
	if !IsUpcoming(task, now, 30) {
		t.Error("expected upcoming")
	}
}

func TestReExport_CategorizeTask(t *testing.T) {
	col := CategorizeTask(overdueTask(), 30)
	if col != categorization.ColumnOverdue {
		t.Errorf("column = %v, want overdue", col)
	}
}

func TestReExport_DetermineKanbanColumn(t *testing.T) {
	col := DetermineKanbanColumn(overdueTask(), 30)
	if col == "" {
		t.Error("expected non-empty column string")
	}
}

func TestReExport_CategorizeTasksIntoColumns(t *testing.T) {
	tasks := []models.Task{*overdueTask(), *dueSoonTask(), *activeTask()}
	result := CategorizeTasksIntoColumns(tasks, 30)
	if result == nil {
		t.Error("expected non-nil result")
	}
}

func TestReExport_NewChain(t *testing.T) {
	chain := NewChain()
	if chain == nil {
		t.Error("expected non-nil chain")
	}
}

func TestReExport_Constants(t *testing.T) {
	if ColumnOverdue.String() != "overdue_tasks" {
		t.Errorf("ColumnOverdue = %q", ColumnOverdue.String())
	}
	if ColumnDueSoon.String() != "due_soon_tasks" {
		t.Errorf("ColumnDueSoon = %q", ColumnDueSoon.String())
	}
	if ColumnUpcoming.String() != "upcoming_tasks" {
		t.Errorf("ColumnUpcoming = %q", ColumnUpcoming.String())
	}
	if ColumnInProgress.String() != "in_progress_tasks" {
		t.Errorf("ColumnInProgress = %q", ColumnInProgress.String())
	}
	if ColumnCompleted.String() != "completed_tasks" {
		t.Errorf("ColumnCompleted = %q", ColumnCompleted.String())
	}
	if ColumnCancelled.String() != "cancelled_tasks" {
		t.Errorf("ColumnCancelled = %q", ColumnCancelled.String())
	}
}

// --- Scope re-exports (use SQLite in-memory DB) ---

func setupDB(t *testing.T) *gorm.DB {
	return testutil.SetupTestDB(t)
}

func seedTask(t *testing.T, db *gorm.DB, task *models.Task) {
	// Ensure we have a user and residence
	user := testutil.CreateTestUser(t, db, "testuser", "test@example.com", "password123")
	res := testutil.CreateTestResidence(t, db, user.ID, "Test House")
	task.CreatedByID = user.ID
	task.ResidenceID = res.ID
	task.Version = 1
	err := db.Create(task).Error
	if err != nil {
		t.Fatalf("failed to create task: %v", err)
	}
}

func TestReExport_ScopeActive_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "active"})
	var tasks []models.Task
	db.Scopes(ScopeActive).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeCancelled_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "cancelled", IsCancelled: true})
	var tasks []models.Task
	db.Scopes(ScopeCancelled).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeArchived_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "archived", IsArchived: true})
	var tasks []models.Task
	db.Scopes(ScopeArchived).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeInProgress_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "inprog", InProgress: true})
	var tasks []models.Task
	db.Scopes(ScopeInProgress).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeNotInProgress_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "not inprog"})
	var tasks []models.Task
	db.Scopes(ScopeNotInProgress).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeCompleted_DB(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u2", "u2@test.com", "password123")
	res := testutil.CreateTestResidence(t, db, user.ID, "House2")
	task := &models.Task{Title: "completed", CreatedByID: user.ID, ResidenceID: res.ID, Version: 1}
	db.Create(task)
	// Add a completion and ensure NextDueDate is nil
	db.Create(&models.TaskCompletion{TaskID: task.ID, CompletedByID: user.ID, CompletedAt: now})
	var tasks []models.Task
	db.Scopes(ScopeCompleted).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeNotCompleted_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "not completed"})
	var tasks []models.Task
	db.Scopes(ScopeNotCompleted).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeOverdue_DB(t *testing.T) {
	db := setupDB(t)
	past := now.AddDate(0, 0, -5)
	seedTask(t, db, &models.Task{Title: "overdue", DueDate: &past})
	var tasks []models.Task
	db.Scopes(ScopeOverdue(now)).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeDueSoon_DB(t *testing.T) {
	db := setupDB(t)
	soon := now.AddDate(0, 0, 5)
	seedTask(t, db, &models.Task{Title: "due soon", DueDate: &soon})
	var tasks []models.Task
	db.Scopes(ScopeDueSoon(now, 30)).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeUpcoming_DB(t *testing.T) {
	db := setupDB(t)
	far := now.AddDate(0, 6, 0)
	seedTask(t, db, &models.Task{Title: "upcoming", DueDate: &far})
	var tasks []models.Task
	db.Scopes(ScopeUpcoming(now, 30)).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeForResidence_DB(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u3", "u3@test.com", "password123")
	res := testutil.CreateTestResidence(t, db, user.ID, "House3")
	db.Create(&models.Task{Title: "t1", CreatedByID: user.ID, ResidenceID: res.ID, Version: 1})
	var tasks []models.Task
	db.Scopes(ScopeForResidence(res.ID)).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeForResidences_DB(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u4", "u4@test.com", "password123")
	res1 := testutil.CreateTestResidence(t, db, user.ID, "H1")
	res2 := testutil.CreateTestResidence(t, db, user.ID, "H2")
	db.Create(&models.Task{Title: "t1", CreatedByID: user.ID, ResidenceID: res1.ID, Version: 1})
	db.Create(&models.Task{Title: "t2", CreatedByID: user.ID, ResidenceID: res2.ID, Version: 1})
	var tasks []models.Task
	db.Scopes(ScopeForResidences([]uint{res1.ID, res2.ID})).Find(&tasks)
	if len(tasks) != 2 {
		t.Errorf("len = %d, want 2", len(tasks))
	}
}

func TestReExport_ScopeDueInRange_DB(t *testing.T) {
	db := setupDB(t)
	due := now.AddDate(0, 0, 3)
	seedTask(t, db, &models.Task{Title: "in range", DueDate: &due})
	var tasks []models.Task
	db.Scopes(ScopeDueInRange(now, now.AddDate(0, 0, 7))).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeHasDueDate_DB(t *testing.T) {
	db := setupDB(t)
	due := now.AddDate(0, 0, 1)
	seedTask(t, db, &models.Task{Title: "with due", DueDate: &due})
	var tasks []models.Task
	db.Scopes(ScopeHasDueDate).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeNoDueDate_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "no due"})
	var tasks []models.Task
	db.Scopes(ScopeNoDueDate).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeHasCompletions_DB(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u5", "u5@test.com", "password123")
	res := testutil.CreateTestResidence(t, db, user.ID, "H5")
	task := &models.Task{Title: "has comp", CreatedByID: user.ID, ResidenceID: res.ID, Version: 1}
	db.Create(task)
	db.Create(&models.TaskCompletion{TaskID: task.ID, CompletedByID: user.ID, CompletedAt: now})
	var tasks []models.Task
	db.Scopes(ScopeHasCompletions).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeNoCompletions_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "no comp"})
	var tasks []models.Task
	db.Scopes(ScopeNoCompletions).Find(&tasks)
	if len(tasks) != 1 {
		t.Errorf("len = %d, want 1", len(tasks))
	}
}

func TestReExport_ScopeOrderByDueDate_DB(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u6", "u6@test.com", "password123")
	res := testutil.CreateTestResidence(t, db, user.ID, "H6")
	d1 := now.AddDate(0, 0, 5)
	d2 := now.AddDate(0, 0, 1)
	db.Create(&models.Task{Title: "later", CreatedByID: user.ID, ResidenceID: res.ID, DueDate: &d1, Version: 1})
	db.Create(&models.Task{Title: "sooner", CreatedByID: user.ID, ResidenceID: res.ID, DueDate: &d2, Version: 1})
	var tasks []models.Task
	db.Scopes(ScopeOrderByDueDate).Find(&tasks)
	if len(tasks) != 2 {
		t.Fatalf("len = %d", len(tasks))
	}
}

func TestReExport_ScopeOrderByPriority_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "prio test"})
	var tasks []models.Task
	db.Scopes(ScopeOrderByPriority).Find(&tasks)
	if len(tasks) < 1 {
		t.Error("expected at least 1 task")
	}
}

func TestReExport_ScopeOrderByCreatedAt_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "created test"})
	var tasks []models.Task
	db.Scopes(ScopeOrderByCreatedAt).Find(&tasks)
	if len(tasks) < 1 {
		t.Error("expected at least 1 task")
	}
}

func TestReExport_ScopeKanbanOrder_DB(t *testing.T) {
	db := setupDB(t)
	seedTask(t, db, &models.Task{Title: "kanban test"})
	var tasks []models.Task
	db.Scopes(ScopeKanbanOrder).Find(&tasks)
	if len(tasks) < 1 {
		t.Error("expected at least 1 task")
	}
}
