/**
 * Portal JWT verification - locks in the audience/issuer hardening shipped
 * in fix(auth): a token without `aud: 'portal'` + `iss: 'pn-crm'` claims
 * must NOT verify, even if it's signed with the correct shared secret.
 *
 * Without these claims the CRM (better-auth) and portal sessions are
 * structurally identical, so a portal token could be replayed against any
 * `verifyPortalToken` consumer (and vice versa).
 *
 * The post-Wave-11 `verifyPortalToken` also does a DB lookup for the
 * portal user's password-change watermark (auth-flow-auditor C1). Mock
 * `@/lib/db` so these tests stay unit-pure and don't need a seeded
 * portal_users row.
 */
import { describe, expect, it, vi } from 'vitest';
import { SignJWT } from 'jose';

const PORTAL_USER_ID = '33333333-3333-3333-3333-333333333333';

vi.mock('@/lib/db', () => ({
  db: {
    query: {
      portalUsers: {
        findFirst: vi.fn(async () => ({
          // Watermark in the past so iat (≈ now) is later.
          passwordChangedAt: new Date(Date.now() - 60_000),
          isActive: true,
        })),
      },
    },
  },
}));

const { createPortalToken, verifyPortalToken } = await import('@/lib/portal/auth');

const SECRET = new TextEncoder().encode(process.env.BETTER_AUTH_SECRET);

const SESSION = {
  portalUserId: PORTAL_USER_ID,
  clientId: '11111111-1111-1111-1111-111111111111',
  portId: '22222222-2222-2222-2222-222222222222',
  email: 'client@example.com',
};

describe('portal JWT', () => {
  it('round-trips a token signed with createPortalToken', async () => {
    const token = await createPortalToken(SESSION);
    const verified = await verifyPortalToken(token);
    expect(verified).toMatchObject(SESSION);
  });

  it('rejects a token missing the `aud: portal` claim', async () => {
    // Issuer present, audience absent - exactly the shape an old (pre-fix)
    // portal session would have.
    const token = await new SignJWT(SESSION as unknown as Record<string, unknown>)
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuer('pn-crm')
      .setExpirationTime('24h')
      .setIssuedAt()
      .sign(SECRET);

    expect(await verifyPortalToken(token)).toBeNull();
  });

  it('rejects a token missing the `iss: pn-crm` claim', async () => {
    const token = await new SignJWT(SESSION as unknown as Record<string, unknown>)
      .setProtectedHeader({ alg: 'HS256' })
      .setAudience('portal')
      .setExpirationTime('24h')
      .setIssuedAt()
      .sign(SECRET);

    expect(await verifyPortalToken(token)).toBeNull();
  });

  it('rejects a token with the wrong audience (CRM session replay shape)', async () => {
    // What a better-auth session token might roughly look like - same secret,
    // different audience. Must not verify against the portal path.
    const token = await new SignJWT(SESSION as unknown as Record<string, unknown>)
      .setProtectedHeader({ alg: 'HS256' })
      .setAudience('crm')
      .setIssuer('pn-crm')
      .setExpirationTime('24h')
      .setIssuedAt()
      .sign(SECRET);

    expect(await verifyPortalToken(token)).toBeNull();
  });

  it('rejects a token with the wrong issuer', async () => {
    const token = await new SignJWT(SESSION as unknown as Record<string, unknown>)
      .setProtectedHeader({ alg: 'HS256' })
      .setAudience('portal')
      .setIssuer('attacker')
      .setExpirationTime('24h')
      .setIssuedAt()
      .sign(SECRET);

    expect(await verifyPortalToken(token)).toBeNull();
  });

  it('rejects garbage', async () => {
    expect(await verifyPortalToken('not.a.jwt')).toBeNull();
    expect(await verifyPortalToken('')).toBeNull();
  });
});
