/**
 * S3Service Unit Tests
 *
 * Tests the S3 service methods including presigned URL generation,
 * upload verification, and file extension mapping.
 */

// Store mock implementations for tests to control
const mockGetSignedUrl = jest.fn();
const mockSend = jest.fn();

// Mock AWS SDK before anything else
jest.mock('@aws-sdk/client-s3', () => ({
  S3Client: jest.fn().mockImplementation(() => ({
    send: mockSend
  })),
  PutObjectCommand: jest.fn().mockImplementation((params) => params),
  GetObjectCommand: jest.fn().mockImplementation((params) => params),
  HeadObjectCommand: jest.fn().mockImplementation((params) => params)
}));

jest.mock('@aws-sdk/s3-request-presigner', () => ({
  getSignedUrl: (...args) => mockGetSignedUrl(...args)
}));

jest.mock('../../../config/aws', () => ({
  getAWSConfig: jest.fn(() => ({ region: 'us-east-1' }))
}));

jest.mock('uuid', () => ({
  v4: jest.fn(() => '550e8400-e29b-41d4-a716-446655440000')
}));

jest.mock('../../../utils/logger', () => ({
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn()
}));

describe('S3Service', () => {
  let s3Service;

  beforeEach(() => {
    // Clear all mocks
    jest.clearAllMocks();

    // Reset module cache to get fresh instance
    jest.resetModules();

    // Set up environment
    process.env.S3_ENABLED = 'true';
    process.env.S3_BUCKET = 'test-bucket';

    // Default mock implementations
    mockGetSignedUrl.mockResolvedValue('https://presigned-url.example.com');
    mockSend.mockResolvedValue({});

    // Load fresh module
    s3Service = require('../../../services/s3Service');
    s3Service.initialize();
  });

  afterEach(() => {
    delete process.env.S3_ENABLED;
    delete process.env.S3_BUCKET;
  });

  describe('initialize', () => {
    beforeEach(() => {
      jest.resetModules();
    });

    it('should disable S3 when S3_ENABLED is not true', () => {
      process.env.S3_ENABLED = 'false';
      const freshService = require('../../../services/s3Service');
      freshService.initialize();

      expect(freshService.isEnabled()).toBe(false);
    });

    it('should initialize successfully with valid config', () => {
      process.env.S3_ENABLED = 'true';
      process.env.S3_BUCKET = 'test-bucket';

      jest.resetModules();
      const freshService = require('../../../services/s3Service');
      freshService.initialize();

      expect(freshService.isEnabled()).toBe(true);
    });
  });

  describe('isEnabled', () => {
    it('should return true when S3 is enabled', () => {
      expect(s3Service.isEnabled()).toBe(true);
    });

    it('should return false when S3 is disabled', () => {
      jest.resetModules();
      process.env.S3_ENABLED = 'false';
      const freshService = require('../../../services/s3Service');
      freshService.initialize();

      expect(freshService.isEnabled()).toBe(false);
    });
  });

  describe('getPresignedUploadUrl', () => {
    it('should generate presigned URL for valid profile upload', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'profile',
        'image/jpeg',
        'photo.jpg',
        1024 * 1024 // 1MB
      );

      expect(result.uploadUrl).toBe('https://presigned-url.example.com');
      expect(result.key).toBe('profiles/550e8400-e29b-41d4-a716-446655440000.jpg');
      expect(result.publicUrl).toBe('https://test-bucket.s3.us-east-1.amazonaws.com/profiles/550e8400-e29b-41d4-a716-446655440000.jpg');
      expect(result.expiresAt).toBeInstanceOf(Date);
    });

    it('should generate presigned URL for item upload', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'item',
        'image/png',
        'item-photo.png',
        5 * 1024 * 1024 // 5MB
      );

      expect(result.key).toBe('items/550e8400-e29b-41d4-a716-446655440000.png');
      expect(result.publicUrl).toContain('items/');
    });

    it('should generate presigned URL for message (private) upload with null publicUrl', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'message',
        'image/jpeg',
        'message.jpg',
        1024 * 1024
      );

      expect(result.key).toBe('messages/550e8400-e29b-41d4-a716-446655440000.jpg');
      expect(result.publicUrl).toBeNull(); // Private uploads don't get public URLs
    });

    it('should generate presigned URL for condition-check (private) upload', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'condition-check',
        'image/jpeg',
        'check.jpg',
        2 * 1024 * 1024
      );

      expect(result.key).toBe('condition-checks/550e8400-e29b-41d4-a716-446655440000.jpg');
      expect(result.publicUrl).toBeNull();
    });

    it('should generate presigned URL for forum upload', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'forum',
        'image/gif',
        'post.gif',
        3 * 1024 * 1024
      );

      expect(result.key).toBe('forum/550e8400-e29b-41d4-a716-446655440000.gif');
      expect(result.publicUrl).toContain('forum/');
    });

    it('should throw error for invalid upload type', async () => {
      await expect(
        s3Service.getPresignedUploadUrl('invalid', 'image/jpeg', 'photo.jpg', 1024)
      ).rejects.toThrow('Invalid upload type: invalid');
    });

    it('should throw error for invalid content type', async () => {
      await expect(
        s3Service.getPresignedUploadUrl('profile', 'application/pdf', 'doc.pdf', 1024)
      ).rejects.toThrow('Invalid content type: application/pdf');
    });

    it('should accept all valid MIME types', async () => {
      const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];

      for (const contentType of validTypes) {
        const result = await s3Service.getPresignedUploadUrl('profile', contentType, 'photo.jpg', 1024);
        expect(result.uploadUrl).toBeDefined();
      }
    });

    it('should throw error for missing file size', async () => {
      await expect(
        s3Service.getPresignedUploadUrl('profile', 'image/jpeg', 'photo.jpg', 0)
      ).rejects.toThrow('File size is required');

      await expect(
        s3Service.getPresignedUploadUrl('profile', 'image/jpeg', 'photo.jpg', null)
      ).rejects.toThrow('File size is required');
    });

    it('should throw error when file exceeds profile max size (5MB)', async () => {
      await expect(
        s3Service.getPresignedUploadUrl('profile', 'image/jpeg', 'photo.jpg', 6 * 1024 * 1024)
      ).rejects.toThrow('File too large. Maximum size is 5MB');
    });

    it('should throw error when file exceeds item max size (10MB)', async () => {
      await expect(
        s3Service.getPresignedUploadUrl('item', 'image/jpeg', 'photo.jpg', 11 * 1024 * 1024)
      ).rejects.toThrow('File too large. Maximum size is 10MB');
    });

    it('should throw error when file exceeds message max size (5MB)', async () => {
      await expect(
        s3Service.getPresignedUploadUrl('message', 'image/jpeg', 'photo.jpg', 6 * 1024 * 1024)
      ).rejects.toThrow('File too large. Maximum size is 5MB');
    });

    it('should accept files at exactly max size', async () => {
      const result = await s3Service.getPresignedUploadUrl('profile', 'image/jpeg', 'photo.jpg', 5 * 1024 * 1024);
      expect(result.uploadUrl).toBeDefined();
    });

    it('should throw error when S3 is disabled', async () => {
      jest.resetModules();
      process.env.S3_ENABLED = 'false';
      const disabledService = require('../../../services/s3Service');
      disabledService.initialize();

      await expect(
        disabledService.getPresignedUploadUrl('profile', 'image/jpeg', 'photo.jpg', 1024)
      ).rejects.toThrow('S3 storage is not enabled');
    });

    it('should use extension from filename when provided', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'profile',
        'image/jpeg',
        'photo.png',
        1024
      );

      expect(result.key).toContain('.png');
    });

    it('should fall back to MIME type extension when filename has none', async () => {
      const result = await s3Service.getPresignedUploadUrl(
        'profile',
        'image/png',
        'photo',
        1024
      );

      expect(result.key).toContain('.png');
    });
  });

  describe('getPresignedDownloadUrl', () => {
    it('should generate download URL with default expiration', async () => {
      const result = await s3Service.getPresignedDownloadUrl('messages/test.jpg');

      expect(result).toBe('https://presigned-url.example.com');
      expect(mockGetSignedUrl).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ Bucket: 'test-bucket', Key: 'messages/test.jpg' }),
        { expiresIn: 3600 }
      );
    });

    it('should generate download URL with custom expiration', async () => {
      await s3Service.getPresignedDownloadUrl('messages/test.jpg', 7200);

      expect(mockGetSignedUrl).toHaveBeenCalledWith(
        expect.anything(),
        expect.anything(),
        { expiresIn: 7200 }
      );
    });

    it('should throw error when S3 is disabled', async () => {
      jest.resetModules();
      process.env.S3_ENABLED = 'false';
      const disabledService = require('../../../services/s3Service');
      disabledService.initialize();

      await expect(
        disabledService.getPresignedDownloadUrl('messages/test.jpg')
      ).rejects.toThrow('S3 storage is not enabled');
    });
  });

  describe('getPublicUrl', () => {
    it('should return correct public URL format', () => {
      const url = s3Service.getPublicUrl('items/test-uuid.jpg');

      expect(url).toBe('https://test-bucket.s3.us-east-1.amazonaws.com/items/test-uuid.jpg');
    });

    it('should return null when S3 is disabled', () => {
      jest.resetModules();
      process.env.S3_ENABLED = 'false';
      const disabledService = require('../../../services/s3Service');
      disabledService.initialize();

      expect(disabledService.getPublicUrl('items/test.jpg')).toBeNull();
    });
  });

  describe('verifyUpload', () => {
    it('should return true when file exists', async () => {
      mockSend.mockResolvedValue({});

      const result = await s3Service.verifyUpload('items/test.jpg');

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

    it('should return false when file does not exist (NotFound)', async () => {
      mockSend.mockRejectedValue({ name: 'NotFound' });

      const result = await s3Service.verifyUpload('items/nonexistent.jpg');

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

    it('should return false when file does not exist (404 status)', async () => {
      mockSend.mockRejectedValue({ $metadata: { httpStatusCode: 404 } });

      const result = await s3Service.verifyUpload('items/nonexistent.jpg');

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

    it('should throw error for other S3 errors', async () => {
      const s3Error = new Error('Access Denied');
      s3Error.name = 'AccessDenied';
      mockSend.mockRejectedValue(s3Error);

      await expect(s3Service.verifyUpload('items/test.jpg')).rejects.toThrow('Access Denied');
    });

    it('should return false when S3 is disabled', async () => {
      jest.resetModules();
      process.env.S3_ENABLED = 'false';
      const disabledService = require('../../../services/s3Service');
      disabledService.initialize();

      const result = await disabledService.verifyUpload('items/test.jpg');

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

  describe('getExtFromMime', () => {
    it('should return correct extension for image/jpeg', () => {
      expect(s3Service.getExtFromMime('image/jpeg')).toBe('.jpg');
    });

    it('should return correct extension for image/jpg', () => {
      expect(s3Service.getExtFromMime('image/jpg')).toBe('.jpg');
    });

    it('should return correct extension for image/png', () => {
      expect(s3Service.getExtFromMime('image/png')).toBe('.png');
    });

    it('should return correct extension for image/gif', () => {
      expect(s3Service.getExtFromMime('image/gif')).toBe('.gif');
    });

    it('should return correct extension for image/webp', () => {
      expect(s3Service.getExtFromMime('image/webp')).toBe('.webp');
    });

    it('should return .jpg as default for unknown MIME types', () => {
      expect(s3Service.getExtFromMime('image/unknown')).toBe('.jpg');
    });
  });
});
