/**
 * Unit tests for src/middleware/errorHandler.ts
 */

import { Request, Response, NextFunction } from 'express';
import { errorHandler } from '../../../src/middleware/errorHandler';
import {
  ValidationError,
  AgentNotFoundError,
  AgentAlreadyExistsError,
  AgentAlreadyDecommissionedError,
  CredentialNotFoundError,
  CredentialAlreadyRevokedError,
  CredentialError,
  AuthenticationError,
  AuthorizationError,
  RateLimitError,
  FreeTierLimitError,
  InsufficientScopeError,
  AuditEventNotFoundError,
  RetentionWindowError,
} from '../../../src/utils/errors';

function makeRes(): { status: jest.Mock; json: jest.Mock } {
  const res = {
    status: jest.fn().mockReturnThis(),
    json: jest.fn().mockReturnThis(),
  };
  return res;
}

const req = {} as Request;
const next = jest.fn() as jest.MockedFunction<NextFunction>;

describe('errorHandler', () => {
  beforeEach(() => jest.clearAllMocks());

  it('should return 400 for ValidationError', () => {
    const res = makeRes();
    errorHandler(new ValidationError('bad input'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'VALIDATION_ERROR' }));
  });

  it('should return 404 for AgentNotFoundError', () => {
    const res = makeRes();
    errorHandler(new AgentNotFoundError(), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(404);
    expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'AGENT_NOT_FOUND' }));
  });

  it('should return 409 for AgentAlreadyExistsError', () => {
    const res = makeRes();
    errorHandler(new AgentAlreadyExistsError('test@test.com'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(409);
    expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'AGENT_ALREADY_EXISTS' }));
  });

  it('should return 409 for AgentAlreadyDecommissionedError', () => {
    const res = makeRes();
    errorHandler(new AgentAlreadyDecommissionedError('id'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(409);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'AGENT_ALREADY_DECOMMISSIONED' }),
    );
  });

  it('should return 404 for CredentialNotFoundError', () => {
    const res = makeRes();
    errorHandler(new CredentialNotFoundError(), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(404);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'CREDENTIAL_NOT_FOUND' }),
    );
  });

  it('should return 409 for CredentialAlreadyRevokedError', () => {
    const res = makeRes();
    errorHandler(
      new CredentialAlreadyRevokedError('cred-id', new Date().toISOString()),
      req,
      res as unknown as Response,
      next,
    );
    expect(res.status).toHaveBeenCalledWith(409);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'CREDENTIAL_ALREADY_REVOKED' }),
    );
  });

  it('should return 400 for CredentialError', () => {
    const res = makeRes();
    errorHandler(new CredentialError('error', 'AGENT_NOT_ACTIVE'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(400);
  });

  it('should return 401 for AuthenticationError', () => {
    const res = makeRes();
    errorHandler(new AuthenticationError(), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'UNAUTHORIZED' }));
  });

  it('should return 403 for AuthorizationError', () => {
    const res = makeRes();
    errorHandler(new AuthorizationError(), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'FORBIDDEN' }));
  });

  it('should return 429 for RateLimitError', () => {
    const res = makeRes();
    errorHandler(new RateLimitError(), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(429);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'RATE_LIMIT_EXCEEDED' }),
    );
  });

  it('should return 403 for FreeTierLimitError', () => {
    const res = makeRes();
    errorHandler(new FreeTierLimitError('Limit reached'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'FREE_TIER_LIMIT_EXCEEDED' }),
    );
  });

  it('should return 403 for InsufficientScopeError', () => {
    const res = makeRes();
    errorHandler(new InsufficientScopeError('audit:read'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'INSUFFICIENT_SCOPE' }),
    );
  });

  it('should return 404 for AuditEventNotFoundError', () => {
    const res = makeRes();
    errorHandler(new AuditEventNotFoundError(), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(404);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'AUDIT_EVENT_NOT_FOUND' }),
    );
  });

  it('should return 400 for RetentionWindowError', () => {
    const res = makeRes();
    errorHandler(
      new RetentionWindowError(90, '2025-12-28T00:00:00.000Z'),
      req,
      res as unknown as Response,
      next,
    );
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'RETENTION_WINDOW_EXCEEDED' }),
    );
  });

  it('should return 500 for unknown errors', () => {
    const res = makeRes();
    errorHandler(new Error('unexpected'), req, res as unknown as Response, next);
    expect(res.status).toHaveBeenCalledWith(500);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'INTERNAL_SERVER_ERROR' }),
    );
  });

  it('should include details in the response when present', () => {
    const res = makeRes();
    errorHandler(
      new ValidationError('bad', { field: 'email' }),
      req,
      res as unknown as Response,
      next,
    );
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ details: { field: 'email' } }),
    );
  });
});
