package store

import (
	"context"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/accessruleaudit"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

func TestAccessRuleAllowWhitelistMigrationArchivesAndRemovesLegacyAllows(t *testing.T) {
	databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
	if databaseURL == "" {
		t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run access-rule migration PostgreSQL integration tests")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()

	admin, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatalf("connect access-rule migration test database: %v", err)
	}
	t.Cleanup(admin.Close)
	var databaseName string
	if err := admin.QueryRow(ctx, `SELECT current_database()`).Scan(&databaseName); err != nil {
		t.Fatalf("read access-rule migration test database name: %v", err)
	}
	if !strings.Contains(strings.ToLower(databaseName), "test") {
		t.Fatalf("refusing to use non-test database %q", databaseName)
	}
	if _, err := admin.Exec(ctx, `CREATE EXTENSION IF NOT EXISTS pgcrypto`); err != nil {
		t.Fatalf("ensure pgcrypto: %v", err)
	}

	schemaName := "gateway_access_rule_migration_" + strings.ReplaceAll(time.Now().UTC().Format("20060102150405.000000000"), ".", "")
	schemaIdentifier := pgx.Identifier{schemaName}.Sanitize()
	if _, err := admin.Exec(ctx, `CREATE SCHEMA `+schemaIdentifier); err != nil {
		t.Fatalf("create access-rule migration test schema: %v", err)
	}
	t.Cleanup(func() {
		if _, err := admin.Exec(context.Background(), `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`); err != nil {
			t.Errorf("drop access-rule migration test schema: %v", err)
		}
	})

	config, err := pgxpool.ParseConfig(databaseURL)
	if err != nil {
		t.Fatalf("parse access-rule migration test database URL: %v", err)
	}
	config.ConnConfig.RuntimeParams["search_path"] = schemaName + ",public"
	pool, err := pgxpool.NewWithConfig(ctx, config)
	if err != nil {
		t.Fatalf("connect access-rule migration test schema: %v", err)
	}
	t.Cleanup(pool.Close)

	if _, err := pool.Exec(ctx, `
CREATE TABLE gateway_access_rules (
  id uuid PRIMARY KEY,
  subject_type text NOT NULL,
  subject_id uuid NOT NULL,
  resource_type text NOT NULL,
  resource_id uuid NOT NULL,
  effect text NOT NULL,
  priority integer NOT NULL DEFAULT 100,
  min_permission_level integer NOT NULL DEFAULT 0,
  conditions jsonb NOT NULL DEFAULT '{}'::jsonb,
  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
  status text NOT NULL DEFAULT 'active',
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO gateway_access_rules (
  id, subject_type, subject_id, resource_type, resource_id, effect,
  priority, min_permission_level, conditions, metadata, status,
  created_at, updated_at
) VALUES
  (
    '10000000-0000-0000-0000-000000000001', 'user_group',
    '20000000-0000-0000-0000-000000000001', 'platform_model',
    '30000000-0000-0000-0000-000000000001', 'allow', 10, 0,
    '{"region":"cn"}', '{"source":"legacy"}', 'active',
    '2026-08-01T00:00:00Z', '2026-08-01T01:00:00Z'
  ),
  (
    '10000000-0000-0000-0000-000000000002', 'api_key',
    '20000000-0000-0000-0000-000000000002', 'platform',
    '30000000-0000-0000-0000-000000000002', 'allow', 20, 1,
    '{}', '{"source":"disabled-legacy"}', 'disabled',
    '2026-08-02T00:00:00Z', '2026-08-02T01:00:00Z'
  ),
  (
    '10000000-0000-0000-0000-000000000003', 'user',
    '20000000-0000-0000-0000-000000000003', 'base_model',
    '30000000-0000-0000-0000-000000000003', 'deny', 30, 2,
    '{"reason":"blocked"}', '{"source":"deny-must-remain"}', 'active',
    '2026-08-03T00:00:00Z', '2026-08-03T01:00:00Z'
  );`); err != nil {
		t.Fatalf("seed pre-migration access rules: %v", err)
	}
	beforeAudit, err := accessruleaudit.Export(ctx, pool, time.Date(2026, 8, 3, 0, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("export pre-migration access-rule audit: %v", err)
	}

	_, currentFile, _, _ := runtime.Caller(0)
	migrationPath := filepath.Join(filepath.Dir(currentFile), "..", "..", "migrations", "0102_access_rule_allow_whitelist_semantics.sql")
	migration, err := os.ReadFile(migrationPath)
	if err != nil {
		t.Fatalf("read access-rule whitelist migration: %v", err)
	}
	if _, err := pool.Exec(ctx, string(migration)); err != nil {
		t.Fatalf("apply access-rule whitelist migration: %v", err)
	}
	afterAudit, err := accessruleaudit.Export(ctx, pool, time.Date(2026, 8, 3, 0, 1, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("export post-migration access-rule audit: %v", err)
	}
	if err := accessruleaudit.VerifyMigration(beforeAudit, afterAudit); err != nil {
		t.Fatalf("verify pre/post access-rule audit: %v", err)
	}

	var liveAllowCount, liveDenyCount, archivedAllowCount int64
	if err := pool.QueryRow(ctx, `SELECT COUNT(*) FILTER (WHERE effect = 'allow'), COUNT(*) FILTER (WHERE effect = 'deny') FROM gateway_access_rules`).Scan(&liveAllowCount, &liveDenyCount); err != nil {
		t.Fatalf("read migrated live rule counts: %v", err)
	}
	if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM gateway_access_rule_allow_archive WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics'`).Scan(&archivedAllowCount); err != nil {
		t.Fatalf("read archived allow count: %v", err)
	}
	if liveAllowCount != 0 || liveDenyCount != 1 || archivedAllowCount != 2 {
		t.Fatalf("unexpected migration counts: liveAllow=%d liveDeny=%d archivedAllow=%d", liveAllowCount, liveDenyCount, archivedAllowCount)
	}

	var expectedAllowCount int64
	var expectedAllowSHA, archivedAllowSHA string
	if err := pool.QueryRow(ctx, `
SELECT allow_count, allow_sha256
FROM gateway_access_rule_migration_batches
WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics'`).Scan(&expectedAllowCount, &expectedAllowSHA); err != nil {
		t.Fatalf("read access-rule migration batch: %v", err)
	}
	if err := pool.QueryRow(ctx, `
SELECT encode(digest(COALESCE(string_agg(row_sha256, E'\n' ORDER BY rule_id), ''), 'sha256'), 'hex')
FROM gateway_access_rule_allow_archive
WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics'`).Scan(&archivedAllowSHA); err != nil {
		t.Fatalf("hash archived allow rules: %v", err)
	}
	if expectedAllowCount != archivedAllowCount || expectedAllowSHA != archivedAllowSHA {
		t.Fatalf("archive manifest mismatch: count=%d/%d sha=%s/%s", expectedAllowCount, archivedAllowCount, expectedAllowSHA, archivedAllowSHA)
	}

	var denyMetadata, denyConditions, denyStatus string
	if err := pool.QueryRow(ctx, `
SELECT metadata::text, conditions::text, status
FROM gateway_access_rules
WHERE id = '10000000-0000-0000-0000-000000000003'`).Scan(&denyMetadata, &denyConditions, &denyStatus); err != nil {
		t.Fatalf("read preserved deny rule: %v", err)
	}
	if denyMetadata != `{"source": "deny-must-remain"}` || denyConditions != `{"reason": "blocked"}` || denyStatus != "active" {
		t.Fatalf("deny rule changed: metadata=%s conditions=%s status=%s", denyMetadata, denyConditions, denyStatus)
	}

	// Reapplying without new allows is idempotent. If new whitelist rules have
	// been written, the recorded legacy batch checksum makes reapplication fail
	// before any new rule can be deleted.
	if _, err := pool.Exec(ctx, string(migration)); err != nil {
		t.Fatalf("reapply access-rule whitelist migration: %v", err)
	}
	if _, err := pool.Exec(ctx, `
INSERT INTO gateway_access_rules (
  id, subject_type, subject_id, resource_type, resource_id, effect
) VALUES (
  '10000000-0000-0000-0000-000000000004', 'api_key',
  '20000000-0000-0000-0000-000000000004', 'platform_model',
  '30000000-0000-0000-0000-000000000004', 'allow'
)`); err != nil {
		t.Fatalf("insert post-migration whitelist rule: %v", err)
	}
	if _, err := pool.Exec(ctx, string(migration)); err == nil {
		t.Fatal("reapplying the legacy cleanup after new whitelist writes must fail")
	}
	if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM gateway_access_rules WHERE id = '10000000-0000-0000-0000-000000000004'`).Scan(&liveAllowCount); err != nil {
		t.Fatalf("read post-migration whitelist rule: %v", err)
	}
	if liveAllowCount != 1 {
		t.Fatalf("failed migration reapply removed a new whitelist rule: count=%d", liveAllowCount)
	}
}
