package scopes_test

import (
	"testing"
	"time"

	"gorm.io/gorm"

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

// --- helpers ---

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

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

func createResidence(t *testing.T, db *gorm.DB) uint {
	user := testutil.CreateTestUser(t, db, "scope_user", "scope@example.com", "pass")
	r := testutil.CreateTestResidence(t, db, user.ID, "Scope Home")
	return r.ID
}

func createTask(t *testing.T, db *gorm.DB, task *models.Task) *models.Task {
	if err := db.Create(task).Error; err != nil {
		t.Fatalf("create task: %v", err)
	}
	return task
}

func createCompletion(t *testing.T, db *gorm.DB, taskID, userID uint) {
	c := &models.TaskCompletion{
		TaskID:        taskID,
		CompletedByID: userID,
		CompletedAt:   time.Now().UTC(),
	}
	if err := db.Create(c).Error; err != nil {
		t.Fatalf("create completion: %v", err)
	}
}

func queryCount(t *testing.T, db *gorm.DB, scopeFns ...func(*gorm.DB) *gorm.DB) int {
	var tasks []models.Task
	q := db.Model(&models.Task{})
	for _, fn := range scopeFns {
		q = q.Scopes(fn)
	}
	if err := q.Find(&tasks).Error; err != nil {
		t.Fatalf("query: %v", err)
	}
	return len(tasks)
}

// --- ScopeActive ---

func TestScopeActive(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u1", "u1@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R1")

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "active"})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "cancelled", IsCancelled: true})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "archived", IsArchived: true})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeActive)
	if got != 1 {
		t.Errorf("active = %d, want 1", got)
	}
}

// --- ScopeCancelled ---

func TestScopeCancelled(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u2", "u2@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R2")

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ok"})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "cancelled", IsCancelled: true})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeCancelled)
	if got != 1 {
		t.Errorf("cancelled = %d, want 1", got)
	}
}

// --- ScopeArchived ---

func TestScopeArchived(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u3", "u3@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R3")

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ok"})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "archived", IsArchived: true})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeArchived)
	if got != 1 {
		t.Errorf("archived = %d, want 1", got)
	}
}

// --- ScopeInProgress / ScopeNotInProgress ---

func TestScopeInProgress(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u4", "u4@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R4")

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ip", InProgress: true})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "not_ip"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeInProgress)
	if got != 1 {
		t.Errorf("in_progress = %d, want 1", got)
	}
}

func TestScopeNotInProgress(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u5", "u5@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R5")

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "ip", InProgress: true})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "not_ip"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNotInProgress)
	if got != 1 {
		t.Errorf("not_in_progress = %d, want 1", got)
	}
}

// --- ScopeCompleted / ScopeNotCompleted ---

func TestScopeCompleted(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u6", "u6@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R6")

	// Completed: NextDueDate nil + has completion
	completed := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "done"})
	createCompletion(t, db, completed.ID, user.ID)

	// Not completed: has NextDueDate (recurring)
	nextWeek := time.Now().AddDate(0, 0, 7)
	recurring := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "recurring", NextDueDate: &nextWeek})
	createCompletion(t, db, recurring.ID, user.ID)

	// Not completed: no completions
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "pending"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeCompleted)
	if got != 1 {
		t.Errorf("completed = %d, want 1", got)
	}
}

func TestScopeNotCompleted(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u7", "u7@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R7")

	completed := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "done"})
	createCompletion(t, db, completed.ID, user.ID)
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "pending"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNotCompleted)
	if got != 1 {
		t.Errorf("not_completed = %d, want 1", got)
	}
}

// --- ScopeOverdue ---

func TestScopeOverdue(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u8", "u8@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R8")
	now := time.Now().UTC()
	yesterday := now.AddDate(0, 0, -1)
	tomorrow := now.AddDate(0, 0, 1)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "overdue", NextDueDate: timePtr(yesterday)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "future", NextDueDate: timePtr(tomorrow)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "cancelled_overdue", NextDueDate: timePtr(yesterday), IsCancelled: true})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeOverdue(now))
	if got != 1 {
		t.Errorf("overdue = %d, want 1", got)
	}
}

// --- ScopeDueSoon ---

func TestScopeDueSoon(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u9", "u9@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R9")
	now := time.Now().UTC()
	in5Days := now.AddDate(0, 0, 5)
	in60Days := now.AddDate(0, 0, 60)
	yesterday := now.AddDate(0, 0, -1)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "due_soon", NextDueDate: timePtr(in5Days)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "far", NextDueDate: timePtr(in60Days)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "overdue", NextDueDate: timePtr(yesterday)})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeDueSoon(now, 30))
	if got != 1 {
		t.Errorf("due_soon = %d, want 1", got)
	}
}

// --- ScopeUpcoming ---

func TestScopeUpcoming(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u10", "u10@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R10")
	now := time.Now().UTC()
	in5Days := now.AddDate(0, 0, 5)
	in60Days := now.AddDate(0, 0, 60)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "due_soon", NextDueDate: timePtr(in5Days)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "upcoming", NextDueDate: timePtr(in60Days)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "no_date"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeUpcoming(now, 30))
	if got != 2 {
		t.Errorf("upcoming = %d, want 2", got)
	}
}

// --- ScopeForResidence / ScopeForResidences ---

func TestScopeForResidence(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u11", "u11@x.com", "p")
	r1 := testutil.CreateTestResidence(t, db, user.ID, "R11a")
	r2 := testutil.CreateTestResidence(t, db, user.ID, "R11b")

	createTask(t, db, &models.Task{ResidenceID: r1.ID, CreatedByID: user.ID, Title: "t1"})
	createTask(t, db, &models.Task{ResidenceID: r2.ID, CreatedByID: user.ID, Title: "t2"})

	got := queryCount(t, db, scopes.ScopeForResidence(r1.ID))
	if got != 1 {
		t.Errorf("for_residence = %d, want 1", got)
	}
}

func TestScopeForResidences(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u12", "u12@x.com", "p")
	r1 := testutil.CreateTestResidence(t, db, user.ID, "R12a")
	r2 := testutil.CreateTestResidence(t, db, user.ID, "R12b")
	r3 := testutil.CreateTestResidence(t, db, user.ID, "R12c")

	createTask(t, db, &models.Task{ResidenceID: r1.ID, CreatedByID: user.ID, Title: "t1"})
	createTask(t, db, &models.Task{ResidenceID: r2.ID, CreatedByID: user.ID, Title: "t2"})
	createTask(t, db, &models.Task{ResidenceID: r3.ID, CreatedByID: user.ID, Title: "t3"})

	got := queryCount(t, db, scopes.ScopeForResidences([]uint{r1.ID, r2.ID}))
	if got != 2 {
		t.Errorf("for_residences = %d, want 2", got)
	}
}

// --- ScopeDueInRange ---

func TestScopeDueInRange(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u13", "u13@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R13")
	now := time.Now().UTC()

	in3Days := now.AddDate(0, 0, 3)
	in10Days := now.AddDate(0, 0, 10)
	in20Days := now.AddDate(0, 0, 20)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "in_range", NextDueDate: timePtr(in10Days)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "before_range", NextDueDate: timePtr(in3Days)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "after_range", NextDueDate: timePtr(in20Days)})

	start := now.AddDate(0, 0, 5)
	end := now.AddDate(0, 0, 15)
	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeDueInRange(start, end))
	if got != 1 {
		t.Errorf("due_in_range = %d, want 1", got)
	}
}

// --- ScopeHasDueDate / ScopeNoDueDate ---

func TestScopeHasDueDate(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u14", "u14@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R14")
	tomorrow := time.Now().AddDate(0, 0, 1)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_date", NextDueDate: timePtr(tomorrow)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_due", DueDate: timePtr(tomorrow)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "no_date"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeHasDueDate)
	if got != 2 {
		t.Errorf("has_due_date = %d, want 2", got)
	}
}

func TestScopeNoDueDate(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u15", "u15@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R15")
	tomorrow := time.Now().AddDate(0, 0, 1)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_date", NextDueDate: timePtr(tomorrow)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "no_date"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNoDueDate)
	if got != 1 {
		t.Errorf("no_due_date = %d, want 1", got)
	}
}

// --- ScopeHasCompletions / ScopeNoCompletions ---

func TestScopeHasCompletions(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u16", "u16@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R16")

	withC := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_completion"})
	createCompletion(t, db, withC.ID, user.ID)
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "without_completion"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeHasCompletions)
	if got != 1 {
		t.Errorf("has_completions = %d, want 1", got)
	}
}

func TestScopeNoCompletions(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u17", "u17@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R17")

	withC := createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "with_completion"})
	createCompletion(t, db, withC.ID, user.ID)
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "without_completion"})

	got := queryCount(t, db, scopes.ScopeForResidence(r.ID), scopes.ScopeNoCompletions)
	if got != 1 {
		t.Errorf("no_completions = %d, want 1", got)
	}
}

// --- Ordering scopes ---

func TestScopeOrderByDueDate(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u18", "u18@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R18")

	in10 := time.Now().AddDate(0, 0, 10)
	in5 := time.Now().AddDate(0, 0, 5)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "later", NextDueDate: timePtr(in10)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "sooner", NextDueDate: timePtr(in5)})

	var tasks []models.Task
	db.Model(&models.Task{}).Scopes(scopes.ScopeForResidence(r.ID), scopes.ScopeOrderByDueDate).Find(&tasks)

	if len(tasks) != 2 {
		t.Fatalf("len = %d, want 2", len(tasks))
	}
	// First should have the earlier date (sooner)
	if tasks[0].Title != "sooner" {
		t.Errorf("first task = %q, want sooner", tasks[0].Title)
	}
}

func TestScopeKanbanOrder(t *testing.T) {
	db := setupDB(t)
	user := testutil.CreateTestUser(t, db, "u19", "u19@x.com", "p")
	r := testutil.CreateTestResidence(t, db, user.ID, "R19")

	in10 := time.Now().AddDate(0, 0, 10)
	in5 := time.Now().AddDate(0, 0, 5)

	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "later", NextDueDate: timePtr(in10)})
	createTask(t, db, &models.Task{ResidenceID: r.ID, CreatedByID: user.ID, Title: "sooner", NextDueDate: timePtr(in5)})

	var tasks []models.Task
	db.Model(&models.Task{}).Scopes(scopes.ScopeForResidence(r.ID), scopes.ScopeKanbanOrder).Find(&tasks)

	if len(tasks) != 2 {
		t.Fatalf("len = %d, want 2", len(tasks))
	}
}
