package postgres_test

import (
	"context"
	"net/url"
	"sort"
	"testing"
	"time"

	backendpg "galaxy/backend/internal/postgres"
	pgshared "galaxy/postgres"

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

const (
	migrationsTestImage = "postgres:16-alpine"
	migrationsTestUser = "galaxy"
	migrationsTestPassword = "galaxy"
	migrationsTestDatabase = "galaxy_backend"
	migrationsTestSchema = "backend"
	migrationsTestStartup = 90 * time.Second
	migrationsTestOpTimeout = 10 * time.Second
)

// expectedBackendTables enumerates every table the embedded migration
// set is expected to materialise inside the `backend` schema. Adding a
// table to the migration without updating this list fails the smoke
// test loudly so readers cannot lose sight of the schema surface.
var expectedBackendTables = []string{
	// Auth domain.
	"auth_challenges",
	"blocked_emails",
	"device_sessions",
	"session_revocations",
	// User domain.
	"accounts",
	"entitlement_records",
	"entitlement_snapshots",
	"limit_active",
	"limit_records",
	"sanction_active",
	"sanction_records",
	// Admin domain.
	"admin_accounts",
	// Lobby domain.
	"applications",
	"games",
	"invites",
	"memberships",
	"race_names",
	// Runtime domain.
	"engine_versions",
	"player_mappings",
	"runtime_health_snapshots",
	"runtime_operation_log",
	"runtime_records",
	// Mail domain.
	"mail_attempts",
	"mail_dead_letters",
	"mail_deliveries",
	"mail_payloads",
	"mail_recipients",
	// Notification domain.
	"notification_dead_letters",
	"notification_malformed_intents",
	"notification_routes",
	"notifications",
	// Diplomail domain.
	"diplomail_messages",
	"diplomail_recipients",
	"diplomail_translations",
	// Geo domain.
	"user_country_counters",
}

func TestMigrationsApplyToFreshSchema(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	t.Cleanup(cancel)

	pgContainer, err := tcpostgres.Run(ctx, migrationsTestImage,
		tcpostgres.WithDatabase(migrationsTestDatabase),
		tcpostgres.WithUsername(migrationsTestUser),
		tcpostgres.WithPassword(migrationsTestPassword),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").
				WithOccurrence(2).
				WithStartupTimeout(migrationsTestStartup),
		),
	)
	if err != nil {
		// testcontainers fails fast when no Docker daemon is reachable; skip
		// rather than fail so the test stays green on machines without
		// Docker (CI without Docker, sandboxed runners, etc.).
		t.Skipf("postgres testcontainer unavailable, skipping: %v", err)
	}
	t.Cleanup(func() {
		if termErr := testcontainers.TerminateContainer(pgContainer); termErr != nil {
			t.Errorf("terminate postgres container: %v", termErr)
		}
	})

	baseDSN, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		t.Fatalf("postgres connection string: %v", err)
	}
	scopedDSN, err := dsnWithSearchPath(baseDSN, migrationsTestSchema)
	if err != nil {
		t.Fatalf("scope dsn to %s: %v", migrationsTestSchema, err)
	}

	cfg := pgshared.DefaultConfig()
	cfg.PrimaryDSN = scopedDSN
	cfg.OperationTimeout = migrationsTestOpTimeout

	db, err := pgshared.OpenPrimary(ctx, cfg, backendpg.NoObservabilityOptions()...)
	if err != nil {
		t.Fatalf("open primary: %v", err)
	}
	t.Cleanup(func() {
		if err := db.Close(); err != nil {
			t.Errorf("close db: %v", err)
		}
	})

	if err := pgshared.Ping(ctx, db, cfg.OperationTimeout); err != nil {
		t.Fatalf("ping: %v", err)
	}

	if err := backendpg.ApplyMigrations(ctx, db); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}

	t.Run("backend schema exists", func(t *testing.T) {
		var present bool
		if err := db.QueryRowContext(ctx, `
			SELECT EXISTS (
				SELECT 1 FROM information_schema.schemata
				WHERE schema_name = $1
			)
		`, migrationsTestSchema).Scan(&present); err != nil {
			t.Fatalf("query schema existence: %v", err)
		}
		if !present {
			t.Fatalf("expected schema %q to exist after migrations", migrationsTestSchema)
		}
	})

	t.Run("every expected table is present", func(t *testing.T) {
		rows, err := db.QueryContext(ctx, `
			SELECT table_name FROM information_schema.tables
			WHERE table_schema = $1 AND table_type = 'BASE TABLE'
		`, migrationsTestSchema)
		if err != nil {
			t.Fatalf("list backend tables: %v", err)
		}
		defer func() { _ = rows.Close() }()

		got := make(map[string]struct{})
		for rows.Next() {
			var name string
			if err := rows.Scan(&name); err != nil {
				t.Fatalf("scan table name: %v", err)
			}
			got[name] = struct{}{}
		}
		if err := rows.Err(); err != nil {
			t.Fatalf("iterate table rows: %v", err)
		}

		// Goose's own bookkeeping table lives inside the same schema. It is
		// not a backend domain table; drop it from the comparison so a
		// goose upgrade that renames the tracker does not break the test.
		delete(got, "goose_db_version")

		var missing, extra []string
		for _, want := range expectedBackendTables {
			if _, ok := got[want]; !ok {
				missing = append(missing, want)
			}
			delete(got, want)
		}
		for name := range got {
			extra = append(extra, name)
		}
		sort.Strings(missing)
		sort.Strings(extra)
		if len(missing) > 0 || len(extra) > 0 {
			t.Fatalf("backend tables mismatch: missing=%v extra=%v", missing, extra)
		}
	})
}

func dsnWithSearchPath(baseDSN, schema string) (string, error) {
	parsed, err := url.Parse(baseDSN)
	if err != nil {
		return "", err
	}
	values := parsed.Query()
	values.Set("search_path", schema)
	if values.Get("sslmode") == "" {
		values.Set("sslmode", "disable")
	}
	parsed.RawQuery = values.Encode()
	return parsed.String(), nil
}
