import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { generateKeyPairSync } from 'node:crypto';
import { PrismaClient } from '@prisma/client';
import {
  ContextAttestationVerifier,
  signDevAttestation,
  signDevAttestationES256,
  type ClientRegistryEntry,
  type ClientRegistryPort,
  type NonceStorePort,
  type PublicKeyResolverPort,
} from './context-attestation';
import { PrismaNonceStore } from './attestation-stores';
import type { PrismaService } from '../prisma/prisma.service';

const APPSHELL_SECRET = 'appshell-dev-signing-key';
const AUD = 'iios-core';
const NOW = new Date('2026-07-13T12:00:00Z');

// ─── in-memory fakes (fast, deterministic — no DB needed) ─────────
function fakeRegistry(over: Partial<ClientRegistryEntry> = {}): ClientRegistryPort {
  const entry: ClientRegistryEntry = {
    clientId: 'appshell-crm',
    clientType: 'SUPPORT_BFF',
    allowedAppIds: ['crm-web'],
    attestSecret: APPSHELL_SECRET,
    status: 'ACTIVE',
    ...over,
  };
  return { find: async (id) => (id === entry.clientId ? entry : null) };
}

function fakeNonces(): NonceStorePort {
  const seen = new Set<string>();
  return { reserve: async ({ nonce }) => (seen.has(nonce) ? false : (seen.add(nonce), true)) };
}

const verifier = (reg: ClientRegistryPort = fakeRegistry(), nonces: NonceStorePort = fakeNonces()) =>
  new ContextAttestationVerifier(reg, nonces, { audience: AUD });

/** A well-formed AppShell attestation, overridable per test. */
function attest(over: Record<string, unknown> = {}, secret = APPSHELL_SECRET, at = NOW): string {
  return signDevAttestation(
    { issuer: 'appshell.crm', issuerType: 'APPSHELL', audience: AUD, clientId: 'appshell-crm', appId: 'crm-web', nonce: 'n_default', ...over },
    secret,
    at,
  );
}

describe('ContextAttestationVerifier (July 12 trust proof)', () => {
  it('accepts a valid attestation from a registered client for the matching app', async () => {
    const res = await verifier().verify(attest({ nonce: 'n_ok' }), 'crm-web', NOW);
    expect(res.ok).toBe(true);
    if (res.ok) expect(res.attestation.clientId).toBe('appshell-crm');
  });

  // ── the five rejection cases the CEO named ──
  it('rejects an unknown / unregistered client (wrong client)', async () => {
    const res = await verifier().verify(attest({ clientId: 'hacker-app', nonce: 'n1' }), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'UNKNOWN_CLIENT' });
  });

  it('rejects the wrong audience (token minted for another service)', async () => {
    const res = await verifier().verify(attest({ audience: 'some-other-service', nonce: 'n2' }), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'WRONG_AUDIENCE' });
  });

  it('rejects an expired attestation', async () => {
    // signed 10 min ago with a 5 min TTL → already expired at NOW
    const stale = attest({ nonce: 'n3', ttlSeconds: 300 }, APPSHELL_SECRET, new Date(NOW.getTime() - 600_000));
    const res = await verifier().verify(stale, 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'EXPIRED' });
  });

  it('rejects a replayed nonce (same attestation used twice)', async () => {
    const v = verifier();
    const token = attest({ nonce: 'n_replay' });
    const first = await v.verify(token, 'crm-web', NOW);
    const second = await v.verify(token, 'crm-web', NOW);
    expect(first.ok).toBe(true);
    expect(second).toMatchObject({ ok: false, reason: 'REPLAY' });
  });

  it('rejects app mismatch: token app != attestation app', async () => {
    const res = await verifier().verify(attest({ appId: 'crm-web', nonce: 'n4' }), 'some-other-app', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'APP_MISMATCH' });
  });

  // ── extra hardening ──
  it('rejects a forged signature (signed with the wrong key)', async () => {
    const res = await verifier().verify(attest({ nonce: 'n5' }, 'attacker-key'), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'BAD_SIGNATURE' });
  });

  it('rejects a disabled client', async () => {
    const res = await verifier(fakeRegistry({ status: 'DISABLED' })).verify(attest({ nonce: 'n6' }), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'CLIENT_DISABLED' });
  });

  it('rejects an app the client is not allowed to attest for', async () => {
    const reg = fakeRegistry({ allowedAppIds: ['other-app'] });
    const res = await verifier(reg).verify(attest({ appId: 'crm-web', nonce: 'n7' }), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'APP_MISMATCH' });
  });
});

// ─── prod asymmetric path: ES256 verified via the client's JWKS ───
describe('ContextAttestationVerifier — JWKS / ES256 (production signing path)', () => {
  const JWKS = 'https://appshell.example/.well-known/jwks.json';
  const KID = 'appshell-key-1';

  // A real EC P-256 keypair (what AppShell would hold; only the public half is published as JWKS).
  const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const privPem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string;
  const pubPem = publicKey.export({ type: 'spki', format: 'pem' }) as string;
  const { privateKey: otherPriv } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const otherPrivPem = otherPriv.export({ type: 'pkcs8', format: 'pem' }) as string;

  // Registry entry that verifies via JWKS (no shared secret) + a resolver that maps KID → pubkey.
  const jwksRegistry: ClientRegistryPort = {
    find: async (id) =>
      id === 'appshell-crm'
        ? { clientId: 'appshell-crm', clientType: 'APPSHELL', allowedAppIds: ['crm-web'], jwksUri: JWKS, status: 'ACTIVE' }
        : null,
  };
  const resolver: PublicKeyResolverPort = { resolve: async (_uri, kid) => (kid === KID ? pubPem : null) };
  const v = () => new ContextAttestationVerifier(jwksRegistry, fakeNonces(), { audience: AUD }, resolver);
  // No 4th arg → no key resolver wired at all (distinct from passing undefined, which hits the default).
  const vNoResolver = () => new ContextAttestationVerifier(jwksRegistry, fakeNonces(), { audience: AUD });

  function es256(over: Record<string, unknown> = {}, priv = privPem, kid = KID): string {
    return signDevAttestationES256(
      { issuer: 'appshell.crm', issuerType: 'APPSHELL', audience: AUD, clientId: 'appshell-crm', appId: 'crm-web', nonce: `n_${Math.random()}`, ...over },
      priv,
      kid,
      NOW,
    );
  }

  it('accepts an ES256 attestation whose signature verifies against the JWKS', async () => {
    const res = await v().verify(es256({ nonce: 'es_ok' }), 'crm-web', NOW);
    expect(res.ok).toBe(true);
    if (res.ok) expect(res.attestation.clientId).toBe('appshell-crm');
  });

  it('rejects an ES256 attestation signed with a DIFFERENT private key (forged)', async () => {
    const res = await v().verify(es256({ nonce: 'es_forged' }, otherPrivPem), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'BAD_SIGNATURE' });
  });

  it('rejects when the JWT header carries an unknown kid (no matching JWKS key)', async () => {
    const res = await v().verify(es256({ nonce: 'es_kid' }, privPem, 'rotated-away-kid'), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'NO_KEY' });
  });

  it('rejects when a JWKS client is configured but no key resolver is wired', async () => {
    const res = await vNoResolver().verify(es256({ nonce: 'es_nores' }), 'crm-web', NOW);
    expect(res).toMatchObject({ ok: false, reason: 'NO_KEY' });
  });

  it('still enforces audience / app / replay on the ES256 path', async () => {
    const vv = v();
    expect(await vv.verify(es256({ nonce: 'es_aud', audience: 'other' }), 'crm-web', NOW)).toMatchObject({ ok: false, reason: 'WRONG_AUDIENCE' });
    expect(await vv.verify(es256({ nonce: 'es_app' }), 'some-other-app', NOW)).toMatchObject({ ok: false, reason: 'APP_MISMATCH' });
    const tok = es256({ nonce: 'es_replay' });
    expect((await vv.verify(tok, 'crm-web', NOW)).ok).toBe(true);
    expect(await vv.verify(tok, 'crm-web', NOW)).toMatchObject({ ok: false, reason: 'REPLAY' });
  });
});

// ─── DB-level atomicity of the nonce ledger ───────────────────────
describe('PrismaNonceStore (atomic replay defense)', () => {
  const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
  const prisma = new PrismaClient({ datasources: { db: { url } } });
  const store = new PrismaNonceStore(prisma as unknown as PrismaService);
  let dbUp = false;

  beforeAll(async () => {
    try { await prisma.$connect(); dbUp = true; } catch { dbUp = false; }
  });
  afterAll(async () => { if (dbUp) await prisma.$disconnect(); });

  it('reserves a nonce once; a second reserve of the same nonce is rejected', async (ctx) => {
    if (!dbUp) return ctx.skip(); // DB unreachable in this environment — the in-memory REPLAY test covers the logic
    const nonce = `n_db_${NOW.getTime()}_${Math.floor(Math.random() * 1e9)}`;
    const expiresAt = new Date(NOW.getTime() + 300_000);
    const first = await store.reserve({ nonce, clientId: 'appshell-crm', expiresAt });
    const second = await store.reserve({ nonce, clientId: 'appshell-crm', expiresAt });
    expect(first).toBe(true);
    expect(second).toBe(false);
    await prisma.iiosAttestationNonce.delete({ where: { nonce } }).catch(() => undefined);
  });
});
