/**
 * Federation M2 E2E test — peer-add enrollment flow (FED-M2-10).
 *
 * Covers MILESTONES.md acceptance test #6:
 *   "`peer add <url>` on Server A yields an `active` peer record with a valid cert + key"
 *
 * This test simulates two gateways using a single bootstrapped NestJS app:
 *   - "Server A": the admin API that generates a keypair and stores the cert
 *   - "Server B": the enrollment endpoint that signs the CSR
 *   Both share the same DB + Step-CA in the test environment.
 *
 * Prerequisites:
 *   docker compose -f docker-compose.federated.yml --profile federated up -d
 *
 * Run:
 *   FEDERATED_INTEGRATION=1 STEP_CA_AVAILABLE=1 \
 *   STEP_CA_URL=https://localhost:9000 \
 *   STEP_CA_PROVISIONER_KEY_JSON="$(docker exec $(docker ps -qf name=step-ca) cat /home/step/secrets/mosaic-fed.json)" \
 *   STEP_CA_ROOT_CERT_PATH=/tmp/step-ca-root.crt \
 *   pnpm --filter @mosaicstack/gateway test \
 *     src/__tests__/integration/federation-m2-e2e.integration.test.ts
 *
 * Obtaining Step-CA credentials:
 *   # Extract provisioner key from running container:
 *   #   docker exec $(docker ps -qf name=step-ca) cat /home/step/secrets/mosaic-fed.json
 *   # Copy root cert from container:
 *   #   docker cp $(docker ps -qf name=step-ca):/home/step/certs/root_ca.crt /tmp/step-ca-root.crt
 *   # Then: export STEP_CA_ROOT_CERT_PATH=/tmp/step-ca-root.crt
 *
 * Skipped unless both FEDERATED_INTEGRATION=1 and STEP_CA_AVAILABLE=1 are set.
 */

import * as crypto from 'node:crypto';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { Test } from '@nestjs/testing';
import { ValidationPipe } from '@nestjs/common';
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
import supertest from 'supertest';
import {
  createDb,
  type Db,
  type DbHandle,
  federationPeers,
  federationGrants,
  federationEnrollmentTokens,
  inArray,
  eq,
} from '@mosaicstack/db';
import * as schema from '@mosaicstack/db';
import { DB } from '../../database/database.module.js';
import { AdminGuard } from '../../admin/admin.guard.js';
import { FederationModule } from '../../federation/federation.module.js';
import { GrantsService } from '../../federation/grants.service.js';
import { EnrollmentService } from '../../federation/enrollment.service.js';

const run = process.env['FEDERATED_INTEGRATION'] === '1';
const stepCaRun =
  run &&
  process.env['STEP_CA_AVAILABLE'] === '1' &&
  !!process.env['STEP_CA_URL'] &&
  !!process.env['STEP_CA_PROVISIONER_KEY_JSON'] &&
  !!process.env['STEP_CA_ROOT_CERT_PATH'];

const PG_URL = 'postgresql://mosaic:mosaic@localhost:5433/mosaic';

const RUN_ID = crypto.randomUUID();

describe.skipIf(!stepCaRun)('federation M2 E2E — peer add enrollment flow', () => {
  let handle: DbHandle;
  let db: Db;
  let app: NestFastifyApplication;
  let agent: ReturnType<typeof supertest>;
  let grantsService: GrantsService;
  let enrollmentService: EnrollmentService;

  const createdTokenGrantIds: string[] = [];
  const createdGrantIds: string[] = [];
  const createdPeerIds: string[] = [];
  const createdUserIds: string[] = [];

  beforeAll(async () => {
    process.env['BETTER_AUTH_SECRET'] ??= 'test-e2e-sealing-key';

    handle = createDb(PG_URL);
    db = handle.db;

    const moduleRef = await Test.createTestingModule({
      imports: [FederationModule],
      providers: [{ provide: DB, useValue: db }],
    })
      .overrideGuard(AdminGuard)
      .useValue({ canActivate: () => true })
      .compile();

    app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
    await app.init();
    await app.getHttpAdapter().getInstance().ready();

    agent = supertest(app.getHttpServer());

    grantsService = moduleRef.get(GrantsService);
    enrollmentService = moduleRef.get(EnrollmentService);
  }, 30_000);

  afterAll(async () => {
    if (db && createdTokenGrantIds.length > 0) {
      await db
        .delete(federationEnrollmentTokens)
        .where(inArray(federationEnrollmentTokens.grantId, createdTokenGrantIds))
        .catch((e: unknown) => console.error('[federation-m2-e2e cleanup]', e));
    }
    if (db && createdGrantIds.length > 0) {
      await db
        .delete(federationGrants)
        .where(inArray(federationGrants.id, createdGrantIds))
        .catch((e: unknown) => console.error('[federation-m2-e2e cleanup]', e));
    }
    if (db && createdPeerIds.length > 0) {
      await db
        .delete(federationPeers)
        .where(inArray(federationPeers.id, createdPeerIds))
        .catch((e: unknown) => console.error('[federation-m2-e2e cleanup]', e));
    }
    if (db && createdUserIds.length > 0) {
      await db
        .delete(schema.users)
        .where(inArray(schema.users.id, createdUserIds))
        .catch((e: unknown) => console.error('[federation-m2-e2e cleanup]', e));
    }
    if (app)
      await app.close().catch((e: unknown) => console.error('[federation-m2-e2e cleanup]', e));
    if (handle)
      await handle.close().catch((e: unknown) => console.error('[federation-m2-e2e cleanup]', e));
  });

  // -------------------------------------------------------------------------
  // #6 — peer add: keypair → enrollment → cert storage → active peer record
  // -------------------------------------------------------------------------
  it('#6 — peer add flow: keypair → enrollment → cert storage → active peer record', async () => {
    // Create a subject user to satisfy FK on federation_grants.subject_user_id
    const userId = crypto.randomUUID();
    await db
      .insert(schema.users)
      .values({
        id: userId,
        name: `e2e-user-${RUN_ID}`,
        email: `e2e-${RUN_ID}@federation-test.invalid`,
        emailVerified: false,
      })
      .onConflictDoNothing();
    createdUserIds.push(userId);

    // ── Step A: "Server B" setup ─────────────────────────────────────────
    // Server B admin creates a grant and generates an enrollment token to
    // share out-of-band with Server A's operator.

    // Insert a placeholder peer on "Server B" to satisfy the grant FK
    const serverBPeerId = crypto.randomUUID();
    await db
      .insert(federationPeers)
      .values({
        id: serverBPeerId,
        commonName: `server-b-peer-${RUN_ID}`,
        displayName: 'Server B Placeholder',
        certPem: '-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----\n',
        certSerial: `serial-b-${serverBPeerId}`,
        certNotAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
        state: 'pending',
      })
      .onConflictDoNothing();
    createdPeerIds.push(serverBPeerId);

    const grant = await grantsService.createGrant({
      subjectUserId: userId,
      scope: { resources: ['tasks'], excluded_resources: [], max_rows_per_query: 100 },
      peerId: serverBPeerId,
    });
    createdGrantIds.push(grant.id);
    createdTokenGrantIds.push(grant.id);

    const { token } = await enrollmentService.createToken({
      grantId: grant.id,
      peerId: serverBPeerId,
      ttlSeconds: 900,
    });

    // ── Step B: "Server A" generates keypair ─────────────────────────────
    const keypairRes = await agent
      .post('/api/admin/federation/peers/keypair')
      .send({
        commonName: `e2e-peer-${RUN_ID.slice(0, 8)}`,
        displayName: 'E2E Test Peer',
        endpointUrl: 'https://test.invalid',
      })
      .set('Content-Type', 'application/json');

    expect(keypairRes.status).toBe(201);
    const { peerId, csrPem } = keypairRes.body as { peerId: string; csrPem: string };
    expect(typeof peerId).toBe('string');
    expect(csrPem).toContain('-----BEGIN CERTIFICATE REQUEST-----');
    createdPeerIds.push(peerId);

    // ── Step C: Enrollment (simulates Server A sending CSR to Server B) ──
    const enrollRes = await agent
      .post(`/api/federation/enrollment/${token}`)
      .send({ csrPem })
      .set('Content-Type', 'application/json');

    expect(enrollRes.status).toBe(200);
    const { certPem, certChainPem } = enrollRes.body as {
      certPem: string;
      certChainPem: string;
    };
    expect(certPem).toContain('-----BEGIN CERTIFICATE-----');
    expect(certChainPem).toContain('-----BEGIN CERTIFICATE-----');

    // ── Step D: "Server A" stores the cert ───────────────────────────────
    const storeRes = await agent
      .patch(`/api/admin/federation/peers/${peerId}/cert`)
      .send({ certPem })
      .set('Content-Type', 'application/json');

    expect(storeRes.status).toBe(200);

    // ── Step E: Verify peer record in DB ─────────────────────────────────
    const [peer] = await db
      .select()
      .from(federationPeers)
      .where(eq(federationPeers.id, peerId))
      .limit(1);

    expect(peer).toBeDefined();
    expect(peer?.state).toBe('active');
    expect(peer?.certPem).toContain('-----BEGIN CERTIFICATE-----');
    expect(typeof peer?.certSerial).toBe('string');
    expect((peer?.certSerial ?? '').length).toBeGreaterThan(0);
    // clientKeyPem is a sealed ciphertext — must not be a raw PEM
    expect(peer?.clientKeyPem?.startsWith('-----BEGIN')).toBe(false);
    // certNotAfter must be in the future
    expect(peer?.certNotAfter?.getTime()).toBeGreaterThan(Date.now());
  }, 60_000);
});
