package main

import (
	"context"
	"errors"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"
)

func TestMigrationStatementsSupportsConcurrentIndexes(t *testing.T) {
	payload := `-- easyai:migration:no-transaction
CREATE INDEX CONCURRENTLY IF NOT EXISTS first_index ON first_table(id);
-- easyai:migration:statement
CREATE INDEX CONCURRENTLY IF NOT EXISTS second_index ON second_table(id);
`
	noTransaction, statements := migrationStatements(payload)
	if !noTransaction {
		t.Fatal("expected a non-transaction migration")
	}
	if len(statements) != 2 {
		t.Fatalf("statements=%d, want 2", len(statements))
	}
	if !strings.Contains(statements[0], "first_index") || !strings.Contains(statements[1], "second_index") {
		t.Fatalf("unexpected statements: %#v", statements)
	}
}

func TestSeedanceInputImageConstraintMigrationKeepsCatalogSnapshotsInSync(t *testing.T) {
	payload, err := os.ReadFile("../../migrations/0078_seedance_input_image_constraints.sql")
	if err != nil {
		t.Fatal(err)
	}
	content := string(payload)
	for _, required := range []string{
		"easyai:豆包Seedance-2.0",
		"easyai:豆包Seedance-2.0-fast",
		"input_image_resolution_range",
		"input_image_aspect_ratio_range",
		"'{metadata,rawModel,capabilities}'",
		"UPDATE platform_models",
	} {
		if !strings.Contains(content, required) {
			t.Fatalf("Seedance input image migration is missing %q", required)
		}
	}
}

func TestSecurityEventSchemaMigrationsDefineCurrentLifecycle(t *testing.T) {
	streamPayload, err := os.ReadFile("../../migrations/0063_oidc_security_events.sql")
	if err != nil {
		t.Fatalf("read security event stream migration: %v", err)
	}
	streamSQL := string(streamPayload)
	for _, statement := range []string{
		"CREATE TABLE IF NOT EXISTS gateway_security_event_stream_state",
		"CREATE TABLE IF NOT EXISTS gateway_security_event_verification_challenges",
		"CREATE INDEX IF NOT EXISTS idx_gateway_security_event_challenges_expiry",
	} {
		if !strings.Contains(streamSQL, statement) {
			t.Fatalf("security event stream migration is missing %q", statement)
		}
	}

	connectionPayload, err := os.ReadFile("../../migrations/0064_security_event_connections.sql")
	if err != nil {
		t.Fatalf("read security event connection migration: %v", err)
	}
	connectionSQL := string(connectionPayload)
	for _, statement := range []string{
		"CREATE TABLE IF NOT EXISTS gateway_security_event_connections",
		"management_client_id text",
		"management_credential_ref text",
		"CREATE TABLE IF NOT EXISTS gateway_security_event_connection_idempotency",
	} {
		if !strings.Contains(connectionSQL, statement) {
			t.Fatalf("security event connection migration is missing %q", statement)
		}
	}
}

func TestIdentityConfigurationRevisionMigrationDefinesSecretReferencesAndSingleActiveRevision(t *testing.T) {
	payload, err := os.ReadFile("../../migrations/0065_identity_configuration_revisions.sql")
	if err != nil {
		t.Fatal(err)
	}
	content := string(payload)
	for _, required := range []string{
		"CREATE TABLE IF NOT EXISTS gateway_identity_configuration_revisions",
		"machine_credential_ref text",
		"session_encryption_key_ref text",
		"security_event_configuration_url text",
		"WHERE state = 'active'",
		"CHECK (state IN ('draft','validated','active','superseded','failed'))",
	} {
		if !strings.Contains(content, required) {
			t.Fatalf("identity configuration migration is missing %q", required)
		}
	}
	for _, forbidden := range []string{"machine_secret", "client_secret", "exchange_token text", "onboarding_code text"} {
		if strings.Contains(strings.ToLower(content), forbidden) {
			t.Fatalf("identity configuration migration stores forbidden secret field %q", forbidden)
		}
	}
}

func TestIdentityOnboardingExchangeMigrationStoresOnlySecretReferences(t *testing.T) {
	payload, err := os.ReadFile("../../migrations/0066_identity_onboarding_exchanges.sql")
	if err != nil {
		t.Fatal(err)
	}
	content := strings.ToLower(string(payload))
	for _, required := range []string{
		"create table if not exists gateway_identity_onboarding_exchanges",
		"exchange_token_ref text not null",
		"cleanup_status text not null default 'none'",
		"security_event_credential_handoff_unsafe",
	} {
		if !strings.Contains(content, required) {
			t.Fatalf("identity onboarding migration is missing %q", required)
		}
	}
	for _, forbidden := range []string{"exchange_token text", "onboarding_code", "client_secret", "machine_secret"} {
		if strings.Contains(content, forbidden) {
			t.Fatalf("identity onboarding migration stores forbidden secret field %q", forbidden)
		}
	}
}

func TestIdentityOnboardingMigrationDefinesCancellationLifecycle(t *testing.T) {
	payload, err := os.ReadFile("../../migrations/0066_identity_onboarding_exchanges.sql")
	if err != nil {
		t.Fatal(err)
	}
	content := strings.ToLower(string(payload))
	for _, required := range []string{
		"'cancelled'",
		"cleanup_status text not null default 'none'",
		"cleanup_completed_at",
		"idx_gateway_identity_onboarding_cleanup",
		"last_error_category",
	} {
		if !strings.Contains(content, required) {
			t.Fatalf("identity pairing cancellation migration is missing %q", required)
		}
	}
	for _, forbidden := range []string{"exchange_token text", "client_secret", "machine_secret"} {
		if strings.Contains(content, forbidden) {
			t.Fatalf("identity pairing cancellation migration stores forbidden secret field %q", forbidden)
		}
	}
}

func TestIdentityOnboardingMigrationDefinesFinalErrorCategories(t *testing.T) {
	payload, err := os.ReadFile("../../migrations/0066_identity_onboarding_exchanges.sql")
	if err != nil {
		t.Fatal(err)
	}
	content := strings.ToLower(string(payload))
	for _, required := range []string{
		"gateway_identity_onboarding_error_category_check",
		"security_event_credential_handoff_unsafe",
		"security_event_connection_binding_missing",
		"security_event_connection_binding_unavailable",
		"security_event_connection_binding_invalid",
		"security_event_connection_binding_mismatch",
		"cleanup_security_event_credential_handoff_unsafe",
	} {
		if !strings.Contains(content, required) {
			t.Fatalf("identity pairing error category migration is missing %q", required)
		}
	}
}

func TestIdentityPairingErrorCategoriesExecuteAgainstCurrentSchema(t *testing.T) {
	pool := newIdentityMigrationPostgresTestSchema(t)
	ctx := context.Background()
	for _, migration := range []string{
		"../../migrations/0065_identity_configuration_revisions.sql",
		"../../migrations/0066_identity_onboarding_exchanges.sql",
		"../../migrations/0067_identity_secret_cleanup_queue.sql",
		"../../migrations/0068_identity_pairing_start_reservation.sql",
	} {
		applyIdentityMigrationTestFile(t, ctx, pool, migration)
	}

	revisionID, pairingID := uuid.NewString(), uuid.NewString()
	if _, err := pool.Exec(ctx, `
INSERT INTO gateway_identity_configuration_revisions (
  id,state,auth_center_url,local_tenant_key,public_base_url,web_base_url
) VALUES ($1::uuid,'draft','https://auth.test.example','default',
  'https://gateway.test.example','https://gateway-web.test.example')`, revisionID); err != nil {
		t.Fatalf("seed identity revision: %v", err)
	}
	if _, err := pool.Exec(ctx, `
INSERT INTO gateway_identity_onboarding_exchanges (
  id,revision_id,remote_exchange_id,exchange_token_ref,status,remote_version,expires_at
) VALUES ($1::uuid,$2::uuid,$3::uuid,$4,'credentials_saved',4,now() + interval '30 minutes')`,
		pairingID, revisionID, uuid.NewString(), "identity-exchange-"+pairingID); err != nil {
		t.Fatalf("seed onboarding exchange: %v", err)
	}

	for _, category := range []string{
		"security_event_credential_handoff_unsafe",
		"security_event_connection_binding_missing",
		"security_event_connection_binding_unavailable",
		"security_event_connection_binding_invalid",
		"security_event_connection_binding_mismatch",
		"cleanup_security_event_credential_handoff_unsafe",
	} {
		if _, err := pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET last_error_category=$2 WHERE id=$1::uuid`, pairingID, category); err != nil {
			t.Fatalf("persist supported error category %q: %v", category, err)
		}
	}
	_, err := pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET last_error_category='credential_secret_leaked' WHERE id=$1::uuid`, pairingID)
	requireIdentityMigrationCheckViolation(t, err, "unknown upgraded identity pairing category")
}

func TestIdentitySecretCleanupQueueMigrationStoresOnlyReferences(t *testing.T) {
	payload, err := os.ReadFile("../../migrations/0067_identity_secret_cleanup_queue.sql")
	if err != nil {
		t.Fatal(err)
	}
	content := strings.ToLower(string(payload))
	for _, required := range []string{
		"create table if not exists gateway_identity_secret_cleanup_queue",
		"secret_ref text primary key",
		"not_before timestamptz not null",
		"status text not null default 'pending'",
		"claim_token uuid",
		"lease_expires_at timestamptz",
		"gateway_identity_secret_cleanup_claim_check",
		"idx_gateway_identity_secret_cleanup_due",
	} {
		if !strings.Contains(content, required) {
			t.Fatalf("identity Secret cleanup migration is missing %q", required)
		}
	}
	for _, forbidden := range []string{"secret_value", "client_secret", "machine_secret", "token text"} {
		if strings.Contains(content, forbidden) {
			t.Fatalf("identity Secret cleanup migration stores forbidden value field %q", forbidden)
		}
	}
}

func TestIdentityPairingCancellationLifecycleRejectsPartialStates(t *testing.T) {
	pool := newIdentityMigrationPostgresTestSchema(t)
	ctx := context.Background()
	applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0065_identity_configuration_revisions.sql")
	applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0066_identity_onboarding_exchanges.sql")

	revisionID, pairingID := uuid.NewString(), uuid.NewString()
	if _, err := pool.Exec(ctx, `
INSERT INTO gateway_identity_configuration_revisions (
  id,state,auth_center_url,local_tenant_key,public_base_url,web_base_url
) VALUES ($1::uuid,'draft','https://auth.test.example','default',
  'https://gateway.test.example','https://gateway-web.test.example')`, revisionID); err != nil {
		t.Fatalf("seed legacy identity revision: %v", err)
	}
	if _, err := pool.Exec(ctx, `
INSERT INTO gateway_identity_onboarding_exchanges (
  id,revision_id,remote_exchange_id,exchange_token_ref,status,remote_version,expires_at,last_error_category
) VALUES ($1::uuid,$2::uuid,$3::uuid,$4,'credentials_saved',4,now() + interval '30 minutes','pairing_step_failed')`,
		pairingID, revisionID, uuid.NewString(), "identity-exchange-"+pairingID); err != nil {
		t.Fatalf("seed legacy onboarding exchange: %v", err)
	}

	var status, cleanupStatus, errorCategory string
	var cancelledAt, cleanupCompletedAt *time.Time
	if err := pool.QueryRow(ctx, `
SELECT status,cleanup_status,last_error_category,cancelled_at,cleanup_completed_at
FROM gateway_identity_onboarding_exchanges WHERE id=$1::uuid`, pairingID).
		Scan(&status, &cleanupStatus, &errorCategory, &cancelledAt, &cleanupCompletedAt); err != nil {
		t.Fatalf("read migrated onboarding exchange: %v", err)
	}
	if status != "credentials_saved" || cleanupStatus != "none" || errorCategory != "pairing_step_failed" ||
		cancelledAt != nil || cleanupCompletedAt != nil {
		t.Fatalf("unexpected migrated exchange status=%q cleanup=%q error=%q cancelled=%v completed=%v",
			status, cleanupStatus, errorCategory, cancelledAt, cleanupCompletedAt)
	}

	_, err := pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges SET status='cancelled' WHERE id=$1::uuid`, pairingID)
	requireIdentityMigrationCheckViolation(t, err, "cancelled status without cleanup intent")

	if _, err := pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET status='cancelled',cleanup_status='pending',cancelled_at=now()
WHERE id=$1::uuid`, pairingID); err != nil {
		t.Fatalf("persist valid pending cleanup lifecycle: %v", err)
	}
	_, err = pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET cleanup_status='completed' WHERE id=$1::uuid`, pairingID)
	requireIdentityMigrationCheckViolation(t, err, "completed cleanup without completion timestamp")

	if _, err := pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET cleanup_status='completed',cleanup_completed_at=now()
WHERE id=$1::uuid`, pairingID); err != nil {
		t.Fatalf("persist valid completed cleanup lifecycle: %v", err)
	}
	_, err = pool.Exec(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET last_error_category='still-invalid' WHERE id=$1::uuid`, pairingID)
	requireIdentityMigrationCheckViolation(t, err, "invalid post-migration error category")

	if err := pool.QueryRow(ctx, `SELECT status,cleanup_status FROM gateway_identity_onboarding_exchanges WHERE id=$1::uuid`, pairingID).
		Scan(&status, &cleanupStatus); err != nil {
		t.Fatalf("read completed cleanup lifecycle: %v", err)
	}
	if status != "cancelled" || cleanupStatus != "completed" {
		t.Fatalf("completed cleanup lifecycle status=%q cleanup=%q", status, cleanupStatus)
	}
}

func TestOIDCSessionClientIdentityRepairMigrationUpgradesApplied0090Schema(t *testing.T) {
	pool := newIdentityMigrationPostgresTestSchema(t)
	ctx := context.Background()
	if _, err := pool.Exec(ctx, `
CREATE TABLE gateway_oidc_sessions (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  oidc_user_binding_id uuid
)`); err != nil {
		t.Fatalf("create pre-repair OIDC session schema: %v", err)
	}

	applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0091_oidc_session_client_identity.sql")
	applyIdentityMigrationTestFile(t, ctx, pool, "../../migrations/0091_oidc_session_client_identity.sql")

	var dataType string
	if err := pool.QueryRow(ctx, `
SELECT data_type
FROM information_schema.columns
WHERE table_schema=current_schema()
  AND table_name='gateway_oidc_sessions'
  AND column_name='oidc_client_id'`).Scan(&dataType); err != nil {
		t.Fatalf("read repaired OIDC session client identity column: %v", err)
	}
	if dataType != "text" {
		t.Fatalf("oidc_client_id data type=%q, want text", dataType)
	}
}

func applyIdentityMigrationTestFile(t *testing.T, ctx context.Context, pool *pgxpool.Pool, path string) {
	t.Helper()
	payload, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read migration %s: %v", path, err)
	}
	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatalf("begin migration %s: %v", path, err)
	}
	if _, err := tx.Exec(ctx, string(payload)); err != nil {
		_ = tx.Rollback(ctx)
		t.Fatalf("execute migration %s: %v", path, err)
	}
	if err := tx.Commit(ctx); err != nil {
		t.Fatalf("commit migration %s: %v", path, err)
	}
}

func requireIdentityMigrationCheckViolation(t *testing.T, err error, operation string) {
	t.Helper()
	if err == nil {
		t.Fatalf("%s unexpectedly satisfied migration constraints", operation)
	}
	var postgresError *pgconn.PgError
	if !errors.As(err, &postgresError) || postgresError.Code != "23514" {
		t.Fatalf("%s error=%v, want PostgreSQL check violation", operation, err)
	}
}

func newIdentityMigrationPostgresTestSchema(t *testing.T) *pgxpool.Pool {
	t.Helper()
	databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
	if databaseURL == "" {
		t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run identity migration PostgreSQL integration tests")
	}
	ctx := context.Background()
	admin, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatalf("connect identity migration test database: %v", err)
	}
	var databaseName string
	if err := admin.QueryRow(ctx, `SELECT current_database()`).Scan(&databaseName); err != nil {
		admin.Close()
		t.Fatalf("read identity migration test database name: %v", err)
	}
	if !strings.Contains(strings.ToLower(databaseName), "test") {
		admin.Close()
		t.Fatalf("refusing to use non-test database %q", databaseName)
	}

	schemaName := "gateway_identity_migration_" + strings.ReplaceAll(uuid.NewString(), "-", "")
	schemaIdentifier := pgx.Identifier{schemaName}.Sanitize()
	if _, err := admin.Exec(ctx, `CREATE SCHEMA `+schemaIdentifier); err != nil {
		admin.Close()
		t.Fatalf("create identity migration test schema: %v", err)
	}
	config, err := pgxpool.ParseConfig(databaseURL)
	if err != nil {
		_, _ = admin.Exec(ctx, `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`)
		admin.Close()
		t.Fatalf("parse identity migration test database URL: %v", err)
	}
	config.ConnConfig.RuntimeParams["search_path"] = schemaName
	pool, err := pgxpool.NewWithConfig(ctx, config)
	if err != nil {
		_, _ = admin.Exec(ctx, `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`)
		admin.Close()
		t.Fatalf("connect identity migration test schema: %v", err)
	}
	t.Cleanup(func() {
		pool.Close()
		if _, err := admin.Exec(context.Background(), `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`); err != nil {
			t.Errorf("drop identity migration test schema: %v", err)
		}
		admin.Close()
	})
	return pool
}
