import { ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import type { Request } from 'express';
import type { Principal } from 'shared-auth';
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[];
  };
  principal?: Principal;
}

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),
  };
}

function principalWithPrivileges(privileges: readonly string[]): Principal {
  return {
    user: {
      id: 'user-oid',
      personId: 'user-oid',
      entraOid: 'user-oid',
      tenantId: 't',
      displayName: 'Jane',
    },
    privileges: privileges as Principal['privileges'],
    roles: [],
    scopes: [{ kind: 'unrestricted' }],
    amr: ['pwd', 'mfa'],
  };
}

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 principal nor legacy user is bound', 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 principal has no privileges', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: { principal: principalWithPrivileges([]) },
        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 principal has non-admin privileges only', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: { principal: principalWithPrivileges(['Portal.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: ['Portal.Auditor'],
    });
  });

  it('returns true and does not audit when the principal carries Portal.Admin', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({ session: { principal: principalWithPrivileges([ADMIN_ROLE]) } }),
    );
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
    expect(audit.adminAccessDenied).not.toHaveBeenCalled();
  });

  it('returns true when admin is one of several privileges', async () => {
    const audit = makeAuditStub();
    const guard = new AdminRoleGuard(audit as unknown as AuditWriter);
    const ctx = makeContext(
      makeRequest({
        session: {
          principal: principalWithPrivileges(['Portal.Auditor', ADMIN_ROLE, 'Portal.DPO']),
        },
      }),
    );
    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: { principal: principalWithPrivileges([]) } }));
    // 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');
  });

  describe('legacy-session bridge (sessions persisted before ADR-0025 landed)', () => {
    it('synthesises a principal from session.user when session.principal is absent', 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, 'Other.Role'],
            },
          },
        }),
      );
      await expect(guard.canActivate(ctx)).resolves.toBe(true);
      expect(audit.adminAccessDenied).not.toHaveBeenCalled();
    });

    it('denies a legacy session whose roles claim carries no Portal.* value', 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'],
            },
          },
        }),
      );
      await expect(guard.canActivate(ctx)).rejects.toBeInstanceOf(ForbiddenException);
      // The legacy bridge filters the raw `roles` claim down to
      // `Portal.*` values for the audit row's `rolesHeld` field —
      // `editor` and `auditor` are not privileges, so the held
      // list is empty.
      expect(audit.adminAccessDenied).toHaveBeenCalledWith({
        actor: { oid: 'user-oid' },
        attemptedRoute: 'GET /api/admin/me',
        rolesHeld: [],
      });
    });
  });
});
