import { Test, TestingModule } from '@nestjs/testing';
import { StorageService } from './storage.service';
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
  HeadObjectCommand,
} from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { Readable } from 'stream';

// Mock AWS SDK
jest.mock('@aws-sdk/client-s3');
jest.mock('@aws-sdk/lib-storage');
jest.mock('@aws-sdk/s3-request-presigner');

describe('StorageService', () => {
  let service: StorageService;
  let mockS3Client: jest.Mocked<S3Client>;

  const mockBuffer = Buffer.from('test file content');
  const mockImageBuffer = Buffer.from('fake image data');

  beforeEach(async () => {
    // Reset mocks
    jest.clearAllMocks();

    // Mock S3Client
    mockS3Client = new S3Client({}) as jest.Mocked<S3Client>;
    (S3Client as jest.Mock).mockImplementation(() => mockS3Client);

    const module: TestingModule = await Test.createTestingModule({
      providers: [StorageService],
    }).compile();

    service = module.get<StorageService>(StorageService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('uploadFile', () => {
    it('should upload file successfully', async () => {
      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      const result = await service.uploadFile(
        mockBuffer,
        'test/file.txt',
        'text/plain',
      );

      expect(result).toHaveProperty('key', 'test/file.txt');
      expect(result).toHaveProperty('bucket', 'maternal-app');
      expect(result).toHaveProperty('url');
      expect(result).toHaveProperty('size', mockBuffer.length);
      expect(result).toHaveProperty('mimeType', 'text/plain');
      expect(mockUpload.done).toHaveBeenCalled();
    });

    it('should upload with metadata', async () => {
      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      const metadata = { userId: 'user_123', type: 'photo' };
      await service.uploadFile(
        mockBuffer,
        'test/file.jpg',
        'image/jpeg',
        metadata,
      );

      expect(Upload).toHaveBeenCalledWith(
        expect.objectContaining({
          params: expect.objectContaining({
            Metadata: metadata,
          }),
        }),
      );
    });

    it('should throw error on upload failure', async () => {
      const mockUpload = {
        done: jest.fn().mockRejectedValue(new Error('Upload failed')),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      await expect(
        service.uploadFile(mockBuffer, 'test/file.txt', 'text/plain'),
      ).rejects.toThrow('File upload failed');
    });
  });

  describe('uploadImage', () => {
    it('should upload and optimize image', async () => {
      // Mock sharp
      const mockSharp = {
        metadata: jest.fn().mockResolvedValue({
          width: 2000,
          height: 1500,
          format: 'jpeg',
        }),
        resize: jest.fn().mockReturnThis(),
        jpeg: jest.fn().mockReturnThis(),
        toBuffer: jest.fn().mockResolvedValue(mockImageBuffer),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      const result = await service.uploadImage(
        mockImageBuffer,
        'photos/photo.jpg',
      );

      expect(result).toHaveProperty('key', 'photos/photo.jpg');
      expect(result).toHaveProperty('metadata');
      expect(result.metadata).toHaveProperty('width');
      expect(result.metadata).toHaveProperty('height');
      expect(mockSharp.resize).toHaveBeenCalled();
      expect(mockSharp.jpeg).toHaveBeenCalled();
    });

    it('should use custom optimization options', async () => {
      const mockSharp = {
        metadata: jest.fn().mockResolvedValue({
          width: 1000,
          height: 800,
          format: 'jpeg',
        }),
        resize: jest.fn().mockReturnThis(),
        jpeg: jest.fn().mockReturnThis(),
        toBuffer: jest.fn().mockResolvedValue(mockImageBuffer),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      await service.uploadImage(mockImageBuffer, 'photos/photo.jpg', {
        maxWidth: 800,
        maxHeight: 600,
        quality: 70,
      });

      expect(mockSharp.resize).toHaveBeenCalledWith(800, 600, expect.any(Object));
      expect(mockSharp.jpeg).toHaveBeenCalledWith({ quality: 70 });
    });

    it('should handle images that don\'t need resizing', async () => {
      const mockSharp = {
        metadata: jest.fn().mockResolvedValue({
          width: 800,
          height: 600,
          format: 'jpeg',
        }),
        resize: jest.fn().mockReturnThis(),
        jpeg: jest.fn().mockReturnThis(),
        toBuffer: jest.fn().mockResolvedValue(mockImageBuffer),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      await service.uploadImage(mockImageBuffer, 'photos/small.jpg');

      // Should still optimize quality even if no resize
      expect(mockSharp.jpeg).toHaveBeenCalled();
    });

    it('should throw error if sharp is not available', async () => {
      jest
        .spyOn(service as any, 'getSharp')
        .mockRejectedValue(new Error('Sharp not available'));

      await expect(
        service.uploadImage(mockImageBuffer, 'photos/photo.jpg'),
      ).rejects.toThrow('Image upload failed');
    });
  });

  describe('generateThumbnail', () => {
    it('should generate thumbnail with default size', async () => {
      const mockSharp = {
        resize: jest.fn().mockReturnThis(),
        jpeg: jest.fn().mockReturnThis(),
        toBuffer: jest.fn().mockResolvedValue(mockImageBuffer),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      await service.generateThumbnail(
        mockImageBuffer,
        'photos/thumb_photo.jpg',
      );

      expect(mockSharp.resize).toHaveBeenCalledWith(200, 200, {
        fit: 'cover',
        position: 'center',
      });
    });

    it('should generate thumbnail with custom size', async () => {
      const mockSharp = {
        resize: jest.fn().mockReturnThis(),
        jpeg: jest.fn().mockReturnThis(),
        toBuffer: jest.fn().mockResolvedValue(mockImageBuffer),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      await service.generateThumbnail(
        mockImageBuffer,
        'photos/thumb_photo.jpg',
        150,
        150,
      );

      expect(mockSharp.resize).toHaveBeenCalledWith(150, 150, expect.any(Object));
    });

    it('should throw error on thumbnail generation failure', async () => {
      jest
        .spyOn(service as any, 'getSharp')
        .mockRejectedValue(new Error('Sharp error'));

      await expect(
        service.generateThumbnail(mockImageBuffer, 'photos/thumb.jpg'),
      ).rejects.toThrow('Thumbnail generation failed');
    });
  });

  describe('getPresignedUrl', () => {
    it('should generate presigned URL with default expiration', async () => {
      const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
      getSignedUrl.mockResolvedValue('https://signed-url.com/file');

      const result = await service.getPresignedUrl('test/file.txt');

      expect(result).toBe('https://signed-url.com/file');
      expect(getSignedUrl).toHaveBeenCalledWith(
        expect.any(Object),
        expect.any(GetObjectCommand),
        { expiresIn: 3600 },
      );
    });

    it('should generate presigned URL with custom expiration', async () => {
      const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
      getSignedUrl.mockResolvedValue('https://signed-url.com/file');

      await service.getPresignedUrl('test/file.txt', 7200);

      expect(getSignedUrl).toHaveBeenCalledWith(
        expect.any(Object),
        expect.any(GetObjectCommand),
        { expiresIn: 7200 },
      );
    });

    it('should throw error on presigned URL generation failure', async () => {
      const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
      getSignedUrl.mockRejectedValue(new Error('Presign failed'));

      await expect(service.getPresignedUrl('test/file.txt')).rejects.toThrow(
        'Presigned URL generation failed',
      );
    });
  });

  describe('getFile', () => {
    it('should retrieve file as buffer', async () => {
      const mockStream = new Readable();
      mockStream.push('file content');
      mockStream.push(null);

      mockS3Client.send = jest.fn().mockResolvedValue({
        Body: mockStream,
      });

      const result = await service.getFile('test/file.txt');

      expect(result).toBeInstanceOf(Buffer);
      expect(result.toString()).toBe('file content');
    });

    it('should throw error on file retrieval failure', async () => {
      mockS3Client.send = jest.fn().mockRejectedValue(new Error('Not found'));

      await expect(service.getFile('test/missing.txt')).rejects.toThrow(
        'File retrieval failed',
      );
    });
  });

  describe('deleteFile', () => {
    it('should delete file successfully', async () => {
      mockS3Client.send = jest.fn().mockResolvedValue({});

      await service.deleteFile('test/file.txt');

      expect(mockS3Client.send).toHaveBeenCalledWith(
        expect.any(DeleteObjectCommand),
      );
    });

    it('should throw error on deletion failure', async () => {
      mockS3Client.send = jest
        .fn()
        .mockRejectedValue(new Error('Delete failed'));

      await expect(service.deleteFile('test/file.txt')).rejects.toThrow(
        'File deletion failed',
      );
    });
  });

  describe('fileExists', () => {
    it('should return true if file exists', async () => {
      mockS3Client.send = jest.fn().mockResolvedValue({});

      const result = await service.fileExists('test/file.txt');

      expect(result).toBe(true);
    });

    it('should return false if file does not exist', async () => {
      mockS3Client.send = jest.fn().mockRejectedValue(new Error('Not found'));

      const result = await service.fileExists('test/missing.txt');

      expect(result).toBe(false);
    });
  });

  describe('getImageMetadata', () => {
    it('should return image metadata', async () => {
      const mockStream = new Readable();
      mockStream.push(mockImageBuffer);
      mockStream.push(null);

      mockS3Client.send = jest.fn().mockResolvedValue({
        Body: mockStream,
      });

      const mockSharp = {
        metadata: jest.fn().mockResolvedValue({
          width: 1920,
          height: 1080,
          format: 'jpeg',
        }),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const result = await service.getImageMetadata('photos/photo.jpg');

      expect(result).toEqual({
        width: 1920,
        height: 1080,
        format: 'jpeg',
        size: mockImageBuffer.length,
      });
    });

    it('should return null on error', async () => {
      mockS3Client.send = jest.fn().mockRejectedValue(new Error('Not found'));

      const result = await service.getImageMetadata('photos/missing.jpg');

      expect(result).toBeNull();
    });

    it('should return null if sharp fails', async () => {
      const mockStream = new Readable();
      mockStream.push(mockImageBuffer);
      mockStream.push(null);

      mockS3Client.send = jest.fn().mockResolvedValue({
        Body: mockStream,
      });

      jest
        .spyOn(service as any, 'getSharp')
        .mockRejectedValue(new Error('Sharp error'));

      const result = await service.getImageMetadata('photos/photo.jpg');

      expect(result).toBeNull();
    });
  });

  describe('integration scenarios', () => {
    it('should upload, retrieve, and delete file', async () => {
      // Upload
      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      const uploadResult = await service.uploadFile(
        mockBuffer,
        'test/integration.txt',
        'text/plain',
      );
      expect(uploadResult.key).toBe('test/integration.txt');

      // Check exists
      mockS3Client.send = jest.fn().mockResolvedValue({});
      const exists = await service.fileExists('test/integration.txt');
      expect(exists).toBe(true);

      // Delete
      await service.deleteFile('test/integration.txt');
      expect(mockS3Client.send).toHaveBeenCalledWith(
        expect.any(DeleteObjectCommand),
      );
    });

    it('should handle image upload with thumbnail generation', async () => {
      const mockSharp = {
        metadata: jest.fn().mockResolvedValue({
          width: 2000,
          height: 1500,
          format: 'jpeg',
        }),
        resize: jest.fn().mockReturnThis(),
        jpeg: jest.fn().mockReturnThis(),
        toBuffer: jest.fn().mockResolvedValue(mockImageBuffer),
      };
      jest.spyOn(service as any, 'getSharp').mockResolvedValue(mockSharp);

      const mockUpload = {
        done: jest.fn().mockResolvedValue({}),
      };
      (Upload as unknown as jest.Mock).mockImplementation(() => mockUpload);

      // Upload main image
      const mainResult = await service.uploadImage(
        mockImageBuffer,
        'photos/main.jpg',
      );
      expect(mainResult.key).toBe('photos/main.jpg');

      // Generate thumbnail
      const thumbResult = await service.generateThumbnail(
        mockImageBuffer,
        'photos/thumb_main.jpg',
      );
      expect(thumbResult.key).toBe('photos/thumb_main.jpg');
    });
  });
});
