import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MediaService } from './MediaService';
import type { MediaApiClient } from '../../api/media/MediaApiClient';
import type { MediaPresenter } from '../../presenters/MediaPresenter';
import type {
  UploadMediaInputDto,
  UploadMediaOutputDto,
  GetMediaOutputDto,
  DeleteMediaOutputDto
} from '../../dtos';
import type {
  UploadMediaViewModel,
  MediaViewModel,
  DeleteMediaViewModel
} from '../../view-models';

describe('MediaService', () => {
  let service: MediaService;
  let mockApiClient: MediaApiClient;
  let mockPresenter: MediaPresenter;

  beforeEach(() => {
    mockApiClient = {
      uploadMedia: vi.fn(),
      getMedia: vi.fn(),
      deleteMedia: vi.fn(),
    } as unknown as MediaApiClient;

    mockPresenter = {
      presentUpload: vi.fn(),
      presentMedia: vi.fn(),
      presentDelete: vi.fn(),
    } as unknown as MediaPresenter;

    service = new MediaService(mockApiClient, mockPresenter);
  });

  describe('constructor', () => {
    it('should create instance with injected dependencies', () => {
      expect(service).toBeInstanceOf(MediaService);
    });
  });

  describe('uploadMedia', () => {
    it('should upload media and transform via presenter', async () => {
      // Arrange
      const input: UploadMediaInputDto = {
        file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
        type: 'image',
        category: 'avatar',
      };

      const mockDto: UploadMediaOutputDto = {
        success: true,
        mediaId: 'media-123',
        url: 'https://example.com/media/test.jpg',
      };

      const mockViewModel: UploadMediaViewModel = {
        success: true,
        mediaId: 'media-123',
        url: 'https://example.com/media/test.jpg',
      };

      vi.mocked(mockApiClient.uploadMedia).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.presentUpload).mockReturnValue(mockViewModel);

      // Act
      const result = await service.uploadMedia(input);

      // Assert
      expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
      expect(mockPresenter.presentUpload).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle upload failure', async () => {
      // Arrange
      const input: UploadMediaInputDto = {
        file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
        type: 'image',
      };

      const mockDto: UploadMediaOutputDto = {
        success: false,
        error: 'Upload failed',
      };

      const mockViewModel: UploadMediaViewModel = {
        success: false,
        error: 'Upload failed',
      };

      vi.mocked(mockApiClient.uploadMedia).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.presentUpload).mockReturnValue(mockViewModel);

      // Act
      const result = await service.uploadMedia(input);

      // Assert
      expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
      expect(mockPresenter.presentUpload).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
      expect(result.success).toBe(false);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const input: UploadMediaInputDto = {
        file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
        type: 'image',
      };
      const error = new Error('Network error');
      vi.mocked(mockApiClient.uploadMedia).mockRejectedValue(error);

      // Act & Assert
      await expect(service.uploadMedia(input)).rejects.toThrow('Network error');
      expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
      expect(mockPresenter.presentUpload).not.toHaveBeenCalled();
    });
  });

  describe('getMedia', () => {
    it('should fetch media and transform via presenter', async () => {
      // Arrange
      const mediaId = 'media-123';
      const mockDto: GetMediaOutputDto = {
        id: 'media-123',
        url: 'https://example.com/media/test.jpg',
        type: 'image',
        category: 'avatar',
        uploadedAt: '2023-01-01T00:00:00Z',
        size: 1024,
      };

      const mockViewModel: MediaViewModel = {
        id: 'media-123',
        url: 'https://example.com/media/test.jpg',
        type: 'image',
        category: 'avatar',
        uploadedAt: new Date('2023-01-01T00:00:00Z'),
        size: 1024,
      };

      vi.mocked(mockApiClient.getMedia).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.presentMedia).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getMedia(mediaId);

      // Assert
      expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId);
      expect(mockPresenter.presentMedia).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle media without optional fields', async () => {
      // Arrange
      const mediaId = 'media-123';
      const mockDto: GetMediaOutputDto = {
        id: 'media-123',
        url: 'https://example.com/media/test.jpg',
        type: 'image',
        uploadedAt: '2023-01-01T00:00:00Z',
      };

      const mockViewModel: MediaViewModel = {
        id: 'media-123',
        url: 'https://example.com/media/test.jpg',
        type: 'image',
        uploadedAt: new Date('2023-01-01T00:00:00Z'),
      };

      vi.mocked(mockApiClient.getMedia).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.presentMedia).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getMedia(mediaId);

      // Assert
      expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId);
      expect(mockPresenter.presentMedia).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const mediaId = 'media-123';
      const error = new Error('Media not found');
      vi.mocked(mockApiClient.getMedia).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getMedia(mediaId)).rejects.toThrow('Media not found');
      expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId);
      expect(mockPresenter.presentMedia).not.toHaveBeenCalled();
    });
  });

  describe('deleteMedia', () => {
    it('should delete media and transform via presenter', async () => {
      // Arrange
      const mediaId = 'media-123';
      const mockDto: DeleteMediaOutputDto = {
        success: true,
      };

      const mockViewModel: DeleteMediaViewModel = {
        success: true,
      };

      vi.mocked(mockApiClient.deleteMedia).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.presentDelete).mockReturnValue(mockViewModel);

      // Act
      const result = await service.deleteMedia(mediaId);

      // Assert
      expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId);
      expect(mockPresenter.presentDelete).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
    });

    it('should handle delete failure', async () => {
      // Arrange
      const mediaId = 'media-123';
      const mockDto: DeleteMediaOutputDto = {
        success: false,
        error: 'Delete failed',
      };

      const mockViewModel: DeleteMediaViewModel = {
        success: false,
        error: 'Delete failed',
      };

      vi.mocked(mockApiClient.deleteMedia).mockResolvedValue(mockDto);
      vi.mocked(mockPresenter.presentDelete).mockReturnValue(mockViewModel);

      // Act
      const result = await service.deleteMedia(mediaId);

      // Assert
      expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId);
      expect(mockPresenter.presentDelete).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModel);
      expect(result.success).toBe(false);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const mediaId = 'media-123';
      const error = new Error('Delete failed');
      vi.mocked(mockApiClient.deleteMedia).mockRejectedValue(error);

      // Act & Assert
      await expect(service.deleteMedia(mediaId)).rejects.toThrow('Delete failed');
      expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId);
      expect(mockPresenter.presentDelete).not.toHaveBeenCalled();
    });
  });
});