import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import { generateKeyPairSync, type KeyObject } from 'node:crypto';
import jwt from 'jsonwebtoken';
import { SessionVerifier } from './session.verifier';

// Two independent fake IdPs (each its own EC signing key + kid).
function makeIssuer(kid: string) {
  const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const jwk = { ...(publicKey.export({ format: 'jwk' }) as Record<string, unknown>), kid, use: 'sig', alg: 'ES256' };
  return { privateKey, jwks: { keys: [jwk] } };
}
const chat = makeIssuer('chat-kid');
const support = makeIssuer('support-kid');
const CHAT_URL = 'https://chat-proj.example.co';
const SUPPORT_URL = 'https://support-proj.example.co';

function sat(priv: KeyObject, url: string, kid: string, email: string, name: string): string {
  return jwt.sign(
    { email, user_metadata: { full_name: name }, role: 'authenticated' },
    priv,
    { algorithm: 'ES256', issuer: `${url}/auth/v1`, audience: 'authenticated', subject: 'uuid-x', keyid: kid, expiresIn: '1h' },
  );
}

describe('SessionVerifier — multi-issuer (OIDC/JWKS registry)', () => {
  let verifier: SessionVerifier;

  beforeAll(async () => {
    process.env.AUTH_ISSUERS = JSON.stringify([
      { url: CHAT_URL, appId: 'portal-demo' },
      { url: SUPPORT_URL, appId: 'support-app' },
    ]);
    // Serve each issuer's JWKS from its own well-known URL.
    vi.stubGlobal(
      'fetch',
      vi.fn(async (u: string) => {
        const s = String(u);
        const body = s.includes('chat-proj') ? chat.jwks : s.includes('support-proj') ? support.jwks : { keys: [] };
        return { ok: true, json: async () => body } as unknown as Response;
      }),
    );
    verifier = new SessionVerifier();
    await verifier.onModuleInit();
  });

  afterAll(() => {
    vi.unstubAllGlobals();
    delete process.env.AUTH_ISSUERS;
  });

  it('routes a token to the appId of its own issuer', () => {
    expect(verifier.verify(sat(chat.privateKey, CHAT_URL, 'chat-kid', 'Alice@x.com', 'Alice'))).toMatchObject({
      userId: 'alice@x.com',
      appId: 'portal-demo',
      displayName: 'Alice',
    });
  });

  it('a second issuer maps to a DIFFERENT appId — isolated scope', () => {
    expect(verifier.verify(sat(support.privateKey, SUPPORT_URL, 'support-kid', 'Bob@x.com', 'Bob'))).toMatchObject({
      userId: 'bob@x.com',
      appId: 'support-app',
    });
  });

  it('rejects a token from an untrusted issuer', () => {
    const rogue = makeIssuer('rogue-kid');
    expect(() => verifier.verify(sat(rogue.privateKey, 'https://evil.example.co', 'rogue-kid', 'e@x.com', 'E'))).toThrow();
  });

  it('rejects a forgery: signed by issuer B but claiming issuer A + A’s kid', () => {
    const forged = sat(support.privateKey, CHAT_URL, 'chat-kid', 'x@x.com', 'X'); // wrong key for the claimed issuer
    expect(() => verifier.verify(forged)).toThrow();
  });
});
