/**
 * E2E Tests: Video Upload Service (Multipart Upload)
 *
 * Epic: OQI-002 - Módulo Educativo
 * Service: video-upload.service.ts
 *
 * Tests validate:
 * - File chunking (5MB parts)
 * - Concurrent uploads (max 3 simultaneous)
 * - Progress tracking
 * - ETag extraction from response headers
 * - Error handling and retry logic
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { videoUploadService, VideoUploadService } from '../../services/video-upload.service';
import { apiClient } from '../../lib/apiClient';

// Mock API client
vi.mock('../../lib/apiClient', () => ({
  apiClient: {
    post: vi.fn(),
    get: vi.fn(),
    delete: vi.fn(),
  },
}));

// Mock fetch for presigned URL uploads
global.fetch = vi.fn() as any;

describe('E2E: Video Upload Service', () => {
  let service: VideoUploadService;

  beforeEach(() => {
    service = videoUploadService;
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // ==========================================================================
  // File Chunking (5MB Parts)
  // ==========================================================================

  describe('File Chunking', () => {
    it('should split file into 5MB parts', async () => {
      // Create 15MB file (should be split into 3 parts)
      const fileSize = 15 * 1024 * 1024;
      const file = new File([new ArrayBuffer(fileSize)], 'video.mp4', { type: 'video/mp4' });

      // Mock init response with 3 presigned URLs
      const presignedUrls = [
        'https://s3.example.com/part1',
        'https://s3.example.com/part2',
        'https://s3.example.com/part3',
      ];

      // Mock fetch for each part
      (global.fetch as vi.Mock).mockImplementation(async (url) => ({
        ok: true,
        headers: new Map([['ETag', '"etag-123"']]),
        statusText: 'OK',
      }));

      const parts = await service.uploadFile(file, presignedUrls);

      // Should have uploaded 3 parts
      expect(parts).toHaveLength(3);
      expect(global.fetch).toHaveBeenCalledTimes(3);

      // Verify each part was uploaded with correct chunk
      const fetchCalls = (global.fetch as vi.Mock).mock.calls;
      fetchCalls.forEach((call, index) => {
        const [url, options] = call;
        expect(url).toBe(presignedUrls[index]);
        expect(options.method).toBe('PUT');
        expect(options.body).toBeInstanceOf(Blob);

        // Verify chunk size (5MB for first 2, remainder for last)
        const expectedSize = index < 2 ? 5 * 1024 * 1024 : fileSize - (2 * 5 * 1024 * 1024);
        expect((options.body as Blob).size).toBe(expectedSize);
      });
    });

    it('should handle file smaller than 5MB (single part)', async () => {
      const file = new File([new ArrayBuffer(2 * 1024 * 1024)], 'small.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"etag-456"']]),
      });

      const parts = await service.uploadFile(file, presignedUrls);

      expect(parts).toHaveLength(1);
      expect(parts[0].partNumber).toBe(1);
      expect(parts[0].etag).toBe('etag-456');
    });

    it('should handle large file (100MB = 20 parts)', async () => {
      const fileSize = 100 * 1024 * 1024;
      const file = new File([new ArrayBuffer(fileSize)], 'large.mp4', {
        type: 'video/mp4',
      });

      const numParts = Math.ceil(fileSize / (5 * 1024 * 1024)); // 20 parts
      const presignedUrls = Array.from({ length: numParts }, (_, i) => `https://s3.example.com/part${i + 1}`);

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"etag-789"']]),
      });

      const parts = await service.uploadFile(file, presignedUrls);

      expect(parts).toHaveLength(numParts);
      expect(global.fetch).toHaveBeenCalledTimes(numParts);
    });
  });

  // ==========================================================================
  // Concurrent Uploads (Max 3)
  // ==========================================================================

  describe('Concurrent Uploads', () => {
    it('should upload max 3 parts concurrently', async () => {
      // Create 25MB file (5 parts)
      const file = new File([new ArrayBuffer(25 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = Array.from({ length: 5 }, (_, i) => `https://s3.example.com/part${i + 1}`);

      let concurrentCalls = 0;
      let maxConcurrent = 0;

      (global.fetch as vi.Mock).mockImplementation(async () => {
        concurrentCalls++;
        maxConcurrent = Math.max(maxConcurrent, concurrentCalls);

        // Simulate network delay
        await new Promise(resolve => setTimeout(resolve, 100));

        concurrentCalls--;

        return {
          ok: true,
          headers: new Map([['ETag', '"etag"']]),
        };
      });

      await service.uploadFile(file, presignedUrls);

      // Should not exceed 3 concurrent uploads
      expect(maxConcurrent).toBeLessThanOrEqual(3);
    });

    it('should process parts in batches of 3', async () => {
      const file = new File([new ArrayBuffer(35 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = Array.from({ length: 7 }, (_, i) => `https://s3.example.com/part${i + 1}`);

      const uploadOrder: number[] = [];

      (global.fetch as vi.Mock).mockImplementation(async (url) => {
        const partNum = parseInt(url.match(/part(\d+)/)?.[1] || '0');
        uploadOrder.push(partNum);

        return {
          ok: true,
          headers: new Map([['ETag', `"etag-${partNum}"`]]),
        };
      });

      await service.uploadFile(file, presignedUrls);

      // First batch: parts 1, 2, 3
      // Second batch: parts 4, 5, 6
      // Third batch: part 7
      expect(uploadOrder.slice(0, 3)).toEqual([1, 2, 3]);
      expect(uploadOrder.slice(3, 6)).toEqual([4, 5, 6]);
      expect(uploadOrder.slice(6)).toEqual([7]);
    });
  });

  // ==========================================================================
  // Progress Tracking
  // ==========================================================================

  describe('Progress Tracking', () => {
    it('should report progress from 0% to 100%', async () => {
      const file = new File([new ArrayBuffer(15 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = Array.from({ length: 3 }, (_, i) => `https://s3.example.com/part${i + 1}`);

      const progressUpdates: number[] = [];
      const progressCallback = vi.fn((progress: number) => {
        progressUpdates.push(progress);
      });

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"etag"']]),
      });

      await service.uploadFile(file, presignedUrls, progressCallback);

      // Should have progress updates
      expect(progressCallback).toHaveBeenCalled();

      // Progress should increase from 0 to 100
      expect(progressUpdates[0]).toBeLessThan(progressUpdates[progressUpdates.length - 1]);

      // Final progress should be 100%
      const finalProgress = progressUpdates[progressUpdates.length - 1];
      expect(finalProgress).toBe(100);
    });

    it('should report status messages during upload', async () => {
      const file = new File([new ArrayBuffer(10 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1', 'https://s3.example.com/part2'];

      const statusMessages: string[] = [];
      const progressCallback = vi.fn((_progress, status, message) => {
        if (message) statusMessages.push(message);
      });

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"etag"']]),
      });

      await service.uploadFile(file, presignedUrls, progressCallback);

      // Should have status messages
      expect(statusMessages.length).toBeGreaterThan(0);
      expect(statusMessages.some(msg => msg.includes('Uploading part'))).toBe(true);
    });

    it('should invoke callback with correct parameters', async () => {
      const file = new File([new ArrayBuffer(5 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      const progressCallback = vi.fn();

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"etag"']]),
      });

      await service.uploadFile(file, presignedUrls, progressCallback);

      expect(progressCallback).toHaveBeenCalledWith(
        expect.any(Number), // progress
        'uploading',        // status
        expect.any(String)  // message
      );
    });
  });

  // ==========================================================================
  // ETag Extraction
  // ==========================================================================

  describe('ETag Extraction', () => {
    it('should extract ETag from response headers', async () => {
      const file = new File([new ArrayBuffer(5 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"abc123def456"']]),
      });

      const parts = await service.uploadFile(file, presignedUrls);

      expect(parts[0].etag).toBe('abc123def456');
    });

    it('should remove quotes from ETag', async () => {
      const file = new File([new ArrayBuffer(5 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"quoted-etag"']]),
      });

      const parts = await service.uploadFile(file, presignedUrls);

      // Should remove quotes
      expect(parts[0].etag).toBe('quoted-etag');
      expect(parts[0].etag).not.toContain('"');
    });

    it('should throw error if ETag missing', async () => {
      const file = new File([new ArrayBuffer(5 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([]), // No ETag
      });

      await expect(service.uploadFile(file, presignedUrls)).rejects.toThrow('No ETag returned');
    });

    it('should collect ETags for all parts', async () => {
      const file = new File([new ArrayBuffer(15 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = Array.from({ length: 3 }, (_, i) => `https://s3.example.com/part${i + 1}`);

      (global.fetch as vi.Mock).mockImplementation(async (url) => {
        const partNum = url.match(/part(\d+)/)?.[1];
        return {
          ok: true,
          headers: new Map([['ETag', `"etag-part${partNum}"`]]),
        };
      });

      const parts = await service.uploadFile(file, presignedUrls);

      expect(parts).toEqual([
        { partNumber: 1, etag: 'etag-part1' },
        { partNumber: 2, etag: 'etag-part2' },
        { partNumber: 3, etag: 'etag-part3' },
      ]);
    });
  });

  // ==========================================================================
  // Error Handling
  // ==========================================================================

  describe('Error Handling', () => {
    it('should throw error on failed part upload', async () => {
      const file = new File([new ArrayBuffer(5 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      (global.fetch as vi.Mock).mockResolvedValue({
        ok: false,
        statusText: 'Forbidden',
      });

      await expect(service.uploadFile(file, presignedUrls)).rejects.toThrow('Failed to upload part 1');
    });

    it('should throw error on network failure', async () => {
      const file = new File([new ArrayBuffer(5 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = ['https://s3.example.com/part1'];

      (global.fetch as vi.Mock).mockRejectedValue(new Error('Network error'));

      await expect(service.uploadFile(file, presignedUrls)).rejects.toThrow('Network error');
    });

    it('should handle partial upload failure gracefully', async () => {
      const file = new File([new ArrayBuffer(15 * 1024 * 1024)], 'video.mp4', {
        type: 'video/mp4',
      });

      const presignedUrls = Array.from({ length: 3 }, (_, i) => `https://s3.example.com/part${i + 1}`);

      (global.fetch as vi.Mock).mockImplementation(async (url) => {
        // Fail on part 2
        if (url.includes('part2')) {
          return { ok: false, statusText: 'Error' };
        }
        return {
          ok: true,
          headers: new Map([['ETag', '"etag"']]),
        };
      });

      await expect(service.uploadFile(file, presignedUrls)).rejects.toThrow('Failed to upload part 2');
    });
  });

  // ==========================================================================
  // Integration: Full Upload Flow
  // ==========================================================================

  describe('Integration: Complete Upload Flow', () => {
    it('should complete full upload flow: init → upload → complete', async () => {
      const file = new File([new ArrayBuffer(10 * 1024 * 1024)], 'test.mp4', {
        type: 'video/mp4',
      });

      // Mock init response
      (apiClient.post as vi.Mock).mockResolvedValueOnce({
        data: {
          success: true,
          data: {
            videoId: 'video-123',
            uploadId: 'upload-456',
            storageKey: 'videos/test.mp4',
            presignedUrls: ['https://s3.example.com/part1', 'https://s3.example.com/part2'],
          },
        },
      });

      // Mock upload response
      (global.fetch as vi.Mock).mockResolvedValue({
        ok: true,
        headers: new Map([['ETag', '"etag-123"']]),
      });

      // Mock complete response
      (apiClient.post as vi.Mock).mockResolvedValueOnce({
        data: {
          success: true,
          data: {
            id: 'video-123',
            status: 'uploaded',
            cdnUrl: 'https://cdn.example.com/video-123.mp4',
          },
        },
      });

      const result = await service.uploadVideo(
        file,
        {
          courseId: 'course-123',
          lessonId: 'lesson-456',
          metadata: {
            title: 'Test Video',
            description: 'Test description',
            tags: ['test'],
            language: 'en',
            difficulty: 'beginner',
          },
        },
        vi.fn()
      );

      expect(result.id).toBe('video-123');
      expect(result.status).toBe('uploaded');
      expect(apiClient.post).toHaveBeenCalledTimes(2); // init + complete
    });
  });
});
