import {
  ExecutionContext,
  ForbiddenException,
  HttpException,
  UnsupportedMediaTypeException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { MultipartFormDataGuard } from './multipart-form-data.guard';
import { RolesGuard } from './roles.guard';
import { SuperAdminPermissionsGuard } from './superadmin-permissions.guard';
import { ThrottleGuard } from './throttle.guard';

const contextFor = (request: Record<string, any>): ExecutionContext =>
  ({
    getHandler: () => function handler() {},
    getClass: () => class Controller {},
    switchToHttp: () => ({ getRequest: () => request }),
  }) as unknown as ExecutionContext;

describe('authorization guards', () => {
  describe('RolesGuard', () => {
    it('allows routes without role metadata', () => {
      const reflector = { getAllAndOverride: jest.fn().mockReturnValue(undefined) };
      const guard = new RolesGuard(reflector as unknown as Reflector);

      expect(guard.canActivate(contextFor({}))).toBe(true);
    });

    it.each([
      [{ role: 'admin' }, true],
      [{ roles: ['support', 'admin'] }, true],
      [{ role: 'user' }, false],
      [{}, false],
    ])('evaluates normalized payload roles %#', (user, expected) => {
      const reflector = { getAllAndOverride: jest.fn().mockReturnValue(['admin']) };
      const guard = new RolesGuard(reflector as unknown as Reflector);

      expect(guard.canActivate(contextFor({ user }))).toBe(expected);
    });
  });

  describe('SuperAdminPermissionsGuard', () => {
    it('allows routes without permission requirements', () => {
      const reflector = { getAllAndOverride: jest.fn().mockReturnValue([]) };
      const guard = new SuperAdminPermissionsGuard(reflector as unknown as Reflector);

      expect(guard.canActivate(contextFor({}))).toBe(true);
    });

    it('requires every declared permission', () => {
      const reflector = {
        getAllAndOverride: jest.fn().mockReturnValue(['users.read', 'users.manage']),
      };
      const guard = new SuperAdminPermissionsGuard(reflector as unknown as Reflector);

      expect(
        guard.canActivate(
          contextFor({ user: { permissions: ['users.read', 'users.manage', 'audit.read'] } }),
        ),
      ).toBe(true);
      expect(() =>
        guard.canActivate(contextFor({ user: { permissions: ['users.read'] } })),
      ).toThrow(ForbiddenException);
      expect(() => guard.canActivate(contextFor({}))).toThrow(
        'Missing superadmin permission',
      );
    });
  });

  describe('ThrottleGuard', () => {
    it('does not touch cache for an unthrottled route', async () => {
      const reflector = { getAllAndOverride: jest.fn().mockReturnValue(undefined) };
      const cache = { incr: jest.fn() };
      const guard = new ThrottleGuard(reflector as any, cache as any);

      await expect(guard.canActivate(contextFor({}))).resolves.toBe(true);
      expect(cache.incr).not.toHaveBeenCalled();
    });

    it('uses authenticated subject and a minimum one-second window', async () => {
      const reflector = {
        getAllAndOverride: jest.fn().mockReturnValue({ limit: 2, windowMs: 10 }),
      };
      const cache = { incr: jest.fn().mockResolvedValue(2) };
      const guard = new ThrottleGuard(reflector as any, cache as any);

      await expect(
        guard.canActivate(
          contextFor({
            user: { sub: 'user-1' },
            ip: '127.0.0.1',
            baseUrl: '/api/v1/auth',
            route: { path: '/login' },
          }),
        ),
      ).resolves.toBe(true);
      expect(cache.incr).toHaveBeenCalledWith(
        'rate-limit:/api/v1/auth/login:user-1',
        1,
      );
    });

    it('falls back to IP/original URL and rejects requests above the limit', async () => {
      const reflector = {
        getAllAndOverride: jest.fn().mockReturnValue({ limit: 1, windowMs: 1_500 }),
      };
      const cache = { incr: jest.fn().mockResolvedValue(2) };
      const guard = new ThrottleGuard(reflector as any, cache as any);

      await expect(
        guard.canActivate(contextFor({ ip: '10.0.0.2', originalUrl: '/fallback' })),
      ).rejects.toBeInstanceOf(HttpException);
      expect(cache.incr).toHaveBeenCalledWith('rate-limit:/fallback:10.0.0.2', 2);
    });

    it('uses stable unknown placeholders if request identity and route are absent', async () => {
      const reflector = {
        getAllAndOverride: jest.fn().mockReturnValue({ limit: 1, windowMs: 1_000 }),
      };
      const cache = { incr: jest.fn().mockResolvedValue(1) };
      const guard = new ThrottleGuard(reflector as any, cache as any);

      await expect(guard.canActivate(contextFor({}))).resolves.toBe(true);
      expect(cache.incr).toHaveBeenCalledWith('rate-limit:unknown-route:unknown', 1);
    });
  });

  describe('MultipartFormDataGuard', () => {
    it('accepts multipart requests and rejects other media types', () => {
      const guard = new MultipartFormDataGuard();
      expect(guard.canActivate(contextFor({ is: jest.fn().mockReturnValue(true) }))).toBe(true);
      expect(() =>
        guard.canActivate(contextFor({ is: jest.fn().mockReturnValue(false) })),
      ).toThrow(UnsupportedMediaTypeException);
    });
  });
});
