package predicates_test

import (
	"testing"
	"time"

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

// Helper to create a time pointer
func timePtr(t time.Time) *time.Time {
	return &t
}

func TestIsCompleted(t *testing.T) {
	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name: "completed: NextDueDate nil with completions",
			task: &models.Task{
				NextDueDate: nil,
				Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
			},
			expected: true,
		},
		{
			name: "not completed: NextDueDate set with completions",
			task: &models.Task{
				NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)),
				Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
			},
			expected: false,
		},
		{
			name: "not completed: NextDueDate nil without completions",
			task: &models.Task{
				NextDueDate: nil,
				Completions: []models.TaskCompletion{},
			},
			expected: false,
		},
		{
			name: "not completed: NextDueDate set without completions",
			task: &models.Task{
				NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)),
				Completions: []models.TaskCompletion{},
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsCompleted(tt.task)
			if result != tt.expected {
				t.Errorf("IsCompleted() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsActive(t *testing.T) {
	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name:     "active: not cancelled, not archived",
			task:     &models.Task{IsCancelled: false, IsArchived: false},
			expected: true,
		},
		{
			name:     "not active: cancelled",
			task:     &models.Task{IsCancelled: true, IsArchived: false},
			expected: false,
		},
		{
			name:     "not active: archived",
			task:     &models.Task{IsCancelled: false, IsArchived: true},
			expected: false,
		},
		{
			name:     "not active: both cancelled and archived",
			task:     &models.Task{IsCancelled: true, IsArchived: true},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsActive(tt.task)
			if result != tt.expected {
				t.Errorf("IsActive() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsInProgress(t *testing.T) {
	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name:     "in progress: InProgress is true",
			task:     &models.Task{InProgress: true},
			expected: true,
		},
		{
			name:     "not in progress: InProgress is false",
			task:     &models.Task{InProgress: false},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsInProgress(tt.task)
			if result != tt.expected {
				t.Errorf("IsInProgress() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestEffectiveDate(t *testing.T) {
	now := time.Now()
	nextWeek := now.AddDate(0, 0, 7)
	nextMonth := now.AddDate(0, 1, 0)

	tests := []struct {
		name     string
		task     *models.Task
		expected *time.Time
	}{
		{
			name: "prefers NextDueDate when both set",
			task: &models.Task{
				NextDueDate: timePtr(nextWeek),
				DueDate:     timePtr(nextMonth),
			},
			expected: timePtr(nextWeek),
		},
		{
			name: "falls back to DueDate when NextDueDate nil",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     timePtr(nextMonth),
			},
			expected: timePtr(nextMonth),
		},
		{
			name: "returns nil when both nil",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     nil,
			},
			expected: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.EffectiveDate(tt.task)
			if tt.expected == nil {
				if result != nil {
					t.Errorf("EffectiveDate() = %v, expected nil", result)
				}
			} else {
				if result == nil {
					t.Errorf("EffectiveDate() = nil, expected %v", tt.expected)
				} else if !result.Equal(*tt.expected) {
					t.Errorf("EffectiveDate() = %v, expected %v", result, tt.expected)
				}
			}
		})
	}
}

func TestIsOverdue(t *testing.T) {
	now := time.Now().UTC()
	yesterday := now.AddDate(0, 0, -1)
	tomorrow := now.AddDate(0, 0, 1)
	// Start of today - this is what a DATE column stores (midnight)
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())

	tests := []struct {
		name     string
		task     *models.Task
		now      time.Time
		expected bool
	}{
		{
			name: "overdue: effective date in past",
			task: &models.Task{
				NextDueDate: timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:      now,
			expected: true,
		},
		{
			name: "not overdue: effective date in future",
			task: &models.Task{
				NextDueDate: timePtr(tomorrow),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:      now,
			expected: false,
		},
		{
			name: "not overdue: task due today (start of day)",
			task: &models.Task{
				NextDueDate: timePtr(startOfToday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:      now, // Current time during the day
			expected: false,
		},
		{
			name: "not overdue: cancelled task",
			task: &models.Task{
				NextDueDate: timePtr(yesterday),
				IsCancelled: true,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:      now,
			expected: false,
		},
		{
			name: "not overdue: archived task",
			task: &models.Task{
				NextDueDate: timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  true,
				Completions: []models.TaskCompletion{},
			},
			now:      now,
			expected: false,
		},
		{
			name: "not overdue: completed task (NextDueDate nil with completions)",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
			},
			now:      now,
			expected: false,
		},
		{
			name: "not overdue: no due date",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     nil,
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:      now,
			expected: false,
		},
		{
			name: "overdue: uses DueDate when NextDueDate nil (no completions)",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:      now,
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsOverdue(tt.task, tt.now)
			if result != tt.expected {
				t.Errorf("IsOverdue() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsDueSoon(t *testing.T) {
	now := time.Now().UTC()
	yesterday := now.AddDate(0, 0, -1)
	in5Days := now.AddDate(0, 0, 5)
	in60Days := now.AddDate(0, 0, 60)
	// Start of today - this is what a DATE column stores (midnight)
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())

	tests := []struct {
		name          string
		task          *models.Task
		now           time.Time
		daysThreshold int
		expected      bool
	}{
		{
			name: "due soon: within threshold",
			task: &models.Task{
				NextDueDate: timePtr(in5Days),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      true,
		},
		{
			name: "due soon: task due today (start of day)",
			task: &models.Task{
				NextDueDate: timePtr(startOfToday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now, // Current time during the day
			daysThreshold: 30,
			expected:      true,
		},
		{
			name: "not due soon: beyond threshold",
			task: &models.Task{
				NextDueDate: timePtr(in60Days),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
		{
			name: "not due soon: overdue (in past)",
			task: &models.Task{
				NextDueDate: timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
		{
			name: "not due soon: cancelled",
			task: &models.Task{
				NextDueDate: timePtr(in5Days),
				IsCancelled: true,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
		{
			name: "not due soon: completed",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     timePtr(in5Days),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsDueSoon(tt.task, tt.now, tt.daysThreshold)
			if result != tt.expected {
				t.Errorf("IsDueSoon() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsUpcoming(t *testing.T) {
	now := time.Now().UTC()
	in5Days := now.AddDate(0, 0, 5)
	in60Days := now.AddDate(0, 0, 60)

	tests := []struct {
		name          string
		task          *models.Task
		now           time.Time
		daysThreshold int
		expected      bool
	}{
		{
			name: "upcoming: beyond threshold",
			task: &models.Task{
				NextDueDate: timePtr(in60Days),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      true,
		},
		{
			name: "upcoming: no due date",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     nil,
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      true,
		},
		{
			name: "not upcoming: within due soon threshold",
			task: &models.Task{
				NextDueDate: timePtr(in5Days),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
		{
			name: "not upcoming: cancelled",
			task: &models.Task{
				NextDueDate: timePtr(in60Days),
				IsCancelled: true,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsUpcoming(tt.task, tt.now, tt.daysThreshold)
			if result != tt.expected {
				t.Errorf("IsUpcoming() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestHasCompletions(t *testing.T) {
	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name:     "has completions",
			task:     &models.Task{Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}},
			expected: true,
		},
		{
			name:     "no completions",
			task:     &models.Task{Completions: []models.TaskCompletion{}},
			expected: false,
		},
		{
			name:     "nil completions",
			task:     &models.Task{Completions: nil},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.HasCompletions(tt.task)
			if result != tt.expected {
				t.Errorf("HasCompletions() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsRecurring(t *testing.T) {
	days7 := 7
	days0 := 0
	days14 := 14

	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name:     "recurring: frequency with days > 0",
			task:     &models.Task{Frequency: &models.TaskFrequency{Days: &days7}},
			expected: true,
		},
		{
			name:     "not recurring: frequency without days (one-time)",
			task:     &models.Task{Frequency: &models.TaskFrequency{Days: nil}},
			expected: false,
		},
		{
			name:     "not recurring: no frequency",
			task:     &models.Task{Frequency: nil},
			expected: false,
		},
		{
			name:     "not recurring: frequency with days = 0 (Once)",
			task:     &models.Task{Frequency: &models.TaskFrequency{Days: &days0}},
			expected: false,
		},
		{
			name: "recurring: FrequencyID set with CustomIntervalDays",
			task: &models.Task{
				FrequencyID:        uintPtr(5),
				Frequency:          &models.TaskFrequency{Days: &days14},
				CustomIntervalDays: intPtr(14),
			},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsRecurring(tt.task)
			if result != tt.expected {
				t.Errorf("IsRecurring() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsOneTime(t *testing.T) {
	days7 := 7

	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name:     "one-time: no frequency",
			task:     &models.Task{Frequency: nil},
			expected: true,
		},
		{
			name:     "one-time: Once frequency (days nil)",
			task:     &models.Task{Frequency: &models.TaskFrequency{Name: "Once", Days: nil}},
			expected: true,
		},
		{
			name:     "not one-time: recurring frequency",
			task:     &models.Task{Frequency: &models.TaskFrequency{Name: "Weekly", Days: &days7}},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsOneTime(tt.task)
			if result != tt.expected {
				t.Errorf("IsOneTime() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsDueSoon_AdditionalCases(t *testing.T) {
	now := time.Now().UTC()
	startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())

	tests := []struct {
		name          string
		task          *models.Task
		now           time.Time
		daysThreshold int
		expected      bool
	}{
		{
			name: "not due soon: no due date",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     nil,
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
		{
			name: "due soon: task due today (start of day)",
			task: &models.Task{
				NextDueDate: timePtr(startOfToday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsDueSoon(tt.task, tt.now, tt.daysThreshold)
			if result != tt.expected {
				t.Errorf("IsDueSoon() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestHasCompletions_CompletionCount(t *testing.T) {
	tests := []struct {
		name     string
		task     *models.Task
		expected bool
	}{
		{
			name:     "has completions via CompletionCount",
			task:     &models.Task{Completions: nil, CompletionCount: 3},
			expected: true,
		},
		{
			name:     "no completions: CompletionCount zero, no preloaded completions",
			task:     &models.Task{Completions: nil, CompletionCount: 0},
			expected: false,
		},
		{
			name: "has completions via preloaded slice (ignores CompletionCount)",
			task: &models.Task{
				Completions:     []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
				CompletionCount: 0,
			},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.HasCompletions(tt.task)
			if result != tt.expected {
				t.Errorf("HasCompletions() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestGetCompletionCount(t *testing.T) {
	tests := []struct {
		name     string
		task     *models.Task
		expected int
	}{
		{
			name:     "zero completions: empty slice",
			task:     &models.Task{Completions: []models.TaskCompletion{}, CompletionCount: 0},
			expected: 0,
		},
		{
			name: "three completions via preloaded slice",
			task: &models.Task{
				Completions: []models.TaskCompletion{
					{BaseModel: models.BaseModel{ID: 1}},
					{BaseModel: models.BaseModel{ID: 2}},
					{BaseModel: models.BaseModel{ID: 3}},
				},
			},
			expected: 3,
		},
		{
			name:     "count via CompletionCount when slice is empty",
			task:     &models.Task{Completions: nil, CompletionCount: 5},
			expected: 5,
		},
		{
			name:     "nil completions and zero CompletionCount",
			task:     &models.Task{Completions: nil, CompletionCount: 0},
			expected: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.GetCompletionCount(tt.task)
			if result != tt.expected {
				t.Errorf("GetCompletionCount() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

func TestIsUpcoming_AdditionalCases(t *testing.T) {
	now := time.Now().UTC()
	yesterday := now.AddDate(0, 0, -1)

	tests := []struct {
		name          string
		task          *models.Task
		now           time.Time
		daysThreshold int
		expected      bool
	}{
		{
			name: "not upcoming: overdue task",
			task: &models.Task{
				NextDueDate: timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
		{
			name: "not upcoming: completed task",
			task: &models.Task{
				NextDueDate: nil,
				DueDate:     nil,
				IsCancelled: false,
				IsArchived:  false,
				Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
			},
			now:           now,
			daysThreshold: 30,
			expected:      false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := predicates.IsUpcoming(tt.task, tt.now, tt.daysThreshold)
			if result != tt.expected {
				t.Errorf("IsUpcoming() = %v, expected %v", result, tt.expected)
			}
		})
	}
}

// Helper to create a uint pointer
func uintPtr(v uint) *uint {
	return &v
}

// Helper to create an int pointer
func intPtr(v int) *int {
	return &v
}
