import { Request, Response } from 'express';
import {
  correlationMiddleware,
  CORRELATION_ID_HEADER,
  REQUEST_ID_HEADER,
  getCorrelationId,
  getRequestId,
  generateCorrelationId,
  validateCorrelationId,
} from '../correlation.middleware';

// EN: Mock express types
// VI: Mock express types
const mockNext = jest.fn();
const mockJson = jest.fn();
const mockStatus = jest.fn().mockReturnValue({ json: mockJson });
const mockSetHeader = jest.fn();
const mockGet = jest.fn();

// EN: Helper to create mock request/response
// VI: Helper để tạo mock request/response
const createMockReq = (overrides: any = {}): Partial<Request> => ({
  path: '/test',
  method: 'GET',
  headers: {},
  ip: '127.0.0.1',
  get: mockGet,
  ...overrides,
});

const createMockRes = (): Partial<Response> => ({
  setHeader: mockSetHeader,
  status: mockStatus,
  json: mockJson,
  end: jest.fn(),
  write: jest.fn(),
  on: jest.fn(),
});

describe('Correlation Middleware', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockGet.mockReturnValue('test-user-agent');
  });

  describe('correlationMiddleware', () => {
    it('should generate new correlation ID when not provided', () => {
      const mockReq = createMockReq();
      const mockRes = createMockRes();

      const middleware = correlationMiddleware();
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockReq.correlationId).toBeDefined();
      expect(mockReq.requestId).toBeDefined();
      expect(mockReq.correlationId).not.toBe(mockReq.requestId);
      expect(mockNext).toHaveBeenCalled();
    });

    it('should use provided correlation ID from header', () => {
      const existingCorrelationId = 'existing-correlation-id';
      const mockReq = createMockReq({
        headers: { [CORRELATION_ID_HEADER]: existingCorrelationId },
      });
      const mockRes = createMockRes();

      const middleware = correlationMiddleware();
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockReq.correlationId).toBe(existingCorrelationId);
      expect(mockReq.requestId).toBeDefined();
      expect(mockReq.requestId).not.toBe(existingCorrelationId);
    });

    it('should set correlation ID headers on response', () => {
      const mockReq = createMockReq();
      const mockRes = createMockRes();

      const middleware = correlationMiddleware();
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockSetHeader).toHaveBeenCalledWith(CORRELATION_ID_HEADER, mockReq.correlationId);
      expect(mockSetHeader).toHaveBeenCalledWith(REQUEST_ID_HEADER, mockReq.requestId);
    });

    it('should skip correlation ID for health check paths', () => {
      const healthPaths = ['/health', '/health/ready', '/health/live', '/metrics'];

      healthPaths.forEach(path => {
        const mockReq = createMockReq({ path });
        const mockRes = createMockRes();

        const middleware = correlationMiddleware();
        middleware(mockReq as Request, mockRes as Response, mockNext);

        expect(mockReq.correlationId).toBe('');
        expect(mockReq.requestId).toBe('');
        expect(mockSetHeader).not.toHaveBeenCalled();
      });
    });

    it('should use custom header name', () => {
      const customHeader = 'x-custom-correlation-id';
      const mockReq = createMockReq({
        headers: { [customHeader]: 'custom-id' },
      });
      const mockRes = createMockRes();

      const middleware = correlationMiddleware({ headerName: customHeader });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockReq.correlationId).toBe('custom-id');
      expect(mockSetHeader).toHaveBeenCalledWith(customHeader, 'custom-id');
    });

    it('should use custom ID generator', () => {
      const customId = 'custom-generated-id';
      const mockReq = createMockReq();
      const mockRes = createMockRes();

      const middleware = correlationMiddleware({
        generateId: () => customId
      });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockReq.correlationId).toBe(customId);
    });

    it('should handle case-insensitive header names', () => {
      const correlationId = 'test-correlation-id';
      const mockReq = createMockReq({
        headers: { 'X-CORRELATION-ID': correlationId }, // Uppercase
      });
      const mockRes = createMockRes();

      const middleware = correlationMiddleware();
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockReq.correlationId).toBe(correlationId);
    });
  });

  describe('Utility Functions', () => {
    it('should get correlation ID from request', () => {
      const mockReq = createMockReq();
      (mockReq as any).correlationId = 'test-id';

      expect(getCorrelationId(mockReq as Request)).toBe('test-id');
    });

    it('should return empty string if no correlation ID', () => {
      const mockReq = createMockReq();

      expect(getCorrelationId(mockReq as Request)).toBe('');
    });

    it('should get request ID from request', () => {
      const mockReq = createMockReq();
      (mockReq as any).requestId = 'test-request-id';

      expect(getRequestId(mockReq as Request)).toBe('test-request-id');
    });

    it('should generate valid correlation ID', () => {
      const id = generateCorrelationId();

      // UUID v4 regex
      const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
      expect(uuidRegex.test(id)).toBe(true);
    });
  });

  describe('validateCorrelationId', () => {
    it('should pass when correlation ID is provided and valid', () => {
      const correlationId = '123e4567-e89b-12d3-a456-426614174000';
      const mockReq = createMockReq({
        headers: { [CORRELATION_ID_HEADER]: correlationId },
      });
      const mockRes = createMockRes();

      const middleware = validateCorrelationId({ required: true, uuidOnly: true });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      expect(mockStatus).not.toHaveBeenCalled();
    });

    it('should return 400 when required correlation ID is missing', () => {
      const mockReq = createMockReq();
      const mockRes = createMockRes();

      const middleware = validateCorrelationId({ required: true });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockNext).not.toHaveBeenCalled();
      expect(mockStatus).toHaveBeenCalledWith(400);
      expect(mockJson).toHaveBeenCalledWith({
        success: false,
        error: {
          code: 'MISSING_CORRELATION_ID',
          message: `Missing required header: ${CORRELATION_ID_HEADER}`,
        },
        timestamp: expect.any(String),
      });
    });

    it('should return 400 when correlation ID is not a valid UUID', () => {
      const mockReq = createMockReq({
        headers: { [CORRELATION_ID_HEADER]: 'invalid-uuid' },
      });
      const mockRes = createMockRes();

      const middleware = validateCorrelationId({ uuidOnly: true });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockNext).not.toHaveBeenCalled();
      expect(mockStatus).toHaveBeenCalledWith(400);
      expect(mockJson).toHaveBeenCalledWith({
        success: false,
        error: {
          code: 'INVALID_CORRELATION_ID',
          message: `Invalid ${CORRELATION_ID_HEADER} format`,
        },
        timestamp: expect.any(String),
      });
    });

    it('should pass when correlation ID is not required and not provided', () => {
      const mockReq = createMockReq();
      const mockRes = createMockRes();

      const middleware = validateCorrelationId({ required: false });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
    });

    it('should use custom header name', () => {
      const customHeader = 'x-custom-id';
      const mockReq = createMockReq({
        headers: { [customHeader]: 'some-value' },
      });
      const mockRes = createMockRes();

      const middleware = validateCorrelationId({
        required: true,
        headerName: customHeader
      });
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
    });
  });

  describe('Request Logging Integration', () => {
    it('should attach correlation context to request', () => {
      const mockReq = createMockReq();
      const mockRes = createMockRes();

      const middleware = correlationMiddleware();
      middleware(mockReq as Request, mockRes as Response, mockNext);

      expect(mockReq.correlationId).toBeDefined();
      expect(typeof mockReq.correlationId).toBe('string');
      expect(mockReq.correlationId!.length).toBeGreaterThan(0);

      expect(mockReq.requestId).toBeDefined();
      expect(typeof mockReq.requestId).toBe('string');
      expect(mockReq.requestId!.length).toBeGreaterThan(0);
    });
  });
});