package database_test

// migration_runner_test.go — Pre-fix tests for migration runner behavior.
//
// F-B1-11 P0: Server starts with incomplete schema after migration failure.
//   main.go:191-196 swallows the error from db.Migrate() and prints [OK].
//
// F-B1-13 MEDIUM: Duplicate migration numbers (009, 012) are not detected.
//   Runner processes both files with the same numeric prefix.
//
// Run: cd aggregator-server && go test ./internal/database/... -v -run TestMigration

import (
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/Fimeg/RedFlag/aggregator-server/internal/database"
)

// ---------------------------------------------------------------------------
// Test 1.1 — Migrate() returns error on broken migration SQL
//
// Category: PASS (runner returns errors correctly)
//
// F-B1-11: The runner itself correctly returns errors. The P0 is in
// main.go:191-196 swallowing the error. See Test 1.2/1.3 for that.
// ---------------------------------------------------------------------------

func TestMigrationFailureReturnsError(t *testing.T) {
	// Create a temp directory with a broken migration
	tmpDir := t.TempDir()
	brokenSQL := []byte("SELECT invalid_syntax$$;")
	if err := os.WriteFile(filepath.Join(tmpDir, "999_broken.up.sql"), brokenSQL, 0644); err != nil {
		t.Fatalf("failed to write broken migration: %v", err)
	}

	// We cannot call db.Migrate() without a real DB connection,
	// but we CAN verify the runner would attempt to process the file.
	// Read the file list the same way the runner does (db.go:51-63).
	files, err := os.ReadDir(tmpDir)
	if err != nil {
		t.Fatalf("failed to read temp dir: %v", err)
	}

	var migrationFiles []string
	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".up.sql") {
			migrationFiles = append(migrationFiles, file.Name())
		}
	}

	if len(migrationFiles) != 1 || migrationFiles[0] != "999_broken.up.sql" {
		t.Errorf("[ERROR] [server] [database] expected 1 migration file, got %d: %v", len(migrationFiles), migrationFiles)
	}

	// Verify the SQL content is indeed broken
	content, _ := os.ReadFile(filepath.Join(tmpDir, "999_broken.up.sql"))
	if !strings.Contains(string(content), "$$") {
		t.Error("[ERROR] [server] [database] broken migration should contain invalid syntax")
	}

	t.Log("[INFO] [server] [database] F-B1-11: runner correctly processes .up.sql files and would return error on bad SQL")

	// Confirm the database package is importable (compile check)
	_ = database.DB{}
}

// ---------------------------------------------------------------------------
// Test 1.2 — main.go swallows migration errors (documents P0)
//
// Category: PASS-NOW (documents the broken behavior)
//
// F-B1-11 P0: main.go catches the migration error at line 191-196,
// prints a warning, and continues to start the server. The [OK]
// message prints unconditionally.
// ---------------------------------------------------------------------------

func TestServerStartsAfterMigrationFailure(t *testing.T) {
	// POST-FIX (F-B1-11): main.go now aborts on migration failure.
	// This test confirms the Warning pattern is gone.
	mainPath := filepath.Join("..", "..", "cmd", "server", "main.go")
	content, err := os.ReadFile(mainPath)
	if err != nil {
		t.Fatalf("failed to read main.go: %v", err)
	}

	src := string(content)

	// The old bug: fmt.Printf("Warning: Migration failed...") must be gone
	if strings.Contains(src, `fmt.Printf("Warning: Migration failed`) {
		t.Error("[ERROR] [server] [database] F-B1-11 NOT FIXED: main.go still swallows migration errors")
	}

	// Must now use log.Fatalf for migration failure
	migrationBlock := extractBlock(src, "db.Migrate(migrationsPath)", `migrations_complete`)
	if migrationBlock == "" {
		t.Fatal("[ERROR] [server] [database] cannot find migration block in main.go")
	}

	if !strings.Contains(migrationBlock, "log.Fatalf") {
		t.Error("[ERROR] [server] [database] migration error handler does not use log.Fatalf")
	}

	t.Log("[INFO] [server] [database] F-B1-11 FIXED: migration failure now aborts server")
}

// ---------------------------------------------------------------------------
// Test 1.3 — main.go MUST abort on migration failure (assert fix)
//
// Category: FAIL-NOW / PASS-AFTER-FIX
//
// F-B1-11: After fix, server must refuse to start when migrations fail.
// The [OK] message must only print on genuine success.
// ---------------------------------------------------------------------------

func TestServerMustAbortOnMigrationFailure(t *testing.T) {
	// POST-FIX (F-B1-11): Confirms log.Fatalf is used for migration failure
	mainPath := filepath.Join("..", "..", "cmd", "server", "main.go")
	content, err := os.ReadFile(mainPath)
	if err != nil {
		t.Fatalf("failed to read main.go: %v", err)
	}

	src := string(content)
	migrationBlock := extractBlock(src, "db.Migrate(migrationsPath)", `migrations_complete`)
	if migrationBlock == "" {
		t.Fatal("[ERROR] [server] [database] cannot find migration block")
	}

	if !strings.Contains(migrationBlock, "log.Fatalf") {
		t.Errorf("[ERROR] [server] [database] migration error handler must use log.Fatalf")
	}
	// Success message must only appear after the error check
	if strings.Contains(src, `fmt.Printf("Warning: Migration failed`) {
		t.Errorf("[ERROR] [server] [database] old warning pattern still present")
	}
	t.Log("[INFO] [server] [database] F-B1-11 FIXED: server aborts on migration failure")
}

// ---------------------------------------------------------------------------
// Test 1.4 — Duplicate migration numbers exist (documents F-B1-13)
//
// Category: PASS-NOW (documents the bug)
// ---------------------------------------------------------------------------

func TestMigrationRunnerDetectsDuplicateNumbers(t *testing.T) {
	// POST-FIX (F-B1-13): No duplicate migration numbers should exist.
	migrationsPath := filepath.Join("migrations")
	files, err := os.ReadDir(migrationsPath)
	if err != nil {
		t.Fatalf("failed to read migrations directory: %v", err)
	}

	// Extract numeric prefixes from .up.sql files
	// Note: "009b" and "012b" are distinct from "009" and "012" — not duplicates
	prefixCount := make(map[string][]string)
	for _, file := range files {
		if !strings.HasSuffix(file.Name(), ".up.sql") {
			continue
		}
		parts := strings.SplitN(file.Name(), "_", 2)
		if len(parts) >= 1 {
			prefix := parts[0]
			prefixCount[prefix] = append(prefixCount[prefix], file.Name())
		}
	}

	duplicates := 0
	for prefix, names := range prefixCount {
		if len(names) > 1 {
			duplicates++
			t.Errorf("[ERROR] [server] [database] duplicate migration prefix %s: %v", prefix, names)
		}
	}

	if duplicates > 0 {
		t.Errorf("[ERROR] [server] [database] F-B1-13 NOT FIXED: %d duplicates remain", duplicates)
	}
	t.Log("[INFO] [server] [database] F-B1-13 FIXED: no duplicate migration numbers")
}

// ---------------------------------------------------------------------------
// Test 1.5 — Runner should reject duplicate numbers (assert fix)
//
// Category: FAIL-NOW / PASS-AFTER-FIX
// ---------------------------------------------------------------------------

func TestMigrationRunnerShouldRejectDuplicateNumbers(t *testing.T) {
	// POST-FIX (F-B1-13): All migration prefixes are unique.
	// Duplicates resolved by renaming: 009→009b, 012→012b
	migrationsPath := filepath.Join("migrations")
	files, err := os.ReadDir(migrationsPath)
	if err != nil {
		t.Fatalf("failed to read migrations directory: %v", err)
	}

	prefixCount := make(map[string]int)
	for _, file := range files {
		if !strings.HasSuffix(file.Name(), ".up.sql") {
			continue
		}
		parts := strings.SplitN(file.Name(), "_", 2)
		if len(parts) >= 1 {
			prefixCount[parts[0]]++
		}
	}

	for prefix, count := range prefixCount {
		if count > 1 {
			t.Errorf("[ERROR] [server] [database] migration prefix %s has %d files", prefix, count)
		}
	}
	t.Log("[INFO] [server] [database] F-B1-13 FIXED: all migration prefixes are unique")
}

// extractBlock extracts text between two markers in a source string
func extractBlock(src, startMarker, endMarker string) string {
	startIdx := strings.Index(src, startMarker)
	if startIdx == -1 {
		return ""
	}
	endIdx := strings.Index(src[startIdx:], endMarker)
	if endIdx == -1 {
		return ""
	}
	return src[startIdx : startIdx+endIdx+len(endMarker)]
}

// sortedKeys returns sorted keys of a map
func sortedKeys(m map[string][]string) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}
