// src/middleware/validation.middleware.test.ts
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { validateRequest } from './validation.middleware';
import { ValidationError } from '../services/db/errors.db';

describe('validateRequest Middleware', () => {
  let mockRequest: Partial<Request>;
  let mockResponse: Partial<Response>;
  let mockNext: NextFunction;

  beforeEach(() => {
    // Reset mocks before each test
    // Use `Object.create(null)` to create bare objects for params and query.
    // This more accurately mimics the behavior of Express's request objects
    // and prevents issues with inherited properties when the middleware
    // attempts to delete keys before merging validated data.
    mockRequest = {
      params: Object.create(null),
      query: Object.create(null),
      body: {},
    };
    mockResponse = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
    };
    mockNext = vi.fn();
  });

  it('should call next() and update request with parsed data on successful validation', async () => {
    // Arrange
    const schema = z.object({
      params: z.object({ id: z.coerce.number() }),
      body: z.object({ name: z.string() }),
    });
    mockRequest.params = { id: '123' };
    mockRequest.body = { name: 'Test Name' };

    const middleware = validateRequest(schema);

    // Act
    await middleware(mockRequest as Request, mockResponse as Response, mockNext);

    // Assert
    expect(mockNext).toHaveBeenCalledTimes(1);
    expect(mockNext).toHaveBeenCalledWith(); // Called with no arguments

    // Check that the request objects were updated with coerced/parsed values
    expect(mockRequest.params.id).toBe(123);
    expect(mockRequest.body.name).toBe('Test Name');
  });

  it('should call next() with a ValidationError on validation failure', async () => {
    // Arrange
    const schema = z.object({
      body: z.object({
        email: z.string().email('A valid email is required.'),
        age: z.number().positive(),
      }),
    });
    // Invalid data: email is missing, age is a string
    mockRequest.body = { age: 'twenty' };

    const middleware = validateRequest(schema);

    // Act
    await middleware(mockRequest as Request, mockResponse as Response, mockNext);

    // Assert
    expect(mockNext).toHaveBeenCalledTimes(1);
    const error = (mockNext as Mock).mock.calls[0][0];
    expect(error).toBeInstanceOf(ValidationError);
    expect(error.validationErrors).toHaveLength(2); // Both email and age are invalid
    expect(error.validationErrors).toEqual(
      expect.arrayContaining([
        // Zod reports "Required" or type mismatch for missing fields before running custom validators like .email()
        expect.objectContaining({ path: ['body', 'email'], message: expect.stringMatching(/required|invalid input/i) }),
        expect.objectContaining({ path: ['body', 'age'], message: expect.stringMatching(/expected number/i) }),
      ])
    );
  });

  it('should call next() with a generic error if parsing fails unexpectedly', async () => {
    // Arrange
    const unexpectedError = new Error('Something went wrong during parsing');
    const mockSchema = {
      parseAsync: vi.fn().mockRejectedValue(unexpectedError),
    };
    // For this test, we only need the `parseAsync` method on our mock schema.
    // We cast it to `unknown` first, then to the expected ZodObject type to satisfy TypeScript.
    const middleware = validateRequest(mockSchema as unknown as z.ZodObject<z.ZodRawShape>);

    // Act
    await middleware(mockRequest as Request, mockResponse as Response, mockNext);

    // Assert
    expect(mockNext).toHaveBeenCalledWith(unexpectedError);
  });
});