// Package postgres_test contains integration tests for PostgreSQL repository
// implementations using testcontainers-go. Tests spin up a real PostgreSQL 16
// container and use schema-per-test isolation for parallel safety.
package postgres_test

import (
	"context"
	"database/sql"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	_ "github.com/lib/pq"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

// testDB holds a shared database connection for a test suite.
// Each test gets its own schema (via search_path) for isolation.
type testDB struct {
	db        *sql.DB
	container testcontainers.Container
}

// setupTestDB starts a PostgreSQL container and runs all migrations.
// Call this once per test file via TestMain or a sync.Once.
func setupTestDB(t *testing.T) *testDB {
	t.Helper()

	if testing.Short() {
		t.Skip("skipping integration test in short mode")
	}

	ctx := context.Background()

	req := testcontainers.ContainerRequest{
		Image:        "postgres:16-alpine",
		ExposedPorts: []string{"5432/tcp"},
		Env: map[string]string{
			"POSTGRES_DB":       "certctl_test",
			"POSTGRES_USER":     "certctl",
			"POSTGRES_PASSWORD": "certctl",
		},
		WaitingFor: wait.ForLog("database system is ready to accept connections").WithOccurrence(2),
	}

	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		t.Fatalf("failed to start postgres container: %v", err)
	}

	host, err := container.Host(ctx)
	if err != nil {
		t.Fatalf("failed to get container host: %v", err)
	}

	port, err := container.MappedPort(ctx, "5432")
	if err != nil {
		t.Fatalf("failed to get mapped port: %v", err)
	}

	connStr := fmt.Sprintf("postgres://certctl:certctl@%s:%s/certctl_test?sslmode=disable", host, port.Port())

	db, err := sql.Open("postgres", connStr)
	if err != nil {
		t.Fatalf("failed to open database: %v", err)
	}

	// Limit to 1 connection so SET search_path persists across all queries.
	db.SetMaxOpenConns(1)

	if err := db.Ping(); err != nil {
		t.Fatalf("failed to ping database: %v", err)
	}

	// Run migrations
	migrationsPath := findMigrationsDir()
	if err := runMigrations(db, migrationsPath); err != nil {
		t.Fatalf("failed to run migrations: %v", err)
	}

	return &testDB{db: db, container: container}
}

// teardown stops the container and closes the connection.
func (tdb *testDB) teardown(t *testing.T) {
	t.Helper()
	if tdb.db != nil {
		tdb.db.Close()
	}
	if tdb.container != nil {
		tdb.container.Terminate(context.Background())
	}
}

// freshSchema creates a new PostgreSQL schema for test isolation
// and returns a *sql.DB with search_path set to that schema.
// Each test gets a unique schema so tests don't interfere with each other.
func (tdb *testDB) freshSchema(t *testing.T) *sql.DB {
	t.Helper()

	// Create a unique schema name from the test name
	schemaName := sanitizeSchemaName(t.Name())

	ctx := context.Background()

	// Create schema
	_, err := tdb.db.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName))
	if err != nil {
		t.Fatalf("failed to create schema %s: %v", schemaName, err)
	}

	// Set search_path for this connection to use the new schema
	_, err = tdb.db.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s, public", schemaName))
	if err != nil {
		t.Fatalf("failed to set search_path: %v", err)
	}

	// Run migrations in the new schema
	migrationsPath := findMigrationsDir()
	if err := runMigrationsWithSearchPath(tdb.db, migrationsPath, schemaName); err != nil {
		t.Fatalf("failed to run migrations in schema %s: %v", schemaName, err)
	}

	// Register cleanup
	t.Cleanup(func() {
		tdb.db.ExecContext(context.Background(), fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName))
	})

	return tdb.db
}

// sanitizeSchemaName converts a test name to a valid PostgreSQL schema name.
func sanitizeSchemaName(name string) string {
	name = strings.ToLower(name)
	name = strings.ReplaceAll(name, "/", "_")
	name = strings.ReplaceAll(name, " ", "_")
	name = strings.ReplaceAll(name, "-", "_")
	name = strings.ReplaceAll(name, ".", "_")
	// Truncate to 63 chars (PG limit)
	if len(name) > 60 {
		name = name[:60]
	}
	return "test_" + name
}

// findMigrationsDir walks up from the test file to find the migrations/ directory.
func findMigrationsDir() string {
	_, filename, _, _ := runtime.Caller(0)
	dir := filepath.Dir(filename)

	// Walk up to find the project root (where migrations/ lives)
	for i := 0; i < 10; i++ {
		candidate := filepath.Join(dir, "migrations")
		if _, err := os.Stat(candidate); err == nil {
			return candidate
		}
		dir = filepath.Dir(dir)
	}

	// Fallback: try relative from working directory
	return "../../../../migrations"
}

// runMigrations reads and executes all .up.sql migration files.
func runMigrations(db *sql.DB, migrationsPath string) error {
	files, err := os.ReadDir(migrationsPath)
	if err != nil {
		return fmt.Errorf("failed to read migrations directory %s: %w", migrationsPath, err)
	}

	for _, file := range files {
		if !file.IsDir() && strings.HasSuffix(file.Name(), ".up.sql") {
			content, err := os.ReadFile(filepath.Join(migrationsPath, file.Name()))
			if err != nil {
				return fmt.Errorf("failed to read migration %s: %w", file.Name(), err)
			}
			if _, err := db.Exec(string(content)); err != nil {
				return fmt.Errorf("failed to execute migration %s: %w", file.Name(), err)
			}
		}
	}
	return nil
}

// runMigrationsWithSearchPath runs migrations within a specific schema.
func runMigrationsWithSearchPath(db *sql.DB, migrationsPath string, schema string) error {
	// Set search_path before running migrations
	if _, err := db.Exec(fmt.Sprintf("SET search_path TO %s, public", schema)); err != nil {
		return fmt.Errorf("failed to set search_path: %w", err)
	}
	return runMigrations(db, migrationsPath)
}
