/* eslint-disable no-unused-vars */
/* eslint-disable jest/no-done-callback */
const fs = require('fs');
const os = require('os');
const path = require('path');
const crypto = require('crypto');
const { createMulterInstance, storage, importFileFilter } = require('./multer');

// Mock only the config service that requires external dependencies
jest.mock('~/server/services/Config', () => ({
  getAppConfig: jest.fn(),
}));

describe('Multer Configuration', () => {
  let tempDir;
  let mockReq;
  let mockFile;

  beforeEach(() => {
    // Create a temporary directory for each test
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'multer-test-'));

    mockReq = {
      user: { id: 'test-user-123' },
      body: {},
      originalUrl: '/api/files/upload',
      config: {
        paths: {
          uploads: tempDir,
        },
      },
    };

    mockFile = {
      originalname: 'test-file.jpg',
      mimetype: 'image/jpeg',
      size: 1024,
    };

    // Clear mocks
    jest.clearAllMocks();
  });

  afterEach(() => {
    // Clean up temporary directory
    if (fs.existsSync(tempDir)) {
      fs.rmSync(tempDir, { recursive: true, force: true });
    }
  });

  describe('Storage Configuration', () => {
    describe('destination function', () => {
      it('should create the correct destination path', (done) => {
        const cb = jest.fn((err, destination) => {
          expect(err).toBeNull();
          expect(destination).toBe(path.join(tempDir, 'temp', 'test-user-123'));
          expect(fs.existsSync(destination)).toBe(true);
          done();
        });

        storage.getDestination(mockReq, mockFile, cb);
      });

      it("should create directory recursively if it doesn't exist", (done) => {
        const deepPath = path.join(tempDir, 'deep', 'nested', 'path');
        mockReq.config.paths.uploads = deepPath;

        const cb = jest.fn((err, destination) => {
          expect(err).toBeNull();
          expect(destination).toBe(path.join(deepPath, 'temp', 'test-user-123'));
          expect(fs.existsSync(destination)).toBe(true);
          done();
        });

        storage.getDestination(mockReq, mockFile, cb);
      });
    });

    describe('filename function', () => {
      it('should generate a UUID for req.file_id', (done) => {
        const cb = jest.fn((err, filename) => {
          expect(err).toBeNull();
          expect(mockReq.file_id).toBeDefined();
          expect(mockReq.file_id).toMatch(
            /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
          );
          done();
        });

        storage.getFilename(mockReq, mockFile, cb);
      });

      it('should decode URI components in filename', (done) => {
        const encodedFile = {
          ...mockFile,
          originalname: encodeURIComponent('test file with spaces.jpg'),
        };

        const cb = jest.fn((err, filename) => {
          expect(err).toBeNull();
          expect(encodedFile.originalname).toBe('test file with spaces.jpg');
          done();
        });

        storage.getFilename(mockReq, encodedFile, cb);
      });

      it('should call real sanitizeFilename with properly encoded filename', (done) => {
        // Test with a properly URI-encoded filename that needs sanitization
        const unsafeFile = {
          ...mockFile,
          originalname: encodeURIComponent('test@#$%^&*()file with spaces!.jpg'),
        };

        const cb = jest.fn((err, filename) => {
          expect(err).toBeNull();
          // The actual sanitizeFilename should have cleaned this up after decoding
          expect(filename).not.toContain('@');
          expect(filename).not.toContain('#');
          expect(filename).not.toContain('*');
          expect(filename).not.toContain('!');
          // Should still preserve dots and hyphens
          expect(filename).toContain('.jpg');
          done();
        });

        storage.getFilename(mockReq, unsafeFile, cb);
      });

      it('should handle very long filenames with actual crypto', (done) => {
        const longFile = {
          ...mockFile,
          originalname: 'a'.repeat(300) + '.jpg',
        };

        const cb = jest.fn((err, filename) => {
          expect(err).toBeNull();
          expect(filename.length).toBeLessThanOrEqual(255);
          expect(filename).toMatch(/\.jpg$/); // Should still end with .jpg
          // Should contain a hex suffix if truncated
          if (filename.length === 255) {
            expect(filename).toMatch(/-[a-f0-9]{6}\.jpg$/);
          }
          done();
        });

        storage.getFilename(mockReq, longFile, cb);
      });

      it('should generate unique file_id for each call', (done) => {
        let firstFileId;

        const firstCb = jest.fn((err, filename) => {
          expect(err).toBeNull();
          firstFileId = mockReq.file_id;

          // Reset req for second call
          delete mockReq.file_id;

          const secondCb = jest.fn((err, filename) => {
            expect(err).toBeNull();
            expect(mockReq.file_id).toBeDefined();
            expect(mockReq.file_id).not.toBe(firstFileId);
            done();
          });

          storage.getFilename(mockReq, mockFile, secondCb);
        });

        storage.getFilename(mockReq, mockFile, firstCb);
      });
    });
  });

  describe('Import File Filter', () => {
    it('should accept JSON files by mimetype', (done) => {
      const jsonFile = {
        ...mockFile,
        mimetype: 'application/json',
        originalname: 'data.json',
      };

      const cb = jest.fn((err, result) => {
        expect(err).toBeNull();
        expect(result).toBe(true);
        done();
      });

      importFileFilter(mockReq, jsonFile, cb);
    });

    it('should accept files with .json extension', (done) => {
      const jsonFile = {
        ...mockFile,
        mimetype: 'text/plain',
        originalname: 'data.json',
      };

      const cb = jest.fn((err, result) => {
        expect(err).toBeNull();
        expect(result).toBe(true);
        done();
      });

      importFileFilter(mockReq, jsonFile, cb);
    });

    it('should reject non-JSON files', (done) => {
      const textFile = {
        ...mockFile,
        mimetype: 'text/plain',
        originalname: 'document.txt',
      };

      const cb = jest.fn((err, result) => {
        expect(err).toBeInstanceOf(Error);
        expect(err.message).toBe('Only JSON files are allowed');
        expect(result).toBe(false);
        done();
      });

      importFileFilter(mockReq, textFile, cb);
    });

    it('should handle files with uppercase .JSON extension', (done) => {
      const jsonFile = {
        ...mockFile,
        mimetype: 'text/plain',
        originalname: 'DATA.JSON',
      };

      const cb = jest.fn((err, result) => {
        expect(err).toBeNull();
        expect(result).toBe(true);
        done();
      });

      importFileFilter(mockReq, jsonFile, cb);
    });
  });

  describe('File Filter with Real defaultFileConfig', () => {
    it('should use real fileConfig.checkType for validation', async () => {
      // Test with actual librechat-data-provider functions
      const {
        fileConfig,
        imageMimeTypes,
        applicationMimeTypes,
      } = require('librechat-data-provider');

      // Test that the real checkType function works with regex patterns
      expect(fileConfig.checkType('image/jpeg', [imageMimeTypes])).toBe(true);
      expect(fileConfig.checkType('video/mp4', [imageMimeTypes])).toBe(false);
      expect(fileConfig.checkType('application/pdf', [applicationMimeTypes])).toBe(true);
      expect(fileConfig.checkType('application/pdf', [])).toBe(false);
    });

    it('should handle audio files for speech-to-text endpoint with real config', async () => {
      mockReq.originalUrl = '/api/speech/stt';

      const multerInstance = await createMulterInstance();
      expect(multerInstance).toBeDefined();
      expect(typeof multerInstance.single).toBe('function');
    });

    it('should reject unsupported file types using real config', async () => {
      // Mock defaultFileConfig for this specific test
      const originalCheckType = require('librechat-data-provider').fileConfig.checkType;
      const mockCheckType = jest.fn().mockReturnValue(false);
      require('librechat-data-provider').fileConfig.checkType = mockCheckType;

      try {
        const multerInstance = await createMulterInstance();
        expect(multerInstance).toBeDefined();

        // Test the actual file filter behavior would reject unsupported files
        expect(mockCheckType).toBeDefined();
      } finally {
        // Restore original function
        require('librechat-data-provider').fileConfig.checkType = originalCheckType;
      }
    });

    it('should use real mergeFileConfig function', async () => {
      const { mergeFileConfig, mbToBytes } = require('librechat-data-provider');

      // Test with actual merge function - note that it converts MB to bytes
      const testConfig = {
        serverFileSizeLimit: 5, // 5 MB
        endpoints: {
          custom: {
            supportedMimeTypes: ['text/plain'],
          },
        },
      };

      const result = mergeFileConfig(testConfig);

      // The function converts MB to bytes, so 5 MB becomes 5 * 1024 * 1024 bytes
      expect(result.serverFileSizeLimit).toBe(mbToBytes(5));
      expect(result.endpoints.custom.supportedMimeTypes).toBeDefined();
      // Should still have the default endpoints
      expect(result.endpoints.default).toBeDefined();
    });
  });

  describe('createMulterInstance with Real Functions', () => {
    it('should create a multer instance with correct configuration', async () => {
      const multerInstance = await createMulterInstance();

      expect(multerInstance).toBeDefined();
      expect(typeof multerInstance.single).toBe('function');
      expect(typeof multerInstance.array).toBe('function');
      expect(typeof multerInstance.fields).toBe('function');
    });

    it('should use real config merging', async () => {
      const { getAppConfig } = require('~/server/services/Config');

      const multerInstance = await createMulterInstance();

      expect(getAppConfig).toHaveBeenCalled();
      expect(multerInstance).toBeDefined();
    });

    it('should create multer instance with expected interface', async () => {
      const multerInstance = await createMulterInstance();

      expect(multerInstance).toBeDefined();
      expect(typeof multerInstance.single).toBe('function');
      expect(typeof multerInstance.array).toBe('function');
      expect(typeof multerInstance.fields).toBe('function');
    });
  });

  describe('Real Crypto Integration', () => {
    it('should use actual crypto.randomUUID()', (done) => {
      // Spy on crypto.randomUUID to ensure it's called
      const uuidSpy = jest.spyOn(crypto, 'randomUUID');

      const cb = jest.fn((err, filename) => {
        expect(err).toBeNull();
        expect(uuidSpy).toHaveBeenCalled();
        expect(mockReq.file_id).toMatch(
          /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
        );

        uuidSpy.mockRestore();
        done();
      });

      storage.getFilename(mockReq, mockFile, cb);
    });

    it('should generate different UUIDs on subsequent calls', (done) => {
      const uuids = [];
      let callCount = 0;
      const totalCalls = 5;

      const cb = jest.fn((err, filename) => {
        expect(err).toBeNull();
        uuids.push(mockReq.file_id);
        callCount++;

        if (callCount === totalCalls) {
          // Check that all UUIDs are unique
          const uniqueUuids = new Set(uuids);
          expect(uniqueUuids.size).toBe(totalCalls);
          done();
        } else {
          // Reset for next call
          delete mockReq.file_id;
          storage.getFilename(mockReq, mockFile, cb);
        }
      });

      // Start the chain
      storage.getFilename(mockReq, mockFile, cb);
    });

    it('should generate cryptographically secure UUIDs', (done) => {
      const generatedUuids = new Set();
      let callCount = 0;
      const totalCalls = 10;

      const cb = jest.fn((err, filename) => {
        expect(err).toBeNull();

        // Verify UUID format and uniqueness
        expect(mockReq.file_id).toMatch(
          /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
        );

        generatedUuids.add(mockReq.file_id);
        callCount++;

        if (callCount === totalCalls) {
          // All UUIDs should be unique
          expect(generatedUuids.size).toBe(totalCalls);
          done();
        } else {
          // Reset for next call
          delete mockReq.file_id;
          storage.getFilename(mockReq, mockFile, cb);
        }
      });

      // Start the chain
      storage.getFilename(mockReq, mockFile, cb);
    });
  });

  describe('Error Handling', () => {
    it('should handle CVE-2024-28870: empty field name DoS vulnerability', async () => {
      // Test for the CVE where empty field name could cause unhandled exception
      const multerInstance = await createMulterInstance();

      // Create a mock request with empty field name (the vulnerability scenario)
      const mockReqWithEmptyField = {
        ...mockReq,
        headers: {
          'content-type': 'multipart/form-data',
        },
      };

      const mockRes = {
        status: jest.fn().mockReturnThis(),
        json: jest.fn(),
        end: jest.fn(),
      };

      // This should not crash or throw unhandled exceptions
      const uploadMiddleware = multerInstance.single(''); // Empty field name

      const mockNext = jest.fn((err) => {
        // If there's an error, it should be handled gracefully, not crash
        if (err) {
          expect(err).toBeInstanceOf(Error);
          // The error should be handled, not crash the process
        }
      });

      // This should complete without crashing the process
      expect(() => {
        uploadMiddleware(mockReqWithEmptyField, mockRes, mockNext);
      }).not.toThrow();
    });

    it('should handle file system errors when directory creation fails', () => {
      // Test with a non-existent parent directory to simulate fs issues
      const invalidPath = '/nonexistent/path/that/should/not/exist';
      mockReq.config.paths.uploads = invalidPath;

      // The current implementation doesn't catch errors, so they're thrown synchronously
      expect(() => {
        storage.getDestination(mockReq, mockFile, jest.fn());
      }).toThrow();
    });

    it('should handle malformed filenames with real sanitization', (done) => {
      const malformedFile = {
        ...mockFile,
        originalname: null, // This should be handled gracefully
      };

      const cb = jest.fn((err, filename) => {
        // The function should handle this gracefully
        expect(typeof err === 'object' || err === null).toBe(true);
        done();
      });

      try {
        storage.getFilename(mockReq, malformedFile, cb);
      } catch (error) {
        // If it throws, that's also acceptable behavior
        done();
      }
    });

    it('should handle edge cases in filename sanitization', (done) => {
      const edgeCaseFiles = [
        { originalname: '', expected: /_/ },
        { originalname: '.hidden', expected: /^_\.hidden/ },
        { originalname: '../../../etc/passwd', expected: /passwd/ },
        { originalname: 'file\x00name.txt', expected: /file_name\.txt/ },
      ];

      let testCount = 0;

      const testNextFile = (fileData) => {
        const fileToTest = { ...mockFile, originalname: fileData.originalname };

        const cb = jest.fn((err, filename) => {
          expect(err).toBeNull();
          expect(filename).toMatch(fileData.expected);

          testCount++;
          if (testCount === edgeCaseFiles.length) {
            done();
          } else {
            testNextFile(edgeCaseFiles[testCount]);
          }
        });

        storage.getFilename(mockReq, fileToTest, cb);
      };

      testNextFile(edgeCaseFiles[0]);
    });
  });

  describe('Real Configuration Testing', () => {
    it('should handle missing custom config gracefully with real mergeFileConfig', async () => {
      const { getAppConfig } = require('~/server/services/Config');

      // Mock getAppConfig to return undefined
      getAppConfig.mockResolvedValueOnce(undefined);

      const multerInstance = await createMulterInstance();
      expect(multerInstance).toBeDefined();
      expect(typeof multerInstance.single).toBe('function');
    });

    it('should properly integrate real fileConfig with custom endpoints', async () => {
      const { getAppConfig } = require('~/server/services/Config');

      // Mock appConfig with fileConfig
      getAppConfig.mockResolvedValueOnce({
        paths: {
          uploads: tempDir,
        },
        fileConfig: {
          endpoints: {
            anthropic: {
              supportedMimeTypes: ['text/plain', 'image/png'],
            },
          },
          serverFileSizeLimit: 20971520, // 20 MB in bytes (mergeFileConfig converts)
        },
      });

      const multerInstance = await createMulterInstance();
      expect(multerInstance).toBeDefined();

      // Verify that getAppConfig was called
      expect(getAppConfig).toHaveBeenCalled();
    });
  });
});
