// src/middleware/multer.middleware.test.ts
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import multer from 'multer';
import type { Request, Response, NextFunction } from 'express';
import { createUploadMiddleware, handleMulterError } from './multer.middleware';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import { ValidationError } from '../services/db/errors.db';

// 1. Hoist the mocks so they can be referenced inside vi.mock factories.
const mocks = vi.hoisted(() => ({
  mkdir: vi.fn(),
  logger: {
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
  },
}));

// 2. Mock node:fs/promises.
// We mock the default export because that's how it's imported in the source file.
vi.mock('node:fs/promises', () => ({
  default: {
    mkdir: mocks.mkdir,
  },
}));

// 3. Mock the logger service.
vi.mock('../services/logger.server', () => ({
  logger: mocks.logger,
}));

// 4. Mock multer to prevent it from doing anything during import.
vi.mock('multer', () => {
  const diskStorage = vi.fn((options) => options);
  // A more realistic mock for MulterError that maps error codes to messages,
  // similar to how the actual multer library works.
  class MulterError extends Error {
    code: string;
    field?: string;

    constructor(code: string, field?: string) {
      const messages: { [key: string]: string } = {
        LIMIT_FILE_SIZE: 'File too large',
        LIMIT_UNEXPECTED_FILE: 'Unexpected file',
        // Add other codes as needed for tests
      };
      const message = messages[code] || code;
      super(message);
      this.code = code;
      this.name = 'MulterError';
      if (field) {
        this.field = field;
      }
    }
  }
  const multer = vi.fn(() => ({
    single: vi.fn().mockImplementation(() => (req: any, res: any, next: any) => next()),
    array: vi.fn().mockImplementation(() => (req: any, res: any, next: any) => next()),
  }));
  (multer as any).diskStorage = diskStorage;
  (multer as any).MulterError = MulterError;
  return {
    default: multer,
    diskStorage,
    MulterError,
  };
});

describe('Multer Middleware Directory Creation', () => {
  beforeEach(() => {
    // Critical: Reset modules to ensure the top-level IIFE runs again for each test.
    vi.resetModules();
    vi.clearAllMocks();
  });

  it('should attempt to create directories on module load and log success', async () => {
    // Arrange
    mocks.mkdir.mockResolvedValue(undefined);

    // Act: Dynamic import triggers the top-level code execution
    await import('./multer.middleware');

    // Assert
    // It should try to create both the flyer storage and avatar storage paths
    expect(mocks.mkdir).toHaveBeenCalledTimes(2);
    expect(mocks.mkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true });
    expect(mocks.logger.info).toHaveBeenCalledWith('Ensured multer storage directories exist.');
    expect(mocks.logger.error).not.toHaveBeenCalled();
  });

  it('should log an error if directory creation fails', async () => {
    // Arrange
    const error = new Error('Permission denied');
    mocks.mkdir.mockRejectedValue(error);

    // Act
    await import('./multer.middleware');

    // Assert
    expect(mocks.mkdir).toHaveBeenCalled();
    expect(mocks.logger.error).toHaveBeenCalledWith(
      { error },
      'Failed to create multer storage directories on startup.',
    );
  });
});

describe('createUploadMiddleware', () => {
  const mockFile = { originalname: 'test.png' } as Express.Multer.File;
  const mockUser = createMockUserProfile({ user: { user_id: 'user-123', email: 'test@user.com' } });
  let originalNodeEnv: string | undefined;

  beforeEach(() => {
    vi.clearAllMocks();
    originalNodeEnv = process.env.NODE_ENV;
  });

  afterEach(() => {
    process.env.NODE_ENV = originalNodeEnv;
  });

  describe('Avatar Storage', () => {
    it('should generate a unique filename for an authenticated user', () => {
      process.env.NODE_ENV = 'production';
      createUploadMiddleware({ storageType: 'avatar' });
      const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
      const cb = vi.fn();
      const mockReq = { user: mockUser } as unknown as Request;

      storageOptions.filename!(mockReq, mockFile, cb);

      expect(cb).toHaveBeenCalledWith(null, expect.stringContaining('user-123-'));
      expect(cb).toHaveBeenCalledWith(null, expect.stringContaining('.png'));
    });

    it('should call the callback with an error for an unauthenticated user', () => {
      // This test covers line 37
      createUploadMiddleware({ storageType: 'avatar' });
      const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
      const cb = vi.fn();
      const mockReq = {} as Request; // No user on request

      storageOptions.filename!(mockReq, mockFile, cb);

      expect(cb).toHaveBeenCalledWith(
        new Error('User not authenticated for avatar upload'),
        expect.any(String),
      );
    });

    it('should use a predictable filename in test environment', () => {
      process.env.NODE_ENV = 'test';
      createUploadMiddleware({ storageType: 'avatar' });
      const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
      const cb = vi.fn();
      const mockReq = { user: mockUser } as unknown as Request;

      storageOptions.filename!(mockReq, mockFile, cb);

      expect(cb).toHaveBeenCalledWith(null, 'test-avatar.png');
    });
  });

  describe('Flyer Storage', () => {
    it('should generate a unique, sanitized filename in production environment', () => {
      process.env.NODE_ENV = 'production';
      const mockFlyerFile = {
        fieldname: 'flyerFile',
        originalname: 'My Flyer (Special!).pdf',
      } as Express.Multer.File;
      createUploadMiddleware({ storageType: 'flyer' });
      const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
      const cb = vi.fn();
      const mockReq = {} as Request;

      storageOptions.filename!(mockReq, mockFlyerFile, cb);

      expect(cb).toHaveBeenCalledWith(
        null,
        expect.stringMatching(/^flyerFile-\d+-\d+-my-flyer-special\.pdf$/i),
      );
    });

    it('should generate a predictable filename in test environment', () => {
      // This test covers lines 43-46
      process.env.NODE_ENV = 'test';
      const mockFlyerFile = {
        fieldname: 'flyerFile',
        originalname: 'test-flyer.jpg',
      } as Express.Multer.File;
      createUploadMiddleware({ storageType: 'flyer' });
      const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
      const cb = vi.fn();
      const mockReq = {} as Request;

      storageOptions.filename!(mockReq, mockFlyerFile, cb);

      expect(cb).toHaveBeenCalledWith(null, 'flyerFile-test-flyer-image.jpg');
    });
  });

  describe('Image File Filter', () => {
    it('should accept files with an image mimetype', () => {
      createUploadMiddleware({ storageType: 'flyer', fileFilter: 'image' });
      const multerOptions = vi.mocked(multer).mock.calls[0][0];
      const cb = vi.fn();
      const mockImageFile = { mimetype: 'image/png' } as Express.Multer.File;

      multerOptions!.fileFilter!({} as Request, mockImageFile, cb);

      expect(cb).toHaveBeenCalledWith(null, true);
    });

    it('should reject files without an image mimetype', () => {
      createUploadMiddleware({ storageType: 'flyer', fileFilter: 'image' });
      const multerOptions = vi.mocked(multer).mock.calls[0][0];
      const cb = vi.fn();
      const mockTextFile = { mimetype: 'text/plain' } as Express.Multer.File;

      multerOptions!.fileFilter!({} as Request, { ...mockTextFile, fieldname: 'test' }, cb);

      const error = (cb as Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(ValidationError);
      expect(error.validationErrors[0].message).toBe('Only image files are allowed!');
    });
  });
});

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

  beforeEach(() => {
    mockRequest = {};
    mockResponse = {
      status: vi.fn().mockReturnThis(),
      json: vi.fn(),
    };
    mockNext = vi.fn();
  });

  it('should handle a MulterError (e.g., file too large)', () => {
    const err = new multer.MulterError('LIMIT_FILE_SIZE');
    handleMulterError(err, mockRequest as Request, mockResponse as Response, mockNext);
    expect(mockResponse.status).toHaveBeenCalledWith(400);
    expect(mockResponse.json).toHaveBeenCalledWith({
      message: 'File upload error: File too large',
    });
    expect(mockNext).not.toHaveBeenCalled();
  });

  it('should pass on a ValidationError to the next handler', () => {
    const err = new ValidationError([], 'Only image files are allowed!');
    handleMulterError(err, mockRequest as Request, mockResponse as Response, mockNext);
    // It should now pass the error to the global error handler
    expect(mockNext).toHaveBeenCalledWith(err);
    expect(mockResponse.status).not.toHaveBeenCalled();
    expect(mockResponse.json).not.toHaveBeenCalled();
  });

  it('should pass on non-multer errors to the next error handler', () => {
    const err = new Error('A generic error');
    handleMulterError(err, mockRequest as Request, mockResponse as Response, mockNext);
    expect(mockNext).toHaveBeenCalledWith(err);
    expect(mockResponse.status).not.toHaveBeenCalled();
  });
});