package responses

import (
	"fmt"
	"testing"
	"time"

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

// --- helpers ---

func timePtr(t time.Time) *time.Time { return &t }
func uintPtr(v uint) *uint           { return &v }
func intPtr(v int) *int              { return &v }
func strPtr(v string) *string        { return &v }
func float64Ptr(v float64) *float64  { return &v }

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

func makeUser() *models.User {
	return &models.User{
		ID:         1,
		Username:   "john",
		Email:      "john@example.com",
		FirstName:  "John",
		LastName:   "Doe",
		IsActive:   true,
		DateJoined: fixedNow,
		LastLogin:  timePtr(fixedNow),
		Profile: &models.UserProfile{
			BaseModel: models.BaseModel{ID: 10},
			UserID:    1,
			Verified:  true,
			Bio:       "hello",
		},
	}
}

func makeUserNoProfile() *models.User {
	u := makeUser()
	u.Profile = nil
	return u
}

// ==================== auth.go ====================

func TestNewUserResponse_AllFields(t *testing.T) {
	u := makeUser()
	resp := NewUserResponse(u)
	if resp.ID != 1 {
		t.Errorf("ID = %d, want 1", resp.ID)
	}
	if resp.Username != "john" {
		t.Errorf("Username = %q", resp.Username)
	}
	if !resp.Verified {
		t.Error("Verified should be true when profile is verified")
	}
	if resp.LastLogin == nil {
		t.Error("LastLogin should not be nil")
	}
}

func TestNewUserResponse_NilProfile(t *testing.T) {
	u := makeUserNoProfile()
	resp := NewUserResponse(u)
	if resp.Verified {
		t.Error("Verified should be false when profile is nil")
	}
}

func TestNewUserProfileResponse_Nil(t *testing.T) {
	resp := NewUserProfileResponse(nil)
	if resp != nil {
		t.Error("expected nil for nil profile")
	}
}

func TestNewUserProfileResponse_Valid(t *testing.T) {
	p := &models.UserProfile{
		BaseModel: models.BaseModel{ID: 5},
		UserID:    1,
		Verified:  true,
		Bio:       "bio",
	}
	resp := NewUserProfileResponse(p)
	if resp == nil {
		t.Fatal("expected non-nil")
	}
	if resp.ID != 5 || resp.UserID != 1 || !resp.Verified || resp.Bio != "bio" {
		t.Errorf("unexpected response: %+v", resp)
	}
}

func TestNewCurrentUserResponse(t *testing.T) {
	u := makeUser()
	resp := NewCurrentUserResponse(u, "apple")
	if resp.AuthProvider != "apple" {
		t.Errorf("AuthProvider = %q, want apple", resp.AuthProvider)
	}
	if resp.Profile == nil {
		t.Error("Profile should not be nil")
	}
	if resp.ID != 1 {
		t.Errorf("ID = %d, want 1", resp.ID)
	}
}

func TestNewLoginResponse(t *testing.T) {
	u := makeUser()
	resp := NewLoginResponse("tok123", u)
	if resp.Token != "tok123" {
		t.Errorf("Token = %q", resp.Token)
	}
	if resp.User.ID != 1 {
		t.Errorf("User.ID = %d", resp.User.ID)
	}
}

func TestNewRegisterResponse(t *testing.T) {
	u := makeUser()
	resp := NewRegisterResponse("tok456", u)
	if resp.Token != "tok456" {
		t.Errorf("Token = %q", resp.Token)
	}
	if resp.Message == "" {
		t.Error("Message should not be empty")
	}
}

func TestNewAppleSignInResponse(t *testing.T) {
	u := makeUser()
	resp := NewAppleSignInResponse("atok", u, true)
	if !resp.IsNewUser {
		t.Error("IsNewUser should be true")
	}
	if resp.Token != "atok" {
		t.Errorf("Token = %q", resp.Token)
	}
}

func TestNewGoogleSignInResponse(t *testing.T) {
	u := makeUser()
	resp := NewGoogleSignInResponse("gtok", u, false)
	if resp.IsNewUser {
		t.Error("IsNewUser should be false")
	}
	if resp.Token != "gtok" {
		t.Errorf("Token = %q", resp.Token)
	}
}

// ==================== task.go ====================

func makeTask() *models.Task {
	due := time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
	catID := uint(1)
	priID := uint(2)
	freqID := uint(3)
	return &models.Task{
		BaseModel:   models.BaseModel{ID: 100, CreatedAt: fixedNow, UpdatedAt: fixedNow},
		ResidenceID: 10,
		CreatedByID: 1,
		CreatedBy:   *makeUser(),
		Title:       "Fix roof",
		Description: "Repair leak",
		CategoryID:  &catID,
		Category:    &models.TaskCategory{BaseModel: models.BaseModel{ID: catID}, Name: "Exterior", Icon: "roof", Color: "#FF0000", DisplayOrder: 1},
		PriorityID:  &priID,
		Priority:    &models.TaskPriority{BaseModel: models.BaseModel{ID: priID}, Name: "High", Level: 3, Color: "#FF0000", DisplayOrder: 1},
		FrequencyID: &freqID,
		Frequency:   &models.TaskFrequency{BaseModel: models.BaseModel{ID: freqID}, Name: "Monthly", Days: intPtr(30), DisplayOrder: 1},
		DueDate:     &due,
	}
}

func TestNewTaskResponse_BasicFields(t *testing.T) {
	task := makeTask()
	resp := NewTaskResponseWithTime(task, 30, fixedNow)
	if resp.ID != 100 {
		t.Errorf("ID = %d", resp.ID)
	}
	if resp.Title != "Fix roof" {
		t.Errorf("Title = %q", resp.Title)
	}
	if resp.CreatedBy == nil {
		t.Error("CreatedBy should not be nil")
	}
	if resp.Category == nil {
		t.Error("Category should not be nil")
	}
	if resp.Priority == nil {
		t.Error("Priority should not be nil")
	}
	if resp.Frequency == nil {
		t.Error("Frequency should not be nil")
	}
	if resp.KanbanColumn == "" {
		t.Error("KanbanColumn should not be empty")
	}
}

func TestNewTaskResponse_NilAssociations(t *testing.T) {
	task := &models.Task{
		BaseModel:   models.BaseModel{ID: 200},
		ResidenceID: 10,
		CreatedByID: 1,
		Title:       "Simple task",
	}
	resp := NewTaskResponseWithTime(task, 30, fixedNow)
	if resp.CreatedBy != nil {
		t.Error("CreatedBy should be nil when CreatedBy.ID is 0")
	}
	if resp.Category != nil {
		t.Error("Category should be nil")
	}
	if resp.Priority != nil {
		t.Error("Priority should be nil")
	}
	if resp.Frequency != nil {
		t.Error("Frequency should be nil")
	}
	if resp.AssignedTo != nil {
		t.Error("AssignedTo should be nil")
	}
}

func TestNewTaskResponse_WithCompletions(t *testing.T) {
	task := makeTask()
	task.Completions = []models.TaskCompletion{
		{BaseModel: models.BaseModel{ID: 1}, TaskID: 100, CompletedAt: fixedNow, CompletedByID: 1},
		{BaseModel: models.BaseModel{ID: 2}, TaskID: 100, CompletedAt: fixedNow, CompletedByID: 1},
	}
	resp := NewTaskResponseWithTime(task, 30, fixedNow)
	if resp.CompletionCount != 2 {
		t.Errorf("CompletionCount = %d, want 2", resp.CompletionCount)
	}
}

func TestNewTaskResponseWithTime_KanbanColumn(t *testing.T) {
	task := makeTask()
	// due date is July 1, now is June 15 → 16 days away → due_soon (within 30 days)
	resp := NewTaskResponseWithTime(task, 30, fixedNow)
	if resp.KanbanColumn == "" {
		t.Error("KanbanColumn should be set")
	}
}

func TestNewTaskListResponse(t *testing.T) {
	tasks := []models.Task{
		{BaseModel: models.BaseModel{ID: 1}, Title: "A"},
		{BaseModel: models.BaseModel{ID: 2}, Title: "B"},
	}
	results := NewTaskListResponse(tasks)
	if len(results) != 2 {
		t.Errorf("len = %d, want 2", len(results))
	}
}

func TestNewTaskListResponse_Empty(t *testing.T) {
	results := NewTaskListResponse([]models.Task{})
	if len(results) != 0 {
		t.Errorf("len = %d, want 0", len(results))
	}
}

func TestNewTaskCompletionResponse_WithImages(t *testing.T) {
	c := &models.TaskCompletion{
		BaseModel:     models.BaseModel{ID: 50},
		TaskID:        100,
		CompletedByID: 1,
		CompletedBy:   *makeUser(),
		CompletedAt:   fixedNow,
		Notes:         "done",
		Images: []models.TaskCompletionImage{
			{BaseModel: models.BaseModel{ID: 1}, ImageURL: "http://img1.jpg", Caption: "before"},
			{BaseModel: models.BaseModel{ID: 2}, ImageURL: "http://img2.jpg", Caption: "after"},
		},
	}
	resp := NewTaskCompletionResponse(c)
	if resp.CompletedBy == nil {
		t.Error("CompletedBy should not be nil")
	}
	if len(resp.Images) != 2 {
		t.Errorf("Images len = %d, want 2", len(resp.Images))
	}
	if resp.Images[0].MediaURL != "/api/media/completion-image/1" {
		t.Errorf("MediaURL = %q", resp.Images[0].MediaURL)
	}
}

func TestNewTaskCompletionResponse_EmptyImages(t *testing.T) {
	c := &models.TaskCompletion{
		BaseModel:     models.BaseModel{ID: 51},
		TaskID:        100,
		CompletedByID: 1,
		CompletedAt:   fixedNow,
	}
	resp := NewTaskCompletionResponse(c)
	if resp.Images == nil {
		t.Error("Images should be empty slice, not nil")
	}
	if len(resp.Images) != 0 {
		t.Errorf("Images len = %d, want 0", len(resp.Images))
	}
}

func TestNewKanbanBoardResponse(t *testing.T) {
	board := &models.KanbanBoard{
		Columns: []models.KanbanColumn{
			{
				Name:        "overdue",
				DisplayName: "Overdue",
				Color:       "#FF0000",
				Tasks:       []models.Task{{BaseModel: models.BaseModel{ID: 1}, Title: "A"}},
				Count:       1,
			},
		},
		DaysThreshold: 30,
	}
	resp := NewKanbanBoardResponse(board, 10, fixedNow)
	if len(resp.Columns) != 1 {
		t.Fatalf("Columns len = %d", len(resp.Columns))
	}
	if resp.ResidenceID != "10" {
		t.Errorf("ResidenceID = %q, want '10'", resp.ResidenceID)
	}
	if resp.Columns[0].Count != 1 {
		t.Errorf("Count = %d", resp.Columns[0].Count)
	}
}

func TestNewKanbanBoardResponseForAll(t *testing.T) {
	board := &models.KanbanBoard{
		Columns:       []models.KanbanColumn{},
		DaysThreshold: 30,
	}
	resp := NewKanbanBoardResponseForAll(board, fixedNow)
	if resp.ResidenceID != "all" {
		t.Errorf("ResidenceID = %q, want 'all'", resp.ResidenceID)
	}
}

func TestDetermineKanbanColumn_Delegates(t *testing.T) {
	task := &models.Task{
		BaseModel: models.BaseModel{ID: 1},
		Title:     "test",
	}
	col := DetermineKanbanColumn(task, 30)
	if col == "" {
		t.Error("expected non-empty column")
	}
}

func TestNewTaskCompletionWithTaskResponse(t *testing.T) {
	c := &models.TaskCompletion{
		BaseModel:     models.BaseModel{ID: 1},
		TaskID:        100,
		CompletedByID: 1,
		CompletedAt:   fixedNow,
	}
	task := makeTask()
	resp := NewTaskCompletionWithTaskResponseWithTime(c, task, 30, fixedNow)
	if resp.Task == nil {
		t.Error("Task should not be nil")
	}
	if resp.Task.ID != 100 {
		t.Errorf("Task.ID = %d", resp.Task.ID)
	}
}

func TestNewTaskCompletionWithTaskResponse_NilTask(t *testing.T) {
	c := &models.TaskCompletion{
		BaseModel:     models.BaseModel{ID: 1},
		TaskID:        100,
		CompletedByID: 1,
		CompletedAt:   fixedNow,
	}
	resp := NewTaskCompletionWithTaskResponseWithTime(c, nil, 30, fixedNow)
	if resp.Task != nil {
		t.Error("Task should be nil")
	}
}

func TestNewTaskCompletionListResponse(t *testing.T) {
	completions := []models.TaskCompletion{
		{BaseModel: models.BaseModel{ID: 1}, TaskID: 100, CompletedAt: fixedNow, CompletedByID: 1},
	}
	results := NewTaskCompletionListResponse(completions)
	if len(results) != 1 {
		t.Errorf("len = %d", len(results))
	}
}

func TestNewTaskCategoryResponse_Nil(t *testing.T) {
	if NewTaskCategoryResponse(nil) != nil {
		t.Error("expected nil")
	}
}

func TestNewTaskPriorityResponse_Nil(t *testing.T) {
	if NewTaskPriorityResponse(nil) != nil {
		t.Error("expected nil")
	}
}

func TestNewTaskFrequencyResponse_Nil(t *testing.T) {
	if NewTaskFrequencyResponse(nil) != nil {
		t.Error("expected nil")
	}
}

func TestNewTaskUserResponse_Nil(t *testing.T) {
	if NewTaskUserResponse(nil) != nil {
		t.Error("expected nil")
	}
}

// ==================== contractor.go ====================

func makeContractor() *models.Contractor {
	resID := uint(10)
	return &models.Contractor{
		BaseModel:     models.BaseModel{ID: 5, CreatedAt: fixedNow, UpdatedAt: fixedNow},
		ResidenceID:   &resID,
		CreatedByID:   1,
		CreatedBy:     *makeUser(),
		Name:          "Bob's Plumbing",
		Company:       "Bob Co",
		Phone:         "555-1234",
		Email:         "bob@plumb.com",
		Rating:        float64Ptr(4.5),
		IsFavorite:    true,
		IsActive:      true,
		Specialties: []models.ContractorSpecialty{
			{BaseModel: models.BaseModel{ID: 1}, Name: "Plumbing", Icon: "wrench", DisplayOrder: 1},
		},
		Tasks: []models.Task{{BaseModel: models.BaseModel{ID: 1}}, {BaseModel: models.BaseModel{ID: 2}}},
	}
}

func TestNewContractorResponse_BasicFields(t *testing.T) {
	c := makeContractor()
	resp := NewContractorResponse(c)
	if resp.ID != 5 {
		t.Errorf("ID = %d", resp.ID)
	}
	if resp.Name != "Bob's Plumbing" {
		t.Errorf("Name = %q", resp.Name)
	}
	if resp.AddedBy != 1 {
		t.Errorf("AddedBy = %d, want 1", resp.AddedBy)
	}
	if resp.CreatedBy == nil {
		t.Error("CreatedBy should not be nil")
	}
	if resp.TaskCount != 2 {
		t.Errorf("TaskCount = %d, want 2", resp.TaskCount)
	}
}

func TestNewContractorResponse_WithSpecialties(t *testing.T) {
	c := makeContractor()
	resp := NewContractorResponse(c)
	if len(resp.Specialties) != 1 {
		t.Fatalf("Specialties len = %d", len(resp.Specialties))
	}
	if resp.Specialties[0].Name != "Plumbing" {
		t.Errorf("Specialty name = %q", resp.Specialties[0].Name)
	}
}

func TestNewContractorListResponse(t *testing.T) {
	contractors := []models.Contractor{*makeContractor()}
	results := NewContractorListResponse(contractors)
	if len(results) != 1 {
		t.Errorf("len = %d", len(results))
	}
}

func TestNewContractorUserResponse_Nil(t *testing.T) {
	if NewContractorUserResponse(nil) != nil {
		t.Error("expected nil")
	}
}

func TestNewContractorSpecialtyResponse(t *testing.T) {
	s := &models.ContractorSpecialty{
		BaseModel:    models.BaseModel{ID: 1},
		Name:         "Electrical",
		Description:  "Electrical work",
		Icon:         "bolt",
		DisplayOrder: 2,
	}
	resp := NewContractorSpecialtyResponse(s)
	if resp.Name != "Electrical" || resp.Icon != "bolt" {
		t.Errorf("unexpected: %+v", resp)
	}
}

// ==================== document.go ====================

func makeDocument() *models.Document {
	price := decimal.NewFromFloat(99.99)
	return &models.Document{
		BaseModel:   models.BaseModel{ID: 20, CreatedAt: fixedNow, UpdatedAt: fixedNow},
		ResidenceID: 10,
		CreatedByID: 1,
		CreatedBy:   *makeUser(),
		Title:       "Warranty",
		Description: "Roof warranty",
		DocumentType: "warranty",
		FileName:    "warranty.pdf",
		FileSize:    func() *int64 { v := int64(1024); return &v }(),
		MimeType:    "application/pdf",
		PurchasePrice: &price,
		IsActive:    true,
		Images: []models.DocumentImage{
			{BaseModel: models.BaseModel{ID: 1}, ImageURL: "http://img.jpg", Caption: "page 1"},
		},
	}
}

func TestNewDocumentResponse_MediaURL(t *testing.T) {
	d := makeDocument()
	resp := NewDocumentResponse(d)
	want := fmt.Sprintf("/api/media/document/%d", d.ID)
	if resp.MediaURL != want {
		t.Errorf("MediaURL = %q, want %q", resp.MediaURL, want)
	}
	if resp.Residence != resp.ResidenceID {
		t.Error("Residence alias should equal ResidenceID")
	}
}

func TestNewDocumentResponse_WithImages(t *testing.T) {
	d := makeDocument()
	resp := NewDocumentResponse(d)
	if len(resp.Images) != 1 {
		t.Fatalf("Images len = %d", len(resp.Images))
	}
	if resp.Images[0].MediaURL != "/api/media/document-image/1" {
		t.Errorf("Image MediaURL = %q", resp.Images[0].MediaURL)
	}
}

func TestNewDocumentResponse_EmptyImageURL(t *testing.T) {
	d := makeDocument()
	d.Images = []models.DocumentImage{
		{BaseModel: models.BaseModel{ID: 5}, ImageURL: "", Caption: "missing"},
	}
	resp := NewDocumentResponse(d)
	if resp.Images[0].Error != "image source URL is missing" {
		t.Errorf("Error = %q", resp.Images[0].Error)
	}
}

func TestNewDocumentListResponse(t *testing.T) {
	docs := []models.Document{*makeDocument()}
	results := NewDocumentListResponse(docs)
	if len(results) != 1 {
		t.Errorf("len = %d", len(results))
	}
}

func TestNewDocumentUserResponse_Nil(t *testing.T) {
	if NewDocumentUserResponse(nil) != nil {
		t.Error("expected nil")
	}
}

// ==================== residence.go ====================

func makeResidence() *models.Residence {
	propTypeID := uint(1)
	return &models.Residence{
		BaseModel:      models.BaseModel{ID: 10, CreatedAt: fixedNow, UpdatedAt: fixedNow},
		OwnerID:        1,
		Owner:          *makeUser(),
		Name:           "My House",
		PropertyTypeID: &propTypeID,
		PropertyType:   &models.ResidenceType{BaseModel: models.BaseModel{ID: 1}, Name: "House"},
		StreetAddress:  "123 Main St",
		City:           "Springfield",
		StateProvince:  "IL",
		PostalCode:     "62701",
		Country:        "USA",
		Bedrooms:       intPtr(3),
		IsPrimary:      true,
		IsActive:       true,
		HasPool:        true,
		HeatingType:    strPtr("central"),
		Users: []models.User{
			{ID: 1, Username: "john", Email: "john@example.com"},
			{ID: 2, Username: "jane", Email: "jane@example.com"},
		},
	}
}

func TestNewResidenceResponse_AllFields(t *testing.T) {
	r := makeResidence()
	resp := NewResidenceResponse(r)
	if resp.ID != 10 {
		t.Errorf("ID = %d", resp.ID)
	}
	if resp.Name != "My House" {
		t.Errorf("Name = %q", resp.Name)
	}
	if resp.Owner == nil {
		t.Error("Owner should not be nil")
	}
	if resp.PropertyType == nil {
		t.Error("PropertyType should not be nil")
	}
	if !resp.HasPool {
		t.Error("HasPool should be true")
	}
	if resp.HeatingType == nil || *resp.HeatingType != "central" {
		t.Error("HeatingType should be 'central'")
	}
}

func TestNewResidenceResponse_WithUsers(t *testing.T) {
	r := makeResidence()
	resp := NewResidenceResponse(r)
	if len(resp.Users) != 2 {
		t.Errorf("Users len = %d, want 2", len(resp.Users))
	}
}

func TestNewResidenceResponse_NoUsers(t *testing.T) {
	r := makeResidence()
	r.Users = nil
	resp := NewResidenceResponse(r)
	if resp.Users == nil {
		t.Error("Users should be empty slice, not nil")
	}
	if len(resp.Users) != 0 {
		t.Errorf("Users len = %d, want 0", len(resp.Users))
	}
}

func TestNewResidenceListResponse(t *testing.T) {
	residences := []models.Residence{*makeResidence()}
	results := NewResidenceListResponse(residences)
	if len(results) != 1 {
		t.Errorf("len = %d", len(results))
	}
}

func TestNewResidenceUserResponse_Nil(t *testing.T) {
	if NewResidenceUserResponse(nil) != nil {
		t.Error("expected nil")
	}
}

func TestNewResidenceTypeResponse_Nil(t *testing.T) {
	if NewResidenceTypeResponse(nil) != nil {
		t.Error("expected nil")
	}
}

func TestNewShareCodeResponse(t *testing.T) {
	sc := &models.ResidenceShareCode{
		BaseModel:   models.BaseModel{ID: 1, CreatedAt: fixedNow},
		Code:        "ABC123",
		ResidenceID: 10,
		CreatedByID: 1,
		IsActive:    true,
		ExpiresAt:   timePtr(fixedNow.Add(24 * time.Hour)),
	}
	resp := NewShareCodeResponse(sc)
	if resp.Code != "ABC123" {
		t.Errorf("Code = %q", resp.Code)
	}
	if resp.ResidenceID != 10 {
		t.Errorf("ResidenceID = %d", resp.ResidenceID)
	}
}

// ==================== task_template.go ====================

func TestParseTags_Empty(t *testing.T) {
	result := parseTags("")
	if len(result) != 0 {
		t.Errorf("len = %d, want 0", len(result))
	}
}

func TestParseTags_Multiple(t *testing.T) {
	result := parseTags("plumbing,electrical,roofing")
	if len(result) != 3 {
		t.Errorf("len = %d, want 3", len(result))
	}
	if result[0] != "plumbing" || result[1] != "electrical" || result[2] != "roofing" {
		t.Errorf("unexpected tags: %v", result)
	}
}

func TestParseTags_Whitespace(t *testing.T) {
	result := parseTags("  plumbing , , electrical  ")
	if len(result) != 2 {
		t.Errorf("len = %d, want 2 (should skip empty after trim)", len(result))
	}
	if result[0] != "plumbing" || result[1] != "electrical" {
		t.Errorf("unexpected tags: %v", result)
	}
}

func makeTemplate(catID *uint, cat *models.TaskCategory) models.TaskTemplate {
	return models.TaskTemplate{
		BaseModel:    models.BaseModel{ID: 1, CreatedAt: fixedNow, UpdatedAt: fixedNow},
		Title:        "Clean Gutters",
		Description:  "Remove debris",
		CategoryID:   catID,
		Category:     cat,
		IconIOS:      "leaf",
		IconAndroid:  "leaf_android",
		Tags:         "exterior,seasonal",
		DisplayOrder: 1,
		IsActive:     true,
	}
}

func TestNewTaskTemplateResponse(t *testing.T) {
	catID := uint(1)
	cat := &models.TaskCategory{BaseModel: models.BaseModel{ID: 1}, Name: "Exterior"}
	tmpl := makeTemplate(&catID, cat)
	resp := NewTaskTemplateResponse(&tmpl)
	if resp.Title != "Clean Gutters" {
		t.Errorf("Title = %q", resp.Title)
	}
	if len(resp.Tags) != 2 {
		t.Errorf("Tags len = %d", len(resp.Tags))
	}
	if resp.Category == nil {
		t.Error("Category should not be nil")
	}
}

func TestNewTaskTemplatesGroupedResponse_Grouping(t *testing.T) {
	catID := uint(1)
	cat := &models.TaskCategory{BaseModel: models.BaseModel{ID: 1}, Name: "Exterior"}
	templates := []models.TaskTemplate{
		makeTemplate(&catID, cat),
		makeTemplate(&catID, cat),
	}
	resp := NewTaskTemplatesGroupedResponse(templates)
	if len(resp.Categories) != 1 {
		t.Fatalf("Categories len = %d, want 1", len(resp.Categories))
	}
	if resp.Categories[0].CategoryName != "Exterior" {
		t.Errorf("CategoryName = %q", resp.Categories[0].CategoryName)
	}
	if resp.Categories[0].Count != 2 {
		t.Errorf("Count = %d, want 2", resp.Categories[0].Count)
	}
	if resp.TotalCount != 2 {
		t.Errorf("TotalCount = %d, want 2", resp.TotalCount)
	}
}

func TestNewTaskTemplatesGroupedResponse_Uncategorized(t *testing.T) {
	tmpl := makeTemplate(nil, nil)
	resp := NewTaskTemplatesGroupedResponse([]models.TaskTemplate{tmpl})
	if len(resp.Categories) != 1 {
		t.Fatalf("Categories len = %d", len(resp.Categories))
	}
	if resp.Categories[0].CategoryName != "Uncategorized" {
		t.Errorf("CategoryName = %q", resp.Categories[0].CategoryName)
	}
}

func TestNewTaskTemplateListResponse(t *testing.T) {
	templates := []models.TaskTemplate{makeTemplate(nil, nil)}
	results := NewTaskTemplateListResponse(templates)
	if len(results) != 1 {
		t.Errorf("len = %d", len(results))
	}
}

// ==================== DetermineKanbanColumnWithTime ====================

func TestDetermineKanbanColumnWithTime(t *testing.T) {
	task := makeTask()
	col := DetermineKanbanColumnWithTime(task, 30, fixedNow)
	if col == "" {
		t.Error("expected non-empty column")
	}
}

// ==================== NewTaskResponse uses NewTaskResponseWithThreshold ====================

func TestNewTaskResponse_UsesDefault30(t *testing.T) {
	task := makeTask()
	resp := NewTaskResponse(task)
	if resp.ID != 100 {
		t.Errorf("ID = %d", resp.ID)
	}
	// Just verify it doesn't panic and produces a response
}

// ==================== NewTaskCompletionWithTaskResponse UTC variant ====================

func TestNewTaskCompletionWithTaskResponse_UTC(t *testing.T) {
	c := &models.TaskCompletion{
		BaseModel:     models.BaseModel{ID: 1},
		TaskID:        100,
		CompletedByID: 1,
		CompletedAt:   fixedNow,
	}
	task := makeTask()
	resp := NewTaskCompletionWithTaskResponse(c, task, 30)
	if resp.Task == nil {
		t.Error("Task should not be nil")
	}
}
