// Package task provides consistency tests that verify all three layers
// (predicates, scopes, and categorization) return identical results.
//
// These tests are critical for ensuring the DRY architecture is maintained.
// If any of these tests fail, it means the three layers have diverged and
// will produce inconsistent results in different parts of the application.
package task_test

import (
	"os"
	"testing"
	"time"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

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

// testDB holds the database connection for integration tests
var testDB *gorm.DB

// testUserID is a user ID that exists in the database for foreign key constraints
var testUserID uint = 1

// TestMain sets up the database connection for all tests in this package.
// If the database is not available, testDB remains nil and individual tests
// will call t.Skip() instead of using os.Exit(0), which preserves proper
// test reporting and coverage output.
func TestMain(m *testing.M) {
	dsn := os.Getenv("TEST_DATABASE_URL")
	if dsn == "" {
		dsn = "host=localhost user=postgres password=postgres dbname=honeydue_test port=5432 sslmode=disable"
	}

	var err error
	testDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		// Explicitly nil out testDB; individual tests will t.Skip("Database not available")
		testDB = nil
		println("Consistency integration tests will be skipped: database not available")
		println("Set TEST_DATABASE_URL to run these tests")
		os.Exit(m.Run())
	}

	sqlDB, err := testDB.DB()
	if err != nil {
		println("Failed to get underlying DB:", err.Error())
		testDB = nil
		os.Exit(m.Run())
	}
	if pingErr := sqlDB.Ping(); pingErr != nil {
		println("Failed to ping database:", pingErr.Error())
		testDB = nil
		os.Exit(m.Run())
	}

	println("Database connected, running consistency tests...")

	code := m.Run()
	cleanupTestData()
	os.Exit(code)
}

func cleanupTestData() {
	if testDB == nil {
		return
	}
	testDB.Exec("DELETE FROM task_taskcompletion WHERE task_id IN (SELECT id FROM task_task WHERE title LIKE 'consistency_test_%')")
	testDB.Exec("DELETE FROM task_task WHERE title LIKE 'consistency_test_%'")
	testDB.Exec("DELETE FROM residence_residence WHERE name LIKE 'consistency_test_%'")
}

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

func createResidence(t *testing.T) uint {
	residence := &models.Residence{
		Name:     "consistency_test_" + time.Now().Format("20060102150405.000"),
		OwnerID:  testUserID,
		IsActive: true,
	}
	if err := testDB.Create(residence).Error; err != nil {
		t.Fatalf("Failed to create residence: %v", err)
	}
	return residence.ID
}

func createTask(t *testing.T, residenceID uint, task *models.Task) *models.Task {
	task.ResidenceID = residenceID
	task.Title = "consistency_test_" + task.Title
	task.CreatedByID = testUserID
	if err := testDB.Create(task).Error; err != nil {
		t.Fatalf("Failed to create task: %v", err)
	}
	return task
}

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


// TaskTestCase defines a test scenario with expected categorization
type TaskTestCase struct {
	Name           string
	Task           *models.Task
	HasCompletion  bool
	ExpectedColumn categorization.KanbanColumn
	// Expected predicate results
	ExpectCompleted  bool
	ExpectActive     bool
	ExpectOverdue    bool
	ExpectDueSoon    bool
	ExpectUpcoming   bool
	ExpectInProgress bool
}

// TestAllThreeLayersMatch is the master consistency test.
// It creates tasks in the database, then verifies that:
// 1. Predicates return the expected boolean values
// 2. Categorization returns the expected kanban column
// 3. Scopes return the same tasks that predicates would filter
func TestAllThreeLayersMatch(t *testing.T) {
	if testDB == nil {
		t.Skip("Database not available")
	}

	residenceID := createResidence(t)
	defer cleanupTestData()

	now := time.Now().UTC()
	yesterday := now.AddDate(0, 0, -1)
	in5Days := now.AddDate(0, 0, 5)
	in60Days := now.AddDate(0, 0, 60)
	daysThreshold := 30

	// Define all test cases with expected results for each layer
	testCases := []TaskTestCase{
		{
			Name: "overdue_active",
			Task: &models.Task{
				Title:       "overdue_active",
				NextDueDate: timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
			},
			ExpectedColumn:  categorization.ColumnOverdue,
			ExpectCompleted: false,
			ExpectActive:    true,
			ExpectOverdue:   true,
			ExpectDueSoon:   false,
			ExpectUpcoming:  false,
		},
		{
			Name: "due_soon_active",
			Task: &models.Task{
				Title:       "due_soon_active",
				NextDueDate: timePtr(in5Days),
				IsCancelled: false,
				IsArchived:  false,
			},
			ExpectedColumn:  categorization.ColumnDueSoon,
			ExpectCompleted: false,
			ExpectActive:    true,
			ExpectOverdue:   false,
			ExpectDueSoon:   true,
			ExpectUpcoming:  false,
		},
		{
			Name: "upcoming_far_future",
			Task: &models.Task{
				Title:       "upcoming_far_future",
				NextDueDate: timePtr(in60Days),
				IsCancelled: false,
				IsArchived:  false,
			},
			ExpectedColumn:  categorization.ColumnUpcoming,
			ExpectCompleted: false,
			ExpectActive:    true,
			ExpectOverdue:   false,
			ExpectDueSoon:   false,
			ExpectUpcoming:  true,
		},
		{
			Name: "upcoming_no_due_date",
			Task: &models.Task{
				Title:       "upcoming_no_due_date",
				NextDueDate: nil,
				DueDate:     nil,
				IsCancelled: false,
				IsArchived:  false,
			},
			ExpectedColumn:  categorization.ColumnUpcoming,
			ExpectCompleted: false,
			ExpectActive:    true,
			ExpectOverdue:   false,
			ExpectDueSoon:   false,
			ExpectUpcoming:  true,
		},
		{
			Name: "completed_one_time",
			Task: &models.Task{
				Title:       "completed_one_time",
				NextDueDate: nil, // No next due date
				DueDate:     timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  false,
			},
			HasCompletion:   true, // Will create a completion
			ExpectedColumn:  categorization.ColumnCompleted,
			ExpectCompleted: true,
			ExpectActive:    true,
			ExpectOverdue:   false, // Completed tasks are not overdue
			ExpectDueSoon:   false,
			ExpectUpcoming:  false,
		},
		{
			Name: "cancelled_task",
			Task: &models.Task{
				Title:       "cancelled_task",
				NextDueDate: timePtr(yesterday), // Would be overdue if not cancelled
				IsCancelled: true,
				IsArchived:  false,
			},
			ExpectedColumn:  categorization.ColumnCancelled,
			ExpectCompleted: false,
			ExpectActive:    false, // Cancelled is not active
			ExpectOverdue:   false, // Cancelled tasks are not overdue
			ExpectDueSoon:   false,
			ExpectUpcoming:  false,
		},
		{
			Name: "archived_task",
			Task: &models.Task{
				Title:       "archived_task",
				NextDueDate: timePtr(yesterday),
				IsCancelled: false,
				IsArchived:  true,
			},
			// Archived tasks don't appear in kanban (filtered out before categorization)
			// but we test predicates still work
			ExpectedColumn:  categorization.ColumnOverdue, // Chain doesn't check archived
			ExpectCompleted: false,
			ExpectActive:    false, // Archived is not active
			ExpectOverdue:   false, // Archived tasks are not overdue (IsOverdue checks IsActive)
			ExpectDueSoon:   false,
			ExpectUpcoming:  false,
		},
		{
			Name: "recurring_with_completion",
			Task: &models.Task{
				Title:       "recurring_with_completion",
				NextDueDate: timePtr(in5Days), // Has next due date (recurring)
				IsCancelled: false,
				IsArchived:  false,
			},
			HasCompletion:   true,
			ExpectedColumn:  categorization.ColumnDueSoon, // Not completed because NextDueDate is set
			ExpectCompleted: false,                        // Has completion but NextDueDate is set
			ExpectActive:    true,
			ExpectOverdue:   false,
			ExpectDueSoon:   true,
			ExpectUpcoming:  false,
		},
		{
			Name: "overdue_uses_duedate_fallback",
			Task: &models.Task{
				Title:       "overdue_uses_duedate_fallback",
				NextDueDate: nil,
				DueDate:     timePtr(yesterday), // Falls back to DueDate
				IsCancelled: false,
				IsArchived:  false,
			},
			ExpectedColumn:  categorization.ColumnOverdue,
			ExpectCompleted: false,
			ExpectActive:    true,
			ExpectOverdue:   true,
			ExpectDueSoon:   false,
			ExpectUpcoming:  false,
		},
		{
			Name: "in_progress_overdue",
			Task: &models.Task{
				Title:       "in_progress_overdue",
				NextDueDate: timePtr(yesterday), // Would be overdue
				InProgress:  true,
				IsCancelled: false,
				IsArchived:  false,
			},
			ExpectedColumn:   categorization.ColumnInProgress, // In Progress takes priority
			ExpectCompleted:  false,
			ExpectActive:     true,
			ExpectOverdue:    true, // Predicate says overdue (doesn't check InProgress)
			ExpectDueSoon:    false,
			ExpectUpcoming:   false,
			ExpectInProgress: true,
		},
	}

	// Create all tasks in database
	createdTasks := make(map[string]*models.Task)
	for _, tc := range testCases {
		task := createTask(t, residenceID, tc.Task)
		if tc.HasCompletion {
			createCompletion(t, task.ID)
		}
		createdTasks[tc.Name] = task
	}

	// Reload all tasks with preloads for predicate testing
	var allTasks []models.Task
	err := testDB.
		Preload("Completions").
		Where("residence_id = ?", residenceID).
		Find(&allTasks).Error
	if err != nil {
		t.Fatalf("Failed to load tasks: %v", err)
	}

	// Create a map for easy lookup
	taskMap := make(map[string]*models.Task)
	for i := range allTasks {
		// Strip the prefix for lookup
		name := allTasks[i].Title[len("consistency_test_"):]
		taskMap[name] = &allTasks[i]
	}

	// Test each case
	for _, tc := range testCases {
		t.Run(tc.Name, func(t *testing.T) {
			task := taskMap[tc.Name]
			if task == nil {
				t.Fatalf("Task %s not found", tc.Name)
			}

			// ========== TEST PREDICATES ==========
			t.Run("predicates", func(t *testing.T) {
				if got := predicates.IsCompleted(task); got != tc.ExpectCompleted {
					t.Errorf("IsCompleted() = %v, want %v", got, tc.ExpectCompleted)
				}
				if got := predicates.IsActive(task); got != tc.ExpectActive {
					t.Errorf("IsActive() = %v, want %v", got, tc.ExpectActive)
				}
				if got := predicates.IsOverdue(task, now); got != tc.ExpectOverdue {
					t.Errorf("IsOverdue() = %v, want %v", got, tc.ExpectOverdue)
				}
				if got := predicates.IsDueSoon(task, now, daysThreshold); got != tc.ExpectDueSoon {
					t.Errorf("IsDueSoon() = %v, want %v", got, tc.ExpectDueSoon)
				}
				if got := predicates.IsUpcoming(task, now, daysThreshold); got != tc.ExpectUpcoming {
					t.Errorf("IsUpcoming() = %v, want %v", got, tc.ExpectUpcoming)
				}
				if tc.ExpectInProgress {
					if got := predicates.IsInProgress(task); got != tc.ExpectInProgress {
						t.Errorf("IsInProgress() = %v, want %v", got, tc.ExpectInProgress)
					}
				}
			})

			// ========== TEST CATEGORIZATION ==========
			t.Run("categorization", func(t *testing.T) {
				got := categorization.CategorizeTask(task, daysThreshold)
				if got != tc.ExpectedColumn {
					t.Errorf("CategorizeTask() = %v, want %v", got, tc.ExpectedColumn)
				}
			})
		})
	}

	// ========== TEST SCOPES MATCH PREDICATES ==========
	// This is the critical test: query with scopes and verify results match predicate filtering

	t.Run("scopes_match_predicates", func(t *testing.T) {
		// Test ScopeActive
		t.Run("ScopeActive", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeActive).
				Find(&scopeResults)

			predicateCount := 0
			for _, task := range allTasks {
				if predicates.IsActive(&task) {
					predicateCount++
				}
			}

			if len(scopeResults) != predicateCount {
				t.Errorf("ScopeActive returned %d, predicates found %d", len(scopeResults), predicateCount)
			}
		})

		// Test ScopeCompleted
		t.Run("ScopeCompleted", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeCompleted).
				Find(&scopeResults)

			predicateCount := 0
			for _, task := range allTasks {
				if predicates.IsCompleted(&task) {
					predicateCount++
				}
			}

			if len(scopeResults) != predicateCount {
				t.Errorf("ScopeCompleted returned %d, predicates found %d", len(scopeResults), predicateCount)
			}
		})

		// Test ScopeOverdue
		t.Run("ScopeOverdue", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeOverdue(now)).
				Find(&scopeResults)

			predicateCount := 0
			for _, task := range allTasks {
				if predicates.IsOverdue(&task, now) {
					predicateCount++
				}
			}

			if len(scopeResults) != predicateCount {
				t.Errorf("ScopeOverdue returned %d, predicates found %d", len(scopeResults), predicateCount)
				t.Logf("Scope results: %v", getTaskNames(scopeResults))
				t.Logf("Predicate matches: %v", getPredicateMatches(allTasks, func(task *models.Task) bool {
					return predicates.IsOverdue(task, now)
				}))
			}
		})

		// Test ScopeDueSoon
		t.Run("ScopeDueSoon", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeDueSoon(now, daysThreshold)).
				Find(&scopeResults)

			predicateCount := 0
			for _, task := range allTasks {
				if predicates.IsDueSoon(&task, now, daysThreshold) {
					predicateCount++
				}
			}

			if len(scopeResults) != predicateCount {
				t.Errorf("ScopeDueSoon returned %d, predicates found %d", len(scopeResults), predicateCount)
			}
		})

		// Test ScopeUpcoming
		t.Run("ScopeUpcoming", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeUpcoming(now, daysThreshold)).
				Find(&scopeResults)

			predicateCount := 0
			for _, task := range allTasks {
				if predicates.IsUpcoming(&task, now, daysThreshold) {
					predicateCount++
				}
			}

			if len(scopeResults) != predicateCount {
				t.Errorf("ScopeUpcoming returned %d, predicates found %d", len(scopeResults), predicateCount)
			}
		})

		// Test ScopeInProgress
		t.Run("ScopeInProgress", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeInProgress).
				Find(&scopeResults)

			predicateCount := 0
			for _, task := range allTasks {
				if predicates.IsInProgress(&task) {
					predicateCount++
				}
			}

			if len(scopeResults) != predicateCount {
				t.Errorf("ScopeInProgress returned %d, predicates found %d", len(scopeResults), predicateCount)
			}
		})
	})

	// ========== TEST CATEGORIZATION MATCHES SCOPES FOR KANBAN ==========
	// Verify that tasks categorized into each column match what scopes would return

	t.Run("categorization_matches_scopes", func(t *testing.T) {
		// Get categorization results
		categorized := categorization.CategorizeTasksIntoColumns(allTasks, daysThreshold)

		// Compare overdue column with scope
		// NOTE: Scopes return tasks based on date criteria only.
		// Categorization uses priority order (In Progress > Overdue).
		// So a task that is overdue by date but "In Progress" won't be in ColumnOverdue.
		// We need to compare scope results MINUS those with higher-priority categorization.
		t.Run("overdue_column", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeOverdue(now)).
				Find(&scopeResults)

			// Filter scope results to exclude tasks that would be categorized differently
			// (i.e., tasks that are In Progress - higher priority than Overdue)
			scopeOverdueNotInProgress := 0
			for _, task := range scopeResults {
				if !predicates.IsInProgress(&task) {
					scopeOverdueNotInProgress++
				}
			}

			// Count active overdue tasks in categorization
			activeOverdue := 0
			for _, task := range categorized[categorization.ColumnOverdue] {
				if predicates.IsActive(&task) {
					activeOverdue++
				}
			}

			if scopeOverdueNotInProgress != activeOverdue {
				t.Errorf("Overdue: scope returned %d (excluding in-progress), categorization has %d active",
					scopeOverdueNotInProgress, activeOverdue)
			}
		})

		// Compare due soon column with scope
		t.Run("due_soon_column", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeDueSoon(now, daysThreshold)).
				Find(&scopeResults)

			activeDueSoon := 0
			for _, task := range categorized[categorization.ColumnDueSoon] {
				if predicates.IsActive(&task) {
					activeDueSoon++
				}
			}

			if len(scopeResults) != activeDueSoon {
				t.Errorf("DueSoon: scope returned %d, categorization has %d active",
					len(scopeResults), activeDueSoon)
			}
		})

		// Compare completed column with scope
		t.Run("completed_column", func(t *testing.T) {
			var scopeResults []models.Task
			testDB.Model(&models.Task{}).
				Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeCompleted).
				Find(&scopeResults)

			if len(scopeResults) != len(categorized[categorization.ColumnCompleted]) {
				t.Errorf("Completed: scope returned %d, categorization has %d",
					len(scopeResults), len(categorized[categorization.ColumnCompleted]))
			}
		})
	})
}

// TestSameDayOverdueConsistency is a regression test for day-based overdue logic.
// With day-based comparisons, a task due TODAY (at any time) is NOT overdue during that day.
// It only becomes overdue the NEXT day. This test verifies all three layers agree.
func TestSameDayOverdueConsistency(t *testing.T) {
	if testDB == nil {
		t.Skip("Database not available")
	}

	residenceID := createResidence(t)
	defer cleanupTestData()

	// Create a task due at midnight today
	now := time.Now().UTC()
	todayMidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)

	task := createTask(t, residenceID, &models.Task{
		Title:       "same_day_midnight",
		NextDueDate: timePtr(todayMidnight),
		IsCancelled: false,
		IsArchived:  false,
	})

	// Reload with preloads
	var loadedTask models.Task
	testDB.Preload("Completions").First(&loadedTask, task.ID)

	// All three layers should agree
	predicateResult := predicates.IsOverdue(&loadedTask, now)

	var scopeResults []models.Task
	testDB.Model(&models.Task{}).
		Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeOverdue(now)).
		Find(&scopeResults)
	scopeResult := len(scopeResults) == 1

	categorizationResult := categorization.CategorizeTask(&loadedTask, 30) == categorization.ColumnOverdue

	// With day-based comparison: task due TODAY is NOT overdue during that day.
	// All three layers should say NOT overdue.
	if predicateResult {
		t.Error("Predicate incorrectly says overdue for same-day task")
	}
	if scopeResult {
		t.Error("Scope incorrectly says overdue for same-day task")
	}
	if categorizationResult {
		t.Error("Categorization incorrectly says overdue for same-day task")
	}

	// Most importantly: all three must agree
	if predicateResult != scopeResult {
		t.Errorf("INCONSISTENCY: predicate=%v, scope=%v", predicateResult, scopeResult)
	}
	if predicateResult != categorizationResult {
		t.Errorf("INCONSISTENCY: predicate=%v, categorization=%v", predicateResult, categorizationResult)
	}
}

// Helper functions

func getTaskNames(tasks []models.Task) []string {
	names := make([]string, len(tasks))
	for i, t := range tasks {
		names[i] = t.Title
	}
	return names
}

func getPredicateMatches(tasks []models.Task, predicate func(*models.Task) bool) []string {
	var names []string
	for _, t := range tasks {
		if predicate(&t) {
			names = append(names, t.Title)
		}
	}
	return names
}
