//go:build integration

package inttest

import (
	"context"
	"database/sql"
	"testing"

	"github.com/google/uuid"
	"github.com/pressly/goose/v3"
	testcontainers "github.com/testcontainers/testcontainers-go"
	tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/postgres"
	"scrabble/backend/internal/postgres/migrations"
	"scrabble/backend/internal/session"
)

// TestSessionPlatformCaptureAndUntrusted proves a session round-trips its captured
// platform through create and a cold-cache (DB-backed) resolve for each kind, and
// that an unattributed session records no platform — the untrusted, view-only case.
func TestSessionPlatformCaptureAndUntrusted(t *testing.T) {
	ctx := context.Background()
	accounts := account.NewStore(testDB)
	store := session.NewStore(testDB)
	svc := session.NewService(store, session.NewCache())

	for _, tc := range []struct {
		name     string
		platform session.Platform
	}{
		{"vk-ios", session.Platform{Kind: session.PlatformKindVK, Subtype: session.SubtypeIOS}},
		{"telegram-android", session.Platform{Kind: session.PlatformKindTelegram, Subtype: session.SubtypeAndroid}},
		{"direct-web", session.Platform{Kind: session.PlatformKindDirect, Subtype: session.SubtypeWeb}},
		{"untrusted", session.Platform{}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			acc, err := accounts.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
			if err != nil {
				t.Fatalf("provision: %v", err)
			}
			token, sess, err := svc.Create(ctx, acc.ID, tc.platform)
			if err != nil {
				t.Fatalf("create: %v", err)
			}
			if sess.Platform != tc.platform {
				t.Errorf("created platform = %+v, want %+v", sess.Platform, tc.platform)
			}
			// Resolve through a cold cache so the value comes back via the DB columns.
			cold := session.NewService(store, session.NewCache())
			if err := cold.Warm(ctx); err != nil {
				t.Fatalf("warm: %v", err)
			}
			got, err := cold.Resolve(ctx, token)
			if err != nil {
				t.Fatalf("resolve: %v", err)
			}
			if got.Platform != tc.platform {
				t.Errorf("resolved platform = %+v, want %+v", got.Platform, tc.platform)
			}
			if got.Platform.Trusted() != tc.platform.Trusted() {
				t.Errorf("resolved Trusted() = %v, want %v", got.Platform.Trusted(), tc.platform.Trusted())
			}
		})
	}
}

// TestSessionPlatformCheckConstraints proves the CHECK constraints reject an
// out-of-range kind or subtype, so a corrupt value cannot be persisted.
func TestSessionPlatformCheckConstraints(t *testing.T) {
	ctx := context.Background()
	acc, err := account.NewStore(testDB).ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
	if err != nil {
		t.Fatalf("provision: %v", err)
	}
	const pgCheckViolation = "23514"
	for _, tc := range []struct {
		name    string
		kind    string
		subtype string
	}{
		{"bad-kind", "facebook", "web"},
		{"bad-subtype", "vk", "windows"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			_, err := testDB.ExecContext(ctx,
				`INSERT INTO backend.sessions (session_id, account_id, token_hash, platform_kind, platform_subtype)
				 VALUES ($1, $2, $3, $4, $5)`,
				uuid.New(), acc.ID, uuid.NewString(), tc.kind, tc.subtype)
			if !isPgCode(err, pgCheckViolation) {
				t.Errorf("insert %s: err = %v, want check_violation", tc.name, err)
			}
		})
	}
}

// TestSessionPlatformMigrationReversible proves migration 00011 is expand-contract:
// it applies, rolls back (dropping the platform columns), and re-applies cleanly —
// so a backend image rollback stays DB-safe. It uses its own container so the Down
// does not disturb the shared suite database.
func TestSessionPlatformMigrationReversible(t *testing.T) {
	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 {
		t.Fatalf("start container: %v", err)
	}
	defer func() { _ = container.Terminate(context.Background()) }()

	baseDSN, err := container.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		t.Fatalf("connection string: %v", err)
	}
	dsn, err := withSearchPath(baseDSN, pgSchema)
	if err != nil {
		t.Fatalf("search path: %v", err)
	}
	cfg := postgres.DefaultConfig()
	cfg.DSN = dsn
	db, err := postgres.Open(ctx, cfg)
	if err != nil {
		t.Fatalf("open pool: %v", err)
	}
	defer func() { _ = db.Close() }()

	if err := postgres.ApplyMigrations(ctx, db); err != nil {
		t.Fatalf("apply up: %v", err)
	}
	if !sessionsPlatformColumnsExist(ctx, t, db) {
		t.Fatal("platform columns absent after up")
	}

	goose.SetBaseFS(migrations.Migrations())
	defer goose.SetBaseFS(nil)
	if err := goose.SetDialect("postgres"); err != nil {
		t.Fatalf("set dialect: %v", err)
	}
	// Down past 00011 (to 00010) and assert the columns are gone.
	if err := goose.DownToContext(ctx, db, ".", 10); err != nil {
		t.Fatalf("down to 10: %v", err)
	}
	if sessionsPlatformColumnsExist(ctx, t, db) {
		t.Error("platform columns survived the down migration")
	}
	// Re-apply and assert they return.
	if err := goose.UpContext(ctx, db, "."); err != nil {
		t.Fatalf("re-apply up: %v", err)
	}
	if !sessionsPlatformColumnsExist(ctx, t, db) {
		t.Fatal("platform columns absent after re-apply")
	}
}

// sessionsPlatformColumnsExist reports whether both platform columns are present on
// backend.sessions.
func sessionsPlatformColumnsExist(ctx context.Context, t *testing.T, db *sql.DB) bool {
	t.Helper()
	var n int
	if err := db.QueryRowContext(ctx,
		`SELECT count(*) FROM information_schema.columns
		 WHERE table_schema = 'backend' AND table_name = 'sessions'
		   AND column_name IN ('platform_kind', 'platform_subtype')`).Scan(&n); err != nil {
		t.Fatalf("count platform columns: %v", err)
	}
	return n == 2
}
