//go:build integration

package inttest

import (
	"context"
	"database/sql"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	testcontainers "github.com/testcontainers/testcontainers-go"
	tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"

	"scrabble/backend/internal/engine"
	"scrabble/backend/internal/postgres"
)

// testDB is the shared, migrated pool every integration test runs against. It is
// hydrated once by TestMain.
var testDB *sql.DB

// testRegistry holds the committed dictionaries the game service plays over,
// loaded once under testDictVersion (the version the game tests pin).
var testRegistry *engine.Registry

// testDictVersion is the single dictionary version the integration suite loads.
const testDictVersion = "v1"

const (
	pgImage           = "postgres:17-alpine"
	pgDatabase        = "scrabble_backend"
	pgUser            = "scrabble"
	pgPassword        = "scrabble"
	pgSchema          = "backend"
	containerStartup  = 90 * time.Second
	containerShutdown = 30 * time.Second
)

// TestMain starts one Postgres container, applies the migrations, and shares the
// resulting pool with every test. Any setup failure aborts the suite loudly
// (exit 1) rather than skipping coverage.
func TestMain(m *testing.M) {
	code, err := runSuite(m)
	if err != nil {
		fmt.Fprintln(os.Stderr, "inttest:", err)
		os.Exit(1)
	}
	os.Exit(code)
}

func runSuite(m *testing.M) (int, error) {
	ctx := context.Background()

	container, err := tcpostgres.Run(ctx, pgImage,
		tcpostgres.WithDatabase(pgDatabase),
		tcpostgres.WithUsername(pgUser),
		tcpostgres.WithPassword(pgPassword),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").
				WithOccurrence(2).
				WithStartupTimeout(containerStartup),
		),
	)
	if err != nil {
		return 0, fmt.Errorf("start postgres container: %w", err)
	}
	defer func() {
		shutdownCtx, cancel := context.WithTimeout(context.Background(), containerShutdown)
		defer cancel()
		if termErr := container.Terminate(shutdownCtx); termErr != nil {
			fmt.Fprintln(os.Stderr, "inttest: terminate container:", termErr)
		}
	}()

	baseDSN, err := container.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		return 0, fmt.Errorf("resolve container dsn: %w", err)
	}
	dsn, err := withSearchPath(baseDSN, pgSchema)
	if err != nil {
		return 0, err
	}

	cfg := postgres.DefaultConfig()
	cfg.DSN = dsn
	db, err := postgres.Open(ctx, cfg)
	if err != nil {
		return 0, fmt.Errorf("open pool: %w", err)
	}
	defer func() { _ = db.Close() }()

	if err := postgres.ApplyMigrations(ctx, db); err != nil {
		return 0, fmt.Errorf("apply migrations: %w", err)
	}

	reg, err := engine.Open(dictDir(), testDictVersion)
	if err != nil {
		return 0, fmt.Errorf("load dictionaries: %w", err)
	}
	defer func() { _ = reg.Close() }()

	testDB = db
	testRegistry = reg
	return m.Run(), nil
}

// dictDir resolves the committed DAWG directory: BACKEND_DICT_DIR when set (CI),
// otherwise the sibling scrabble-solver checkout located relative to this file.
func dictDir() string {
	if dir := os.Getenv("BACKEND_DICT_DIR"); dir != "" {
		return dir
	}
	_, file, _, _ := runtime.Caller(0)
	return filepath.Join(filepath.Dir(file), "..", "..", "..", "..", "scrabble-solver", "dawg")
}

// withSearchPath rewrites dsn so every connection pins search_path to schema.
func withSearchPath(dsn, schema string) (string, error) {
	u, err := url.Parse(dsn)
	if err != nil {
		return "", fmt.Errorf("parse dsn: %w", err)
	}
	q := u.Query()
	q.Set("search_path", schema)
	if q.Get("sslmode") == "" {
		q.Set("sslmode", "disable")
	}
	u.RawQuery = q.Encode()
	return u.String(), nil
}
