/**
 * FED-M2-01 — Integration test: federation DB schema (peers / grants / audit_log).
 *
 * Prereq: docker compose -f docker-compose.federated.yml --profile federated up -d
 *         (or any postgres with the mosaic schema already applied)
 * Run:    FEDERATED_INTEGRATION=1 pnpm --filter @mosaicstack/db test src/federation.integration.test.ts
 *
 * Skipped when FEDERATED_INTEGRATION !== '1'.
 *
 * Strategy:
 *  - Applies the federation migration SQL directly (idempotent: CREATE TYPE/TABLE
 *    with IF NOT EXISTS guards applied via inline SQL before the migration DDL).
 *  - Assumes the base schema (users table etc.) already exists in the target DB.
 *  - All test rows use the `fed-m2-01-` prefix; cleanup in afterAll.
 *
 * Coverage:
 *  1. Federation tables + enums apply cleanly against the existing schema.
 *  2. Insert a sample user + peer + grant + audit row; verify round-trip.
 *  3. FK cascade: deleting the user cascades to federation_grants.
 *  4. FK set-null: deleting the peer sets federation_audit_log.peer_id to NULL.
 *  5. Enum constraint: inserting an invalid status/state value throws a DB error.
 *  6. Unique constraint: duplicate cert_serial throws a DB error.
 */

import postgres from 'postgres';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const run = process.env['FEDERATED_INTEGRATION'] === '1';

const PG_URL = process.env['DATABASE_URL'] ?? 'postgresql://mosaic:mosaic@localhost:5433/mosaic';

/** Recognisable test-row prefix for safe cleanup without full-table truncation. */
const T = 'fed-m2-01';

// Deterministic IDs (UUID format required for uuid PK columns: 8-4-4-4-12 hex digits).
const PEER1_ID = `f2000001-0000-4000-8000-000000000001`;
const PEER2_ID = `f2000002-0000-4000-8000-000000000002`;
const USER1_ID = `${T}-user-1`;

let sql: ReturnType<typeof postgres> | undefined;

beforeAll(async () => {
  if (!run) return;
  sql = postgres(PG_URL, { max: 1, connect_timeout: 10, idle_timeout: 10 });

  // Apply the federation enums and tables idempotently.
  // This mirrors the migration file but uses IF NOT EXISTS guards so it can run
  // against a DB that may not have had drizzle migrations tracked.
  await sql`
    DO $$ BEGIN
      CREATE TYPE peer_state AS ENUM ('pending', 'active', 'suspended', 'revoked');
    EXCEPTION WHEN duplicate_object THEN NULL;
    END $$
  `;
  await sql`
    DO $$ BEGIN
      CREATE TYPE grant_status AS ENUM ('active', 'revoked', 'expired');
    EXCEPTION WHEN duplicate_object THEN NULL;
    END $$
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS federation_peers (
      id              uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      common_name     text NOT NULL,
      display_name    text NOT NULL,
      cert_pem        text NOT NULL,
      cert_serial     text NOT NULL,
      cert_not_after  timestamp with time zone NOT NULL,
      client_key_pem  text,
      state           peer_state NOT NULL DEFAULT 'pending',
      endpoint_url    text,
      last_seen_at    timestamp with time zone,
      created_at      timestamp with time zone NOT NULL DEFAULT now(),
      revoked_at      timestamp with time zone,
      CONSTRAINT federation_peers_common_name_unique UNIQUE (common_name),
      CONSTRAINT federation_peers_cert_serial_unique UNIQUE (cert_serial)
    )
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_peers_cert_serial_idx ON federation_peers (cert_serial)
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_peers_state_idx ON federation_peers (state)
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS federation_grants (
      id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      subject_user_id  text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
      peer_id          uuid NOT NULL REFERENCES federation_peers(id) ON DELETE CASCADE,
      scope            jsonb NOT NULL,
      status           grant_status NOT NULL DEFAULT 'active',
      expires_at       timestamp with time zone,
      created_at       timestamp with time zone NOT NULL DEFAULT now(),
      revoked_at       timestamp with time zone,
      revoked_reason   text
    )
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_grants_subject_status_idx ON federation_grants (subject_user_id, status)
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_grants_peer_status_idx ON federation_grants (peer_id, status)
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS federation_audit_log (
      id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      request_id       text NOT NULL,
      peer_id          uuid REFERENCES federation_peers(id) ON DELETE SET NULL,
      subject_user_id  text REFERENCES users(id) ON DELETE SET NULL,
      grant_id         uuid REFERENCES federation_grants(id) ON DELETE SET NULL,
      verb             text NOT NULL,
      resource         text NOT NULL,
      status_code      integer NOT NULL,
      result_count     integer,
      denied_reason    text,
      latency_ms       integer,
      created_at       timestamp with time zone NOT NULL DEFAULT now(),
      query_hash       text,
      outcome          text,
      bytes_out        integer
    )
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_audit_log_peer_created_at_idx
      ON federation_audit_log (peer_id, created_at DESC NULLS LAST)
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_audit_log_subject_created_at_idx
      ON federation_audit_log (subject_user_id, created_at DESC NULLS LAST)
  `;
  await sql`
    CREATE INDEX IF NOT EXISTS federation_audit_log_created_at_idx
      ON federation_audit_log (created_at DESC NULLS LAST)
  `;
});

afterAll(async () => {
  if (!sql) return;

  // Cleanup in FK-safe order (children before parents).
  await sql`DELETE FROM federation_audit_log WHERE request_id LIKE ${T + '%'}`.catch(() => {});
  await sql`
    DELETE FROM federation_grants
    WHERE subject_user_id LIKE ${T + '%'}
       OR revoked_reason LIKE ${T + '%'}
  `.catch(() => {});
  await sql`DELETE FROM federation_peers WHERE common_name LIKE ${T + '%'}`.catch(() => {});
  await sql`DELETE FROM users WHERE id LIKE ${T + '%'}`.catch(() => {});
  await sql.end({ timeout: 3 }).catch(() => {});
});

describe.skipIf(!run)('federation schema — integration', () => {
  // ── 1. Insert sample rows ──────────────────────────────────────────────────

  it('inserts a user, peer, grant, and audit row without constraint violation', async () => {
    const certPem = '-----BEGIN CERTIFICATE-----\nMIItest\n-----END CERTIFICATE-----';

    // User — BetterAuth users.id is text (any string, not uuid).
    await sql!`
      INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
      VALUES (${USER1_ID}, ${'M2-01 Test User'}, ${USER1_ID + '@example.com'}, false, now(), now())
      ON CONFLICT (id) DO NOTHING
    `;

    // Peer
    await sql!`
      INSERT INTO federation_peers
        (id, common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at)
      VALUES (
        ${PEER1_ID},
        ${T + '-gateway-example-com'},
        ${'Test Peer'},
        ${certPem},
        ${T + '-serial-001'},
        now() + interval '1 year',
        ${'active'},
        now()
      )
      ON CONFLICT (id) DO NOTHING
    `;

    // Grant — scope is jsonb; pass as JSON string and cast server-side.
    const scopeJson = JSON.stringify({
      resources: ['tasks', 'notes'],
      operations: ['list', 'get'],
    });
    const grants = await sql!`
      INSERT INTO federation_grants
        (subject_user_id, peer_id, scope, status, created_at)
      VALUES (
        ${USER1_ID},
        ${PEER1_ID},
        ${scopeJson}::jsonb,
        ${'active'},
        now()
      )
      RETURNING id
    `;
    expect(grants).toHaveLength(1);
    const grantId = grants[0]!['id'] as string;

    // Audit log row
    await sql!`
      INSERT INTO federation_audit_log
        (request_id, peer_id, subject_user_id, grant_id, verb, resource, status_code, created_at)
      VALUES (
        ${T + '-req-001'},
        ${PEER1_ID},
        ${USER1_ID},
        ${grantId},
        ${'list'},
        ${'tasks'},
        ${200},
        now()
      )
    `;

    // Verify the audit row is present and has correct data.
    const auditRows = await sql!`
      SELECT * FROM federation_audit_log WHERE request_id = ${T + '-req-001'}
    `;
    expect(auditRows).toHaveLength(1);
    expect(auditRows[0]!['status_code']).toBe(200);
    expect(auditRows[0]!['verb']).toBe('list');
    expect(auditRows[0]!['resource']).toBe('tasks');
  }, 30_000);

  // ── 2. FK cascade: user delete cascades grants ─────────────────────────────

  it('cascade-deletes federation_grants when the subject user is deleted', async () => {
    const cascadeUserId = `${T}-cascade-user`;
    await sql!`
      INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
      VALUES (${cascadeUserId}, ${'Cascade User'}, ${cascadeUserId + '@example.com'}, false, now(), now())
      ON CONFLICT (id) DO NOTHING
    `;
    const scopeJson = JSON.stringify({ resources: ['tasks'] });
    await sql!`
      INSERT INTO federation_grants
        (subject_user_id, peer_id, scope, status, revoked_reason, created_at)
      VALUES (
        ${cascadeUserId},
        ${PEER1_ID},
        ${scopeJson}::jsonb,
        ${'active'},
        ${T + '-cascade-test'},
        now()
      )
    `;

    const before = await sql!`
      SELECT count(*)::int AS cnt FROM federation_grants WHERE subject_user_id = ${cascadeUserId}
    `;
    expect(before[0]!['cnt']).toBe(1);

    // Delete user → grants should cascade-delete.
    await sql!`DELETE FROM users WHERE id = ${cascadeUserId}`;

    const after = await sql!`
      SELECT count(*)::int AS cnt FROM federation_grants WHERE subject_user_id = ${cascadeUserId}
    `;
    expect(after[0]!['cnt']).toBe(0);
  }, 15_000);

  // ── 3. FK set-null: peer delete sets audit_log.peer_id to NULL ────────────

  it('sets federation_audit_log.peer_id to NULL when the peer is deleted', async () => {
    // Insert a throwaway peer for this specific cascade test.
    await sql!`
      INSERT INTO federation_peers
        (id, common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at)
      VALUES (
        ${PEER2_ID},
        ${T + '-gateway-throwaway-com'},
        ${'Throwaway Peer'},
        ${'cert-pem-placeholder'},
        ${T + '-serial-002'},
        now() + interval '1 year',
        ${'active'},
        now()
      )
      ON CONFLICT (id) DO NOTHING
    `;
    const reqId = `${T}-req-setnull`;
    await sql!`
      INSERT INTO federation_audit_log
        (request_id, peer_id, subject_user_id, verb, resource, status_code, created_at)
      VALUES (
        ${reqId},
        ${PEER2_ID},
        ${USER1_ID},
        ${'get'},
        ${'tasks'},
        ${200},
        now()
      )
    `;

    await sql!`DELETE FROM federation_peers WHERE id = ${PEER2_ID}`;

    const rows = await sql!`
      SELECT peer_id FROM federation_audit_log WHERE request_id = ${reqId}
    `;
    expect(rows).toHaveLength(1);
    expect(rows[0]!['peer_id']).toBeNull();
  }, 15_000);

  // ── 4. Enum constraint: invalid grant_status rejected ─────────────────────

  it('rejects an invalid grant_status value with a DB error', async () => {
    const scopeJson = JSON.stringify({ resources: ['tasks'] });
    await expect(
      sql!`
        INSERT INTO federation_grants
          (subject_user_id, peer_id, scope, status, created_at)
        VALUES (
          ${USER1_ID},
          ${PEER1_ID},
          ${scopeJson}::jsonb,
          ${'invalid_status'},
          now()
        )
      `,
    ).rejects.toThrow();
  }, 10_000);

  // ── 5. Enum constraint: invalid peer_state rejected ───────────────────────

  it('rejects an invalid peer_state value with a DB error', async () => {
    await expect(
      sql!`
        INSERT INTO federation_peers
          (common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at)
        VALUES (
          ${'bad-state-peer'},
          ${'Bad State'},
          ${'pem'},
          ${'bad-serial-999'},
          now() + interval '1 year',
          ${'invalid_state'},
          now()
        )
      `,
    ).rejects.toThrow();
  }, 10_000);

  // ── 6. Unique constraint: duplicate cert_serial rejected ──────────────────

  it('rejects a duplicate cert_serial with a unique constraint violation', async () => {
    await expect(
      sql!`
        INSERT INTO federation_peers
          (common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at)
        VALUES (
          ${T + '-dup-cn'},
          ${'Dup Peer'},
          ${'pem'},
          ${T + '-serial-001'},
          now() + interval '1 year',
          ${'pending'},
          now()
        )
      `,
    ).rejects.toThrow();
  }, 10_000);

  // ── 7. FK cascade: peer delete cascades to federation_grants ─────────────

  it('cascade-deletes federation_grants when the owning peer is deleted', async () => {
    const PEER3_ID = `f2000003-0000-4000-8000-000000000003`;
    const cascadeGrantUserId = `${T}-cascade-grant-user`;

    // Insert a dedicated user and peer for this test.
    await sql!`
      INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
      VALUES (${cascadeGrantUserId}, ${'Cascade Grant User'}, ${cascadeGrantUserId + '@example.com'}, false, now(), now())
      ON CONFLICT (id) DO NOTHING
    `;
    await sql!`
      INSERT INTO federation_peers
        (id, common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at)
      VALUES (
        ${PEER3_ID},
        ${T + '-gateway-cascade-peer'},
        ${'Cascade Peer'},
        ${'cert-pem-cascade'},
        ${T + '-serial-003'},
        now() + interval '1 year',
        ${'active'},
        now()
      )
      ON CONFLICT (id) DO NOTHING
    `;

    const scopeJson = JSON.stringify({ resources: ['tasks'] });
    await sql!`
      INSERT INTO federation_grants
        (subject_user_id, peer_id, scope, status, created_at)
      VALUES (
        ${cascadeGrantUserId},
        ${PEER3_ID},
        ${scopeJson}::jsonb,
        ${'active'},
        now()
      )
    `;

    const before = await sql!`
      SELECT count(*)::int AS cnt FROM federation_grants WHERE peer_id = ${PEER3_ID}
    `;
    expect(before[0]!['cnt']).toBe(1);

    // Delete peer → grants should cascade-delete.
    await sql!`DELETE FROM federation_peers WHERE id = ${PEER3_ID}`;

    const after = await sql!`
      SELECT count(*)::int AS cnt FROM federation_grants WHERE peer_id = ${PEER3_ID}
    `;
    expect(after[0]!['cnt']).toBe(0);

    // Cleanup
    await sql!`DELETE FROM users WHERE id = ${cascadeGrantUserId}`.catch(() => {});
  }, 15_000);
});
