// ABOUTME: Unit tests for request-logging middleware
// ABOUTME: Tests correlation ID extraction/generation and request logging behaviour

import { Request, Response } from 'express';
import { correlationIdMiddleware, requestLoggingMiddleware } from '../request-logging';

// Mock logger
jest.mock('../../utils/logger', () => ({
  __esModule: true,
  default: {
    info: jest.fn(),
    debug: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
  },
}));

import logger from '../../utils/logger';

describe('correlationIdMiddleware', () => {
  let req: Partial<Request>;
  let res: Partial<Response>;
  let next: jest.Mock;

  beforeEach(() => {
    req = {
      headers: {},
    };
    res = {
      setHeader: jest.fn(),
    };
    next = jest.fn();
    jest.clearAllMocks();
  });

  it('should generate a correlation ID when no header is present', () => {
    correlationIdMiddleware(req as Request, res as Response, next);

    expect(req.correlationId).toBeDefined();
    expect(typeof req.correlationId).toBe('string');
    expect(req.correlationId!.length).toBeGreaterThan(0);
    expect(res.setHeader).toHaveBeenCalledWith('X-Correlation-Id', req.correlationId);
    expect(next).toHaveBeenCalledTimes(1);
  });

  it('should use the x-correlation-id header when present', () => {
    const existingId = 'my-trace-id-123';
    req.headers = { 'x-correlation-id': existingId };

    correlationIdMiddleware(req as Request, res as Response, next);

    expect(req.correlationId).toBe(existingId);
    expect(res.setHeader).toHaveBeenCalledWith('X-Correlation-Id', existingId);
    expect(next).toHaveBeenCalledTimes(1);
  });

  it('should generate unique correlation IDs for separate requests', () => {
    const req2: Partial<Request> = { headers: {} };
    const res2: Partial<Response> = { setHeader: jest.fn() };

    correlationIdMiddleware(req as Request, res as Response, next);
    correlationIdMiddleware(req2 as Request, res2 as Response, jest.fn());

    expect(req.correlationId).not.toBe(req2.correlationId);
  });

  it('should trim whitespace from the x-correlation-id header', () => {
    req.headers = { 'x-correlation-id': '  my-id-with-spaces  ' };

    correlationIdMiddleware(req as Request, res as Response, next);

    expect(req.correlationId).toBe('  my-id-with-spaces  ');
    expect(res.setHeader).toHaveBeenCalledWith('X-Correlation-Id', '  my-id-with-spaces  ');
    expect(next).toHaveBeenCalledTimes(1);
  });
});

describe('requestLoggingMiddleware', () => {
  let req: Partial<Request>;
  let res: Partial<Response>;
  let next: jest.Mock;

  beforeEach(() => {
    jest.clearAllMocks();

    req = {
      correlationId: 'test-correlation-id',
      method: 'GET',
      path: '/test-path',
      headers: {
        'user-agent': 'test-agent',
      },
    };

    res = {
      statusCode: 200,
      on: jest.fn().mockImplementation((event: string, handler: () => void) => {
        if (event === 'finish') {
          // Simulate the finish event
          handler();
        }
        return res;
      }),
    } as any;

    next = jest.fn();
  });

  it('should set startTime on the request', () => {
    requestLoggingMiddleware(req as Request, res as Response, next);

    expect(req.startTime).toBeDefined();
    expect(typeof req.startTime).toBe('number');
    expect(next).toHaveBeenCalledTimes(1);
  });

  it('should log request details on finish', () => {
    requestLoggingMiddleware(req as Request, res as Response, next);

    expect(logger.info).toHaveBeenCalledTimes(1);
    expect(logger.info).toHaveBeenCalledWith(
      expect.objectContaining({
        msg: 'Request completed',
        correlationId: 'test-correlation-id',
        method: 'GET',
        path: '/test-path',
        statusCode: 200,
        userAgent: 'test-agent',
      })
    );
  });

  it('should log durationMs as a number', () => {
    requestLoggingMiddleware(req as Request, res as Response, next);

    expect(logger.info).toHaveBeenCalledWith(
      expect.objectContaining({
        durationMs: expect.any(Number),
      })
    );
  });

  it('should work without a user-agent header', () => {
    req.headers = {};

    requestLoggingMiddleware(req as Request, res as Response, next);

    expect(logger.info).toHaveBeenCalledWith(
      expect.objectContaining({
        userAgent: undefined,
      })
    );
  });

  it('should call next() and log on finish', () => {
    requestLoggingMiddleware(req as Request, res as Response, next);

    // next() must be called (the finish handler fires synchronously in our mock)
    expect(next).toHaveBeenCalledTimes(1);
    expect(logger.info).toHaveBeenCalledTimes(1);
  });
});
