import { Test, TestingModule } from '@nestjs/testing';
import {
  ExecutionContext,
  ForbiddenException,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { ServiceCredentialGuard } from './service-credential.guard';
import { ServiceCredentialValidatorService } from '../../services/service-credential-validator.service';
import { ClientJwtGuard } from '../../../auth/guards/client-jwt.guard';
import { UserJwtGuard } from '../../../auth/guards/user-jwt.guard';
import { REQUIRE_SCOPES_KEY } from '../../decorators/require-scopes.decorator';
import { ALLOW_AUTH_KEY } from '../../decorators/allow-auth.decorator';

describe('ServiceCredentialGuard', () => {
  let guard: ServiceCredentialGuard;

  const mockValidator = { validate: jest.fn() };
  const mockUserJwtGuard = { canActivate: jest.fn() };
  const mockClientJwtGuard = { canActivate: jest.fn() };
  const mockReflector = {
    getAllAndOverride: jest.fn(),
    getAllAndMerge: jest.fn(),
  };
  const config: Record<string, unknown> = {
    SERVICE_CREDENTIALS_ENABLED: true,
    LEGACY_API_KEY_ENABLED: true,
    API_KEY: 'legacy-global-key',
  };
  const mockConfigService = { get: jest.fn((k: string) => config[k]) };

  const makeContext = (request: Record<string, unknown>): ExecutionContext =>
    ({
      switchToHttp: () => ({
        getRequest: () => request,
        getResponse: () => ({}),
        getNext: () => ({}),
      }),
      getHandler: () => ({}),
      getClass: () => ({}),
    }) as unknown as ExecutionContext;

  const reqWith = (
    headers: Record<string, unknown>,
    route = '/audit-events',
    method = 'POST',
  ) => ({
    headers,
    method,
    route: { path: route },
    path: route,
    url: route,
    ip: '10.0.0.1',
  });

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ServiceCredentialGuard,
        { provide: ServiceCredentialValidatorService, useValue: mockValidator },
        { provide: UserJwtGuard, useValue: mockUserJwtGuard },
        { provide: ClientJwtGuard, useValue: mockClientJwtGuard },
        { provide: Reflector, useValue: mockReflector },
        { provide: ConfigService, useValue: mockConfigService },
      ],
    }).compile();

    guard = module.get(ServiceCredentialGuard);
    // Default metadata: service_key only, no required scope.
    mockReflector.getAllAndOverride.mockImplementation((key: string) =>
      key === ALLOW_AUTH_KEY ? ['service_key'] : undefined,
    );
    mockReflector.getAllAndMerge.mockImplementation((key: string) =>
      key === REQUIRE_SCOPES_KEY ? [] : [],
    );
  });

  afterEach(() => jest.clearAllMocks());

  it('should be defined', () => {
    expect(guard).toBeDefined();
  });

  it('passes and attaches request.serviceCredential when the key is valid', async () => {
    mockValidator.validate.mockResolvedValue({
      valid: true,
      credential: { prefix: 'pf_sk_live_a1b2c3d4', owner_type: 'bff' },
    });
    const request = reqWith({
      'x-planfi-service-key': 'pf_sk_live_a1b2c3d4.SECRET',
    });

    const result = await guard.canActivate(makeContext(request));

    expect(result).toBe(true);
    expect((request as Record<string, unknown>).serviceCredential).toEqual({
      prefix: 'pf_sk_live_a1b2c3d4',
      owner_type: 'bff',
    });
  });

  it('sends the request route + method + required_scope to the validator', async () => {
    mockReflector.getAllAndMerge.mockImplementation((key: string) =>
      key === REQUIRE_SCOPES_KEY ? ['bff:audit-events:write'] : [],
    );
    mockValidator.validate.mockResolvedValue({ valid: true, credential: {} });
    const request = reqWith(
      { 'x-planfi-service-key': 'pf_sk_live_a1b2c3d4.SECRET' },
      '/audit-events',
      'POST',
    );

    await guard.canActivate(makeContext(request));

    expect(mockValidator.validate).toHaveBeenCalledWith(
      expect.objectContaining({
        presentedKey: 'pf_sk_live_a1b2c3d4.SECRET',
        route: '/audit-events',
        method: 'POST',
        requiredScope: 'bff:audit-events:write',
      }),
    );
  });

  it('throws 403 ForbiddenException on route_denied', async () => {
    mockValidator.validate.mockResolvedValue({
      valid: false,
      reason: 'route_denied',
    });
    const request = reqWith(
      { 'x-planfi-service-key': 'pf_sk_live_a1b2c3d4.SECRET' },
      '/usuarios',
    );

    await expect(
      guard.canActivate(makeContext(request)),
    ).rejects.toBeInstanceOf(ForbiddenException);
  });

  it('throws 403 ForbiddenException on method_denied', async () => {
    mockValidator.validate.mockResolvedValue({
      valid: false,
      reason: 'method_denied',
    });
    const request = reqWith(
      { 'x-planfi-service-key': 'pf_sk_live_a1b2c3d4.SECRET' },
      '/audit-events',
      'DELETE',
    );

    await expect(
      guard.canActivate(makeContext(request)),
    ).rejects.toBeInstanceOf(ForbiddenException);
  });

  it('throws 401 UnauthorizedException when the validator is unreachable (fail-closed)', async () => {
    mockValidator.validate.mockResolvedValue({
      valid: false,
      reason: 'validator_unreachable',
    });
    const request = reqWith({
      'x-planfi-service-key': 'pf_sk_live_a1b2c3d4.SECRET',
    });

    await expect(
      guard.canActivate(makeContext(request)),
    ).rejects.toBeInstanceOf(UnauthorizedException);
  });

  it('throws 401 when no key is presented and only service_key is allowed', async () => {
    const request = reqWith({});

    await expect(
      guard.canActivate(makeContext(request)),
    ).rejects.toBeInstanceOf(UnauthorizedException);
    expect(mockValidator.validate).not.toHaveBeenCalled();
  });

  it('accepts the legacy x-api-key when LEGACY_API_KEY_ENABLED and the global key matches', async () => {
    const request = reqWith({ 'x-api-key': 'legacy-global-key' });

    const result = await guard.canActivate(makeContext(request));

    expect(result).toBe(true);
    expect(mockValidator.validate).not.toHaveBeenCalled();
  });

  it('rejects the legacy x-api-key when LEGACY_API_KEY_ENABLED is false', async () => {
    config.LEGACY_API_KEY_ENABLED = false;
    const request = reqWith({ 'x-api-key': 'legacy-global-key' });

    await expect(
      guard.canActivate(makeContext(request)),
    ).rejects.toBeInstanceOf(UnauthorizedException);
    config.LEGACY_API_KEY_ENABLED = true; // restore
  });

  it('falls through to UserJwtGuard when @AllowAuth includes user_jwt and no service key is present', async () => {
    mockReflector.getAllAndOverride.mockImplementation((key: string) =>
      key === ALLOW_AUTH_KEY ? ['service_key', 'user_jwt'] : undefined,
    );
    mockUserJwtGuard.canActivate.mockResolvedValue(true);
    const request = reqWith({ authorization: 'Bearer jwt-token' });

    const result = await guard.canActivate(makeContext(request));

    expect(result).toBe(true);
    expect(mockUserJwtGuard.canActivate).toHaveBeenCalled();
    expect(mockValidator.validate).not.toHaveBeenCalled();
  });

  it('fails closed (401) when subsystem is disabled via SERVICE_CREDENTIALS_ENABLED=false', async () => {
    config.SERVICE_CREDENTIALS_ENABLED = false;
    const request = reqWith({
      'x-planfi-service-key': 'pf_sk_live_a1b2c3d4.SECRET',
    });

    await expect(
      guard.canActivate(makeContext(request)),
    ).rejects.toBeInstanceOf(UnauthorizedException);
    config.SERVICE_CREDENTIALS_ENABLED = true; // restore
  });
});
