import { describe, it, expect, afterEach } from 'vitest';
import { ForbiddenException, type ExecutionContext } from '@nestjs/common';
import { ContextAttestationGuard } from './context-attestation.guard';
import {
  ContextAttestationVerifier,
  signDevAttestation,
  type ClientRegistryPort,
  type NonceStorePort,
} from './context-attestation';
import type { SessionVerifier } from './session.verifier';

const SECRET = 'appshell-dev-signing-key';

// SessionVerifier stand-in: the token always resolves to app "crm-web".
const fakeSession = {
  verify: () => ({ userId: 'agent_1', appId: 'crm-web', orgId: 'org', tenantId: 'tnt', displayName: 'A' }),
} as unknown as SessionVerifier;

const registryFor = (): ClientRegistryPort => ({
  find: async (id) =>
    id === 'appshell-crm'
      ? { clientId: 'appshell-crm', clientType: 'APPSHELL', allowedAppIds: ['crm-web'], attestSecret: SECRET, status: 'ACTIVE' }
      : null,
});
const freshNonces = (): NonceStorePort => {
  const seen = new Set<string>();
  return { reserve: async ({ nonce }) => (seen.has(nonce) ? false : (seen.add(nonce), true)) };
};

function makeGuard(): ContextAttestationGuard {
  const verifier = new ContextAttestationVerifier(registryFor(), freshNonces(), { audience: 'iios-core' });
  return new ContextAttestationGuard(fakeSession, verifier);
}

// A guard whose actor token resolves to `tokenApp` — used to test attestation↔token app binding.
function makeGuardForApp(tokenApp: string): ContextAttestationGuard {
  const session = {
    verify: () => ({ userId: 'agent_1', appId: tokenApp, orgId: 'org', tenantId: 'tnt', displayName: 'A' }),
  } as unknown as SessionVerifier;
  const verifier = new ContextAttestationVerifier(registryFor(), freshNonces(), { audience: 'iios-core' });
  return new ContextAttestationGuard(session, verifier);
}

function ctxWith(headers: Record<string, string>): ExecutionContext {
  return { switchToHttp: () => ({ getRequest: () => ({ headers }) }) } as unknown as ExecutionContext;
}

function att(over: Record<string, unknown> = {}, secret = SECRET): string {
  return signDevAttestation(
    { issuer: 'appshell.crm', audience: 'iios-core', clientId: 'appshell-crm', appId: 'crm-web', nonce: `n_${Math.random()}`, ...over },
    secret,
  );
}

describe('ContextAttestationGuard (three-proof gate on the request path)', () => {
  afterEach(() => { delete process.env.IIOS_REQUIRE_ATTESTATION; });

  it('allows a request with no attestation when NOT required (dev / zero-trust)', async () => {
    expect(await makeGuard().canActivate(ctxWith({}))).toBe(true);
  });

  it('rejects a request with no attestation when IIOS_REQUIRE_ATTESTATION=1', async () => {
    process.env.IIOS_REQUIRE_ATTESTATION = '1';
    await expect(makeGuard().canActivate(ctxWith({}))).rejects.toBeInstanceOf(ForbiddenException);
  });

  it('allows a valid attestation bound to the token app', async () => {
    const headers = { authorization: 'Bearer tok', 'x-context-attestation': att() };
    expect(await makeGuard().canActivate(ctxWith(headers))).toBe(true);
  });

  it('rejects a forged attestation (wrong signing key) — stolen-context defense', async () => {
    const headers = { authorization: 'Bearer tok', 'x-context-attestation': att({ nonce: 'n_forge' }, 'attacker-key') };
    await expect(makeGuard().canActivate(ctxWith(headers))).rejects.toBeInstanceOf(ForbiddenException);
  });

  it('rejects a replayed attestation (same nonce twice)', async () => {
    const guard = makeGuard();
    const headers = { authorization: 'Bearer tok', 'x-context-attestation': att({ nonce: 'n_replay' }) };
    expect(await guard.canActivate(ctxWith(headers))).toBe(true);
    await expect(guard.canActivate(ctxWith({ ...headers }))).rejects.toBeInstanceOf(ForbiddenException);
  });

  // ── doc rejection matrix: "valid token + wrong client/context → reject" ──
  it('rejects a valid attestation whose app binding != the actor token app (stolen context reused by another app)', async () => {
    // Attestation is validly signed for app crm-web, but the presented actor token is for a different app.
    const headers = { authorization: 'Bearer tok', 'x-context-attestation': att({ nonce: 'n_appmix' }) };
    await expect(makeGuardForApp('other-app').canActivate(ctxWith(headers))).rejects.toThrow(/APP_MISMATCH/);
  });

  // ── doc rejection matrix: "valid token + wrong audience → reject before business logic" ──
  it('rejects an attestation minted for another service (audience != iios-core) replayed at IIOS', async () => {
    const headers = { authorization: 'Bearer tok', 'x-context-attestation': att({ nonce: 'n_aud', audience: 'some-other-service' }) };
    await expect(makeGuard().canActivate(ctxWith(headers))).rejects.toThrow(/WRONG_AUDIENCE/);
  });
});
