import { BadRequestException, PayloadTooLargeException } from '@nestjs/common';
import { describe, expect, it } from 'vitest';
import { FileValidationPipe, type UploadedFile } from '../pipes/file-validation.pipe';

function makeFile(overrides: Partial<UploadedFile> = {}): UploadedFile {
  // Minimal valid JPEG: starts with FF D8 FF
  const jpegBuffer = Buffer.from([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10]);
  return {
    fieldname: 'file',
    originalname: 'test.jpg',
    encoding: '7bit',
    mimetype: 'image/jpeg',
    size: 1024,
    buffer: jpegBuffer,
    ...overrides,
  };
}

function makeMp4File(size: number): UploadedFile {
  // MP4 magic bytes: "ftyp" at offset 4
  const buf = Buffer.alloc(Math.max(size, 12));
  buf.write('ftyp', 4, 'ascii');
  return {
    fieldname: 'file',
    originalname: 'test.mp4',
    encoding: '7bit',
    mimetype: 'video/mp4',
    size,
    buffer: buf,
  };
}

describe('FileValidationPipe', () => {
  describe('file required', () => {
    it('should throw BadRequestException when no file provided', () => {
      const pipe = new FileValidationPipe();
      expect(() => pipe.transform(null as unknown as UploadedFile)).toThrow(BadRequestException);
    });
  });

  describe('MIME type validation', () => {
    it('should accept allowed MIME types', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['image/jpeg', 'image/png'],
      });
      const file = makeFile({ mimetype: 'image/jpeg' });
      expect(pipe.transform(file)).toBe(file);
    });

    it('should reject disallowed MIME types', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['image/jpeg'],
      });
      const file = makeFile({ mimetype: 'application/pdf' });
      expect(() => pipe.transform(file)).toThrow(BadRequestException);
      expect(() => pipe.transform(file)).toThrow(/not allowed/);
    });
  });

  describe('size validation', () => {
    it('should accept files within default size limit', () => {
      const pipe = new FileValidationPipe();
      const file = makeFile({ size: 1024 });
      expect(pipe.transform(file)).toBe(file);
    });

    it('should throw PayloadTooLargeException for oversized files', () => {
      const pipe = new FileValidationPipe({ maxSizeBytes: 1024 });
      const file = makeFile({ size: 2048 });
      expect(() => pipe.transform(file)).toThrow(PayloadTooLargeException);
    });

    it('should accept file exactly at the size limit', () => {
      const pipe = new FileValidationPipe({ maxSizeBytes: 1024 });
      const file = makeFile({ size: 1024 });
      expect(pipe.transform(file)).toBe(file);
    });

    it('should reject file 1 byte over the limit', () => {
      const pipe = new FileValidationPipe({ maxSizeBytes: 1024 });
      const file = makeFile({ size: 1025 });
      expect(() => pipe.transform(file)).toThrow(PayloadTooLargeException);
    });
  });

  describe('per-MIME-type size limits', () => {
    const pipe = new FileValidationPipe({
      maxSizeBytes: 10 * 1024 * 1024, // 10 MB default
      maxSizeByMimeType: {
        'video/mp4': 100 * 1024 * 1024, // 100 MB for video
      },
      allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'video/mp4'],
    });

    it('should enforce default limit for images', () => {
      const oversizedImage = makeFile({ size: 11 * 1024 * 1024 });
      expect(() => pipe.transform(oversizedImage)).toThrow(PayloadTooLargeException);
    });

    it('should allow images within default limit', () => {
      const validImage = makeFile({ size: 5 * 1024 * 1024 });
      expect(pipe.transform(validImage)).toBe(validImage);
    });

    it('should allow videos up to per-type limit (100MB)', () => {
      const validVideo = makeMp4File(50 * 1024 * 1024);
      expect(pipe.transform(validVideo)).toBe(validVideo);
    });

    it('should reject videos exceeding per-type limit', () => {
      const oversizedVideo = makeMp4File(101 * 1024 * 1024);
      expect(() => pipe.transform(oversizedVideo)).toThrow(PayloadTooLargeException);
      expect(() => pipe.transform(makeMp4File(101 * 1024 * 1024))).toThrow(/100MB/);
    });

    it('should use per-type limit instead of default when available', () => {
      // 50MB video — over 10MB default but under 100MB per-type limit
      const largeVideo = makeMp4File(50 * 1024 * 1024);
      expect(pipe.transform(largeVideo)).toBe(largeVideo);
    });
  });

  describe('magic bytes validation', () => {
    it('should reject file with mismatched magic bytes', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['image/jpeg'],
        verifyMagicBytes: true,
      });
      // PNG magic bytes but declared as JPEG
      const file = makeFile({
        mimetype: 'image/jpeg',
        buffer: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
      });
      expect(() => pipe.transform(file)).toThrow(BadRequestException);
      expect(() => pipe.transform(makeFile({
        mimetype: 'image/jpeg',
        buffer: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
      }))).toThrow(/does not match/);
    });

    it('should accept file with correct magic bytes', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['image/png'],
      });
      const file = makeFile({
        mimetype: 'image/png',
        buffer: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
        size: 100,
      });
      expect(pipe.transform(file)).toBe(file);
    });

    it('should skip magic byte check when disabled', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['image/jpeg'],
        verifyMagicBytes: false,
      });
      // Wrong magic bytes but verification disabled
      const file = makeFile({
        buffer: Buffer.from([0x00, 0x00, 0x00, 0x00]),
      });
      expect(pipe.transform(file)).toBe(file);
    });

    it('should validate MP4 ftyp signature at offset 4', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['video/mp4'],
      });
      const validMp4 = makeMp4File(1024);
      expect(pipe.transform(validMp4)).toBe(validMp4);
    });
  });

  describe('error messages', () => {
    it('should include MIME type in size error message', () => {
      const pipe = new FileValidationPipe({
        maxSizeBytes: 1024,
        allowedMimeTypes: ['image/jpeg'],
      });
      const file = makeFile({ size: 2048 });
      expect(() => pipe.transform(file)).toThrow(/image\/jpeg/);
    });

    it('should list allowed types in MIME error message', () => {
      const pipe = new FileValidationPipe({
        allowedMimeTypes: ['image/jpeg', 'image/png'],
      });
      const file = makeFile({ mimetype: 'text/plain' });
      expect(() => pipe.transform(file)).toThrow(/image\/jpeg, image\/png/);
    });
  });
});
