import { ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import type { Request } from 'express';
import { AuditWriter } from '../audit/audit.service';
import { ADMIN_ROLE, AdminRoleGuard } from './admin-role.guard';

interface SessionStub {
  user?: {
    oid: string;
    tid: string;
    username: string;
    displayName: string;
    amr: readonly string[];
    roles: readonly string[];
  };
}

function makeRequest(opts: {
  session?: SessionStub;
  method?: string;
  originalUrl?: string;
}): Request {
  return {
    session: opts.session,
    method: opts.method ?? 'GET',
    originalUrl: opts.originalUrl ?? '/api/admin/me',
  } as unknown as Request;
}

function makeContext(req: Request): ExecutionContext {
  return {
    switchToHttp: () => ({ getRequest: <T>() => req as T }),
  } as unknown as ExecutionContext;
}

function makeAuditStub(): jest.Mocked<Pick<AuditWriter, 'adminAccessDenied'>> {
  return {
    adminAccessDenied: jest.fn().mockResolvedValue(undefined),
  };
}

describe('AdminRoleGuard', () => {
  it('throws 401 when the request has no session at all', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(makeRequest({ session: undefined }));
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(UnauthorizedException);
    expect(audit.adminAccessDenied).not.toHaveBeenCalled();
  });

  it('throws 401 when the session exists but no user is bound to it', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(makeRequest({ session: {} }));
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(UnauthorizedException);
    expect(audit.adminAccessDenied).not.toHaveBeenCalled();
  });

  it('throws 403 + records admin.access_denied when the user has no roles at all', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: {
          user: {
            oid: 'user-oid',
            tid: 't',
            username: 'jane@example',
            displayName: 'Jane',
            amr: ['pwd'],
            roles: [],
          },
        },
        method: 'GET',
        originalUrl: '/api/admin/me',
      }),
    );
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit.adminAccessDenied).toHaveBeenCalledWith({
      actor: { oid: 'user-oid' },
      attemptedRoute: 'GET /api/admin/me',
      rolesHeld: [],
    });
  });

  it('throws 403 + records admin.access_denied when the user has unrelated roles only', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: {
          user: {
            oid: 'user-oid',
            tid: 't',
            username: 'jane@example',
            displayName: 'Jane',
            amr: ['pwd'],
            roles: ['editor', 'auditor'],
          },
        },
        method: 'POST',
        originalUrl: '/api/admin/cms/pages',
      }),
    );
    await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
    expect(audit.adminAccessDenied).toHaveBeenCalledWith({
      actor: { oid: 'user-oid' },
      attemptedRoute: 'POST /api/admin/cms/pages',
      rolesHeld: ['editor', 'auditor'],
    });
  });

  it('returns true and does not audit when the user has the admin role', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: {
          user: {
            oid: 'user-oid',
            tid: 't',
            username: 'jane@example',
            displayName: 'Jane',
            amr: ['pwd', 'mfa'],
            roles: [ADMIN_ROLE],
          },
        },
      }),
    );
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
    expect(audit.adminAccessDenied).not.toHaveBeenCalled();
  });

  it('returns true when admin is one of several roles', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: {
          user: {
            oid: 'user-oid',
            tid: 't',
            username: 'jane@example',
            displayName: 'Jane',
            amr: ['pwd', 'mfa'],
            roles: ['editor', ADMIN_ROLE, 'auditor'],
          },
        },
      }),
    );
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('propagates audit write failures (no audit ⇒ no action, per ADR-0013)', async () => {
    const audit = {
      adminAccessDenied: jest.fn().mockRejectedValue(new Error('audit_writer denied')),
    };
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: {
          user: {
            oid: 'user-oid',
            tid: 't',
            username: 'jane@example',
            displayName: 'Jane',
            amr: ['pwd'],
            roles: [],
          },
        },
      }),
    );
    // The 403 path goes through audit first; if audit throws, the
    // guard surfaces the underlying error rather than the
    // ForbiddenException — the caller sees a 500 and the audit
    // invariant is preserved.
    await expect(guard.canActivate(ctx)).rejects.toThrow('audit_writer denied');
  });
});
