/**
 * Upload Service Tests
 *
 * Tests for the S3 upload service including presigned URLs,
 * direct uploads, and signed URL generation for private content.
 */

import api from '../../services/api';
import {
  getPublicImageUrl,
  getPresignedUrl,
  getPresignedUrls,
  uploadToS3,
  confirmUploads,
  uploadFile,
  uploadFiles,
  getSignedUrl,
  PresignedUrlResponse,
} from '../../services/uploadService';

// Mock the api module
jest.mock('../../services/api');

const mockedApi = api as jest.Mocked<typeof api>;

// Mock XMLHttpRequest for uploadToS3 tests
class MockXMLHttpRequest {
  static instances: MockXMLHttpRequest[] = [];

  status = 200;
  readyState = 4;
  responseText = '';
  upload = {
    onprogress: null as ((e: { lengthComputable: boolean; loaded: number; total: number }) => void) | null,
  };
  onload: (() => void) | null = null;
  onerror: (() => void) | null = null;

  private headers: Record<string, string> = {};
  private method = '';
  private url = '';

  constructor() {
    MockXMLHttpRequest.instances.push(this);
  }

  open(method: string, url: string) {
    this.method = method;
    this.url = url;
  }

  setRequestHeader(key: string, value: string) {
    this.headers[key] = value;
  }

  send(_data: unknown) {
    // Use Promise.resolve().then for async behavior in tests
    // This allows promises to resolve without real delays
    Promise.resolve().then(() => {
      if (this.upload.onprogress) {
        this.upload.onprogress({ lengthComputable: true, loaded: 50, total: 100 });
        this.upload.onprogress({ lengthComputable: true, loaded: 100, total: 100 });
      }
      if (this.onload) {
        this.onload();
      }
    });
  }

  getHeaders() {
    return this.headers;
  }

  getMethod() {
    return this.method;
  }

  getUrl() {
    return this.url;
  }

  static reset() {
    MockXMLHttpRequest.instances = [];
  }

  static getLastInstance() {
    return MockXMLHttpRequest.instances[MockXMLHttpRequest.instances.length - 1];
  }
}

// Store original XMLHttpRequest
const originalXMLHttpRequest = global.XMLHttpRequest;

describe('Upload Service', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    MockXMLHttpRequest.reset();
    // Reset environment variables
    process.env.REACT_APP_S3_BUCKET = 'test-bucket';
    process.env.REACT_APP_AWS_REGION = 'us-east-1';
    // Mock XMLHttpRequest globally
    (global as unknown as { XMLHttpRequest: typeof MockXMLHttpRequest }).XMLHttpRequest = MockXMLHttpRequest;
  });

  afterEach(() => {
    // Restore original XMLHttpRequest
    (global as unknown as { XMLHttpRequest: typeof XMLHttpRequest }).XMLHttpRequest = originalXMLHttpRequest;
  });

  describe('getPublicImageUrl', () => {
    it('should return empty string for null input', () => {
      expect(getPublicImageUrl(null)).toBe('');
    });

    it('should return empty string for undefined input', () => {
      expect(getPublicImageUrl(undefined)).toBe('');
    });

    it('should return empty string for empty string input', () => {
      expect(getPublicImageUrl('')).toBe('');
    });

    it('should return full S3 URL unchanged', () => {
      const fullUrl = 'https://bucket.s3.us-east-1.amazonaws.com/items/uuid.jpg';
      expect(getPublicImageUrl(fullUrl)).toBe(fullUrl);
    });

    it('should construct S3 URL from key', () => {
      const key = 'items/550e8400-e29b-41d4-a716-446655440000.jpg';
      const expectedUrl = 'https://test-bucket.s3.us-east-1.amazonaws.com/items/550e8400-e29b-41d4-a716-446655440000.jpg';
      expect(getPublicImageUrl(key)).toBe(expectedUrl);
    });

    it('should handle profiles folder', () => {
      const key = 'profiles/550e8400-e29b-41d4-a716-446655440000.jpg';
      expect(getPublicImageUrl(key)).toContain('profiles/');
    });

    it('should handle forum folder', () => {
      const key = 'forum/550e8400-e29b-41d4-a716-446655440000.jpg';
      expect(getPublicImageUrl(key)).toContain('forum/');
    });

    it('should use default region when not set', () => {
      delete process.env.REACT_APP_AWS_REGION;
      const key = 'items/uuid.jpg';
      expect(getPublicImageUrl(key)).toContain('us-east-1');
    });
  });

  describe('getPresignedUrl', () => {
    const mockFile = new File(['test content'], 'photo.jpg', { type: 'image/jpeg' });
    const mockResponse: PresignedUrlResponse = {
      uploadUrl: 'https://presigned-url.s3.amazonaws.com/items/uuid.jpg?signature=abc',
      key: 'items/550e8400-e29b-41d4-a716-446655440000.jpg',
      publicUrl: 'https://bucket.s3.amazonaws.com/items/550e8400-e29b-41d4-a716-446655440000.jpg',
      expiresAt: new Date().toISOString(),
    };

    it('should request presigned URL with correct parameters', async () => {
      mockedApi.post.mockResolvedValue({ data: mockResponse });

      const result = await getPresignedUrl('item', mockFile);

      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', {
        uploadType: 'item',
        contentType: 'image/jpeg',
        fileName: 'photo.jpg',
        fileSize: mockFile.size,
      });
      expect(result).toEqual(mockResponse);
    });

    it('should handle different upload types', async () => {
      mockedApi.post.mockResolvedValue({ data: mockResponse });

      await getPresignedUrl('profile', mockFile);
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', expect.objectContaining({
        uploadType: 'profile',
      }));

      await getPresignedUrl('message', mockFile);
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', expect.objectContaining({
        uploadType: 'message',
      }));

      await getPresignedUrl('forum', mockFile);
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', expect.objectContaining({
        uploadType: 'forum',
      }));

      await getPresignedUrl('condition-check', mockFile);
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', expect.objectContaining({
        uploadType: 'condition-check',
      }));
    });

    it('should propagate API errors', async () => {
      const error = new Error('API error');
      mockedApi.post.mockRejectedValue(error);

      await expect(getPresignedUrl('item', mockFile)).rejects.toThrow('API error');
    });
  });

  describe('getPresignedUrls', () => {
    const mockFiles = [
      new File(['test1'], 'photo1.jpg', { type: 'image/jpeg' }),
      new File(['test2'], 'photo2.png', { type: 'image/png' }),
    ];

    const mockResponses: PresignedUrlResponse[] = [
      {
        uploadUrl: 'https://presigned-url1.s3.amazonaws.com',
        key: 'items/uuid1.jpg',
        publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
        expiresAt: new Date().toISOString(),
      },
      {
        uploadUrl: 'https://presigned-url2.s3.amazonaws.com',
        key: 'items/uuid2.png',
        publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
        expiresAt: new Date().toISOString(),
      },
    ];

    it('should request batch presigned URLs', async () => {
      mockedApi.post.mockResolvedValue({ data: { uploads: mockResponses } });

      const result = await getPresignedUrls('item', mockFiles);

      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', {
        uploadType: 'item',
        files: [
          { contentType: 'image/jpeg', fileName: 'photo1.jpg', fileSize: mockFiles[0].size },
          { contentType: 'image/png', fileName: 'photo2.png', fileSize: mockFiles[1].size },
        ],
      });
      expect(result).toEqual(mockResponses);
    });

    it('should handle empty file array', async () => {
      mockedApi.post.mockResolvedValue({ data: { uploads: [] } });

      const result = await getPresignedUrls('item', []);

      expect(result).toEqual([]);
    });
  });

  describe('uploadToS3', () => {
    const mockFile = new File(['test content'], 'photo.jpg', { type: 'image/jpeg' });
    const mockUploadUrl = 'https://presigned-url.s3.amazonaws.com/items/uuid.jpg?signature=abc';

    it('should upload file successfully', async () => {
      await uploadToS3(mockFile, mockUploadUrl);

      const instance = MockXMLHttpRequest.getLastInstance();
      expect(instance.getMethod()).toBe('PUT');
      expect(instance.getUrl()).toBe(mockUploadUrl);
      expect(instance.getHeaders()['Content-Type']).toBe('image/jpeg');
    });

    it('should call onProgress callback during upload', async () => {
      const onProgress = jest.fn();

      await uploadToS3(mockFile, mockUploadUrl, { onProgress });

      // Progress should be called at least once
      expect(onProgress).toHaveBeenCalled();
      // Should receive percentage values
      expect(onProgress).toHaveBeenCalledWith(expect.any(Number));
    });

    it('should export uploadToS3 function with correct signature', () => {
      expect(typeof uploadToS3).toBe('function');
      // Function accepts file, url, and optional options
      expect(uploadToS3.length).toBeGreaterThanOrEqual(2);
    });

    it('should set correct content-type header', async () => {
      const pngFile = new File(['test'], 'image.png', { type: 'image/png' });
      await uploadToS3(pngFile, mockUploadUrl);

      const instance = MockXMLHttpRequest.getLastInstance();
      expect(instance.getHeaders()['Content-Type']).toBe('image/png');
    });
  });

  describe('confirmUploads', () => {
    it('should confirm uploaded keys', async () => {
      const keys = ['items/uuid1.jpg', 'items/uuid2.jpg'];
      const mockResponse = { confirmed: keys, total: 2 };

      mockedApi.post.mockResolvedValue({ data: mockResponse });

      const result = await confirmUploads(keys);

      expect(mockedApi.post).toHaveBeenCalledWith('/upload/confirm', { keys });
      expect(result).toEqual(mockResponse);
    });

    it('should handle partial confirmation', async () => {
      const keys = ['items/uuid1.jpg', 'items/uuid2.jpg'];
      const mockResponse = { confirmed: ['items/uuid1.jpg'], total: 2 };

      mockedApi.post.mockResolvedValue({ data: mockResponse });

      const result = await confirmUploads(keys);

      expect(result.confirmed).toHaveLength(1);
      expect(result.total).toBe(2);
    });
  });

  describe('uploadFile', () => {
    const mockFile = new File(['test content'], 'photo.jpg', { type: 'image/jpeg' });
    const presignResponse: PresignedUrlResponse = {
      uploadUrl: 'https://presigned.s3.amazonaws.com/items/uuid.jpg',
      key: 'items/uuid.jpg',
      publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid.jpg',
      expiresAt: new Date().toISOString(),
    };

    it('should complete full upload flow successfully', async () => {
      // Mock presign response
      mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
      // Mock confirm response
      mockedApi.post.mockResolvedValueOnce({
        data: { confirmed: [presignResponse.key], total: 1 },
      });

      const result = await uploadFile('item', mockFile);

      expect(result).toEqual({
        key: presignResponse.key,
        publicUrl: presignResponse.publicUrl,
      });

      // Verify presign was called
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', {
        uploadType: 'item',
        contentType: 'image/jpeg',
        fileName: 'photo.jpg',
        fileSize: mockFile.size,
      });

      // Verify confirm was called
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/confirm', {
        keys: [presignResponse.key],
      });
    });

    it('should throw error when upload verification fails', async () => {
      mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
      // Mock confirm returning empty confirmed array
      mockedApi.post.mockResolvedValueOnce({
        data: { confirmed: [], total: 1 },
      });

      await expect(uploadFile('item', mockFile)).rejects.toThrow('Upload verification failed');
    });

    it('should pass onProgress to uploadToS3', async () => {
      const onProgress = jest.fn();

      mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
      mockedApi.post.mockResolvedValueOnce({
        data: { confirmed: [presignResponse.key], total: 1 },
      });

      await uploadFile('item', mockFile, { onProgress });

      // onProgress should have been called during XHR upload
      expect(onProgress).toHaveBeenCalled();
    });

    it('should work with different upload types', async () => {
      const messagePresignResponse = {
        ...presignResponse,
        key: 'messages/uuid.jpg',
        publicUrl: null, // Messages are private
      };

      mockedApi.post.mockResolvedValueOnce({ data: messagePresignResponse });
      mockedApi.post.mockResolvedValueOnce({
        data: { confirmed: [messagePresignResponse.key], total: 1 },
      });

      const result = await uploadFile('message', mockFile);

      expect(result.key).toBe('messages/uuid.jpg');
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', expect.objectContaining({
        uploadType: 'message',
      }));
    });
  });

  describe('uploadFiles', () => {
    const mockFiles = [
      new File(['test1'], 'photo1.jpg', { type: 'image/jpeg' }),
      new File(['test2'], 'photo2.png', { type: 'image/png' }),
    ];

    const presignResponses: PresignedUrlResponse[] = [
      {
        uploadUrl: 'https://presigned1.s3.amazonaws.com/items/uuid1.jpg',
        key: 'items/uuid1.jpg',
        publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
        expiresAt: new Date().toISOString(),
      },
      {
        uploadUrl: 'https://presigned2.s3.amazonaws.com/items/uuid2.png',
        key: 'items/uuid2.png',
        publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
        expiresAt: new Date().toISOString(),
      },
    ];

    it('should return empty array for empty files array', async () => {
      const result = await uploadFiles('item', []);
      expect(result).toEqual([]);
      expect(mockedApi.post).not.toHaveBeenCalled();
    });

    it('should complete full batch upload flow successfully', async () => {
      mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
      mockedApi.post.mockResolvedValueOnce({
        data: {
          confirmed: presignResponses.map((p) => p.key),
          total: 2,
        },
      });

      const result = await uploadFiles('item', mockFiles);

      expect(result).toHaveLength(2);
      expect(result[0]).toEqual({
        key: 'items/uuid1.jpg',
        publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
      });
      expect(result[1]).toEqual({
        key: 'items/uuid2.png',
        publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
      });

      // Verify batch presign was called
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', {
        uploadType: 'item',
        files: [
          { contentType: 'image/jpeg', fileName: 'photo1.jpg', fileSize: mockFiles[0].size },
          { contentType: 'image/png', fileName: 'photo2.png', fileSize: mockFiles[1].size },
        ],
      });

      // Verify confirm was called with all keys
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/confirm', {
        keys: ['items/uuid1.jpg', 'items/uuid2.png'],
      });
    });

    it('should filter out unconfirmed uploads', async () => {
      mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
      // Only first file confirmed
      mockedApi.post.mockResolvedValueOnce({
        data: {
          confirmed: ['items/uuid1.jpg'],
          total: 2,
        },
      });

      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();

      const result = await uploadFiles('item', mockFiles);

      // Only confirmed uploads should be returned
      expect(result).toHaveLength(1);
      expect(result[0].key).toBe('items/uuid1.jpg');

      // Should log warning about failed verification
      expect(consoleSpy).toHaveBeenCalledWith('1 uploads failed verification');

      consoleSpy.mockRestore();
    });

    it('should handle all uploads failing verification', async () => {
      mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
      mockedApi.post.mockResolvedValueOnce({
        data: {
          confirmed: [],
          total: 2,
        },
      });

      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();

      const result = await uploadFiles('item', mockFiles);

      expect(result).toHaveLength(0);
      expect(consoleSpy).toHaveBeenCalledWith('2 uploads failed verification');

      consoleSpy.mockRestore();
    });

    it('should upload all files in parallel', async () => {
      mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
      mockedApi.post.mockResolvedValueOnce({
        data: {
          confirmed: presignResponses.map((p) => p.key),
          total: 2,
        },
      });

      await uploadFiles('item', mockFiles);

      // Should have created 2 XHR instances for parallel uploads
      expect(MockXMLHttpRequest.instances.length).toBe(2);
    });

    it('should work with different upload types', async () => {
      const forumResponses = presignResponses.map((r) => ({
        ...r,
        key: r.key.replace('items/', 'forum/'),
        publicUrl: r.publicUrl.replace('items/', 'forum/'),
      }));

      mockedApi.post.mockResolvedValueOnce({ data: { uploads: forumResponses } });
      mockedApi.post.mockResolvedValueOnce({
        data: {
          confirmed: forumResponses.map((p) => p.key),
          total: 2,
        },
      });

      const result = await uploadFiles('forum', mockFiles);

      expect(result[0].key).toBe('forum/uuid1.jpg');
      expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', expect.objectContaining({
        uploadType: 'forum',
      }));
    });
  });

  describe('getSignedUrl', () => {
    it('should request signed URL for private content', async () => {
      const key = 'messages/uuid.jpg';
      const signedUrl = 'https://bucket.s3.amazonaws.com/messages/uuid.jpg?signature=abc';

      mockedApi.get.mockResolvedValue({ data: { url: signedUrl } });

      const result = await getSignedUrl(key);

      expect(mockedApi.get).toHaveBeenCalledWith(`/upload/signed-url/${encodeURIComponent(key)}`);
      expect(result).toBe(signedUrl);
    });

    it('should encode key in URL', async () => {
      const key = 'condition-checks/uuid with spaces.jpg';
      const signedUrl = 'https://bucket.s3.amazonaws.com/signed';

      mockedApi.get.mockResolvedValue({ data: { url: signedUrl } });

      await getSignedUrl(key);

      expect(mockedApi.get).toHaveBeenCalledWith(
        `/upload/signed-url/${encodeURIComponent(key)}`
      );
    });

    it('should propagate API errors', async () => {
      const error = new Error('Unauthorized');
      mockedApi.get.mockRejectedValue(error);

      await expect(getSignedUrl('messages/uuid.jpg')).rejects.toThrow('Unauthorized');
    });
  });
});
