import { generateKeyPair, exportJWK, SignJWT, createLocalJWKSet } from 'jose';
import type { KeyLike } from 'jose';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { OidcAuthGuard } from './oidc-auth.guard';

const issuer = 'https://idp.example.test/';
const audience = 'travel-planner-api';

function contextWithHeader(authorization?: string): ExecutionContext {
  const req: Record<string, unknown> = authorization
    ? { headers: { authorization } }
    : { headers: {} };
  return {
    switchToHttp: () => ({ getRequest: () => req }),
  } as unknown as ExecutionContext;
}

describe('OidcAuthGuard', () => {
  let privateKey: KeyLike;
  let discovery: {
    getVerificationKeySet: jest.Mock;
    getIssuer: jest.Mock;
    getAudience: jest.Mock;
  };
  let usersService: { findOrCreateByExternalSubjectId: jest.Mock };

  beforeAll(async () => {
    const { publicKey, privateKey: pk } = await generateKeyPair('RS256');
    privateKey = pk;
    const jwk = await exportJWK(publicKey);
    (jwk as Record<string, string>).kid = 'test-key';
    const jwks = createLocalJWKSet({ keys: [jwk as never] });
    discovery = {
      getVerificationKeySet: jest.fn().mockReturnValue(jwks),
      getIssuer: jest.fn().mockReturnValue(issuer),
      getAudience: jest.fn().mockReturnValue(audience),
    };
  });

  beforeEach(() => {
    usersService = {
      findOrCreateByExternalSubjectId: jest.fn().mockResolvedValue({
        id: 'local-1',
        externalSubjectId: 'idp-sub-1',
        displayName: 'A',
        email: 'a@example.com',
      }),
    };
  });

  async function sign(claims: Record<string, unknown>, expires = '5m') {
    return new SignJWT(claims)
      .setProtectedHeader({ alg: 'RS256', kid: 'test-key' })
      .setIssuer(issuer)
      .setAudience(audience)
      .setIssuedAt()
      .setExpirationTime(expires)
      .sign(privateKey);
  }

  it('rejects a request with no Authorization header', async () => {
    const guard = new OidcAuthGuard(discovery as never, usersService as never);
    await expect(guard.canActivate(contextWithHeader())).rejects.toThrow(
      UnauthorizedException,
    );
  });

  it('rejects an expired token', async () => {
    const token = await sign(
      { sub: 'idp-sub-1', email: 'a@example.com', name: 'A' },
      '-10s',
    );
    const guard = new OidcAuthGuard(discovery as never, usersService as never);
    await expect(
      guard.canActivate(contextWithHeader(`Bearer ${token}`)),
    ).rejects.toThrow(UnauthorizedException);
  });

  it('rejects a token issued for a different audience', async () => {
    const token = await new SignJWT({ sub: 'idp-sub-1' })
      .setProtectedHeader({ alg: 'RS256', kid: 'test-key' })
      .setIssuer(issuer)
      .setAudience('some-other-api')
      .setIssuedAt()
      .setExpirationTime('5m')
      .sign(privateKey);
    const guard = new OidcAuthGuard(discovery as never, usersService as never);
    await expect(
      guard.canActivate(contextWithHeader(`Bearer ${token}`)),
    ).rejects.toThrow(UnauthorizedException);
  });

  it('provisions the local user and attaches req.user on a valid token', async () => {
    const token = await sign({
      sub: 'idp-sub-1',
      email: 'a@example.com',
      name: 'A',
    });
    const req: Record<string, unknown> = {
      headers: { authorization: `Bearer ${token}` },
    };
    const context = {
      switchToHttp: () => ({ getRequest: () => req }),
    } as unknown as ExecutionContext;

    const guard = new OidcAuthGuard(discovery as never, usersService as never);
    await expect(guard.canActivate(context)).resolves.toBe(true);

    expect(usersService.findOrCreateByExternalSubjectId).toHaveBeenCalledWith(
      'idp-sub-1',
      {
        email: 'a@example.com',
        displayName: 'A',
      },
    );
    expect(req.user).toEqual({
      id: 'local-1',
      externalSubjectId: 'idp-sub-1',
      displayName: 'A',
      email: 'a@example.com',
    });
  });
});
