import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
  formatTime,
  calculateProgress,
  isValidTrack,
  findTrackIndex,
  shuffleTracks,
  AudioPlayerService,
} from './playerService';
import type { Track } from '../types';

describe('playerService', () => {
  describe('formatTime', () => {
    it('should format seconds to MM:SS', () => {
      expect(formatTime(0)).toBe('0:00');
      expect(formatTime(30)).toBe('0:30');
      expect(formatTime(65)).toBe('1:05');
      expect(formatTime(125)).toBe('2:05');
      expect(formatTime(3661)).toBe('61:01');
    });

    it('should handle invalid values', () => {
      expect(formatTime(NaN)).toBe('0:00');
      expect(formatTime(Infinity)).toBe('0:00');
      expect(formatTime(-1)).toBe('0:00');
    });

    it('should round down to nearest second', () => {
      expect(formatTime(65.7)).toBe('1:05');
      expect(formatTime(125.9)).toBe('2:05');
    });
  });

  describe('calculateProgress', () => {
    it('should calculate progress percentage', () => {
      expect(calculateProgress(0, 100)).toBe(0);
      expect(calculateProgress(50, 100)).toBe(50);
      expect(calculateProgress(100, 100)).toBe(100);
      expect(calculateProgress(25, 200)).toBe(12.5);
    });

    it('should return 0 for invalid duration', () => {
      expect(calculateProgress(50, 0)).toBe(0);
      expect(calculateProgress(50, NaN)).toBe(0);
    });
  });

  describe('isValidTrack', () => {
    it('should return true for valid track', () => {
      const track: Track = {
        id: 1,
        title: 'Test Track',
        duration: 180,
        url: 'https://example.com/track.mp3',
      };

      expect(isValidTrack(track)).toBe(true);
    });

    it('should return false for null track', () => {
      expect(isValidTrack(null)).toBe(false);
    });

    it('should return false for track without id', () => {
      const track = {
        title: 'Test Track',
        duration: 180,
        url: 'https://example.com/track.mp3',
      } as Track;

      expect(isValidTrack(track)).toBe(false);
    });

    it('should return false for track without title', () => {
      const track = {
        id: 1,
        duration: 180,
        url: 'https://example.com/track.mp3',
      } as Track;

      expect(isValidTrack(track)).toBe(false);
    });

    it('should return false for track without url', () => {
      const track = {
        id: 1,
        title: 'Test Track',
        duration: 180,
      } as Track;

      expect(isValidTrack(track)).toBe(false);
    });
  });

  describe('findTrackIndex', () => {
    it('should find track index in queue', () => {
      const queue: Track[] = [
        { id: 1, title: 'Track 1', duration: 180, url: 'url1' },
        { id: 2, title: 'Track 2', duration: 200, url: 'url2' },
        { id: 3, title: 'Track 3', duration: 220, url: 'url3' },
      ];

      expect(findTrackIndex(queue, 2)).toBe(1);
      expect(findTrackIndex(queue, 1)).toBe(0);
      expect(findTrackIndex(queue, 3)).toBe(2);
    });

    it('should return -1 if track not found', () => {
      const queue: Track[] = [
        { id: 1, title: 'Track 1', duration: 180, url: 'url1' },
      ];

      expect(findTrackIndex(queue, 999)).toBe(-1);
    });

    it('should return -1 for empty queue', () => {
      expect(findTrackIndex([], 1)).toBe(-1);
    });
  });

  describe('shuffleTracks', () => {
    it('should shuffle tracks array', () => {
      const tracks: Track[] = [
        { id: 1, title: 'Track 1', duration: 180, url: 'url1' },
        { id: 2, title: 'Track 2', duration: 200, url: 'url2' },
        { id: 3, title: 'Track 3', duration: 220, url: 'url3' },
        { id: 4, title: 'Track 4', duration: 240, url: 'url4' },
        { id: 5, title: 'Track 5', duration: 260, url: 'url5' },
      ];

      const shuffled = shuffleTracks(tracks);

      // Should have same length
      expect(shuffled.length).toBe(tracks.length);

      // Should have same tracks (by id)
      const originalIds = tracks.map((t) => t.id).sort();
      const shuffledIds = shuffled.map((t) => t.id).sort();
      expect(shuffledIds).toEqual(originalIds);
    });

    it('should handle empty array', () => {
      expect(shuffleTracks([])).toEqual([]);
    });

    it('should handle single track', () => {
      const tracks: Track[] = [
        { id: 1, title: 'Track 1', duration: 180, url: 'url1' },
      ];

      const shuffled = shuffleTracks(tracks);
      expect(shuffled).toEqual(tracks);
    });

    it('should not mutate original array', () => {
      const tracks: Track[] = [
        { id: 1, title: 'Track 1', duration: 180, url: 'url1' },
        { id: 2, title: 'Track 2', duration: 200, url: 'url2' },
      ];

      const original = [...tracks];
      shuffleTracks(tracks);

      expect(tracks).toEqual(original);
    });
  });

  describe('AudioPlayerService', () => {
    let audioElement: HTMLAudioElement;
    let service: AudioPlayerService;

    beforeEach(() => {
      audioElement = document.createElement('audio');
      service = new AudioPlayerService();
    });

    describe('Initialization', () => {
      it('should initialize with audio element', () => {
        service.initialize(audioElement);
        expect(service.getCurrentTime()).toBe(0);
      });

      it('should throw error if not initialized', async () => {
        await expect(service.play()).rejects.toThrow(
          'Audio element not initialized',
        );
        expect(() => service.pause()).toThrow('Audio element not initialized');
        expect(() => service.seek(10)).toThrow('Audio element not initialized');
      });
    });

    describe('Load track', () => {
      beforeEach(() => {
        service.initialize(audioElement);
      });

      it('should load valid track', async () => {
        const track: Track = {
          id: 1,
          title: 'Test Track',
          duration: 180,
          url: 'https://example.com/track.mp3',
        };

        await service.loadTrack(track);

        expect(audioElement.src).toContain('example.com/track.mp3');
      });

      it('should clear src when loading null track', async () => {
        const track: Track = {
          id: 1,
          title: 'Test Track',
          duration: 180,
          url: 'https://example.com/track.mp3',
        };

        await service.loadTrack(track);
        expect(audioElement.src).toContain('example.com/track.mp3');

        await service.loadTrack(null);

        // After loading null, src should be cleared (empty or about:blank)
        const srcAfterClear = audioElement.src;
        expect(
          srcAfterClear === '' ||
            srcAfterClear === 'about:blank' ||
            srcAfterClear === window.location.href,
        ).toBe(true);
      });

      it('should throw error for invalid track', async () => {
        const invalidTrack = {
          id: 1,
          title: '',
          duration: 180,
          url: '',
        } as Track;

        await expect(service.loadTrack(invalidTrack)).rejects.toThrow(
          'Invalid track',
        );
      });
    });

    describe('Playback control', () => {
      beforeEach(() => {
        service.initialize(audioElement);
      });

      it('should play audio', async () => {
        // Mock play method
        const playPromise = Promise.resolve();
        audioElement.play = vi.fn().mockReturnValue(playPromise);

        await service.play();

        expect(audioElement.play).toHaveBeenCalled();
      });

      it('should pause audio', () => {
        audioElement.pause = vi.fn();

        service.pause();

        expect(audioElement.pause).toHaveBeenCalled();
      });

      it('should stop audio', () => {
        audioElement.pause = vi.fn();
        audioElement.currentTime = 50;

        service.stop();

        expect(audioElement.pause).toHaveBeenCalled();
        expect(audioElement.currentTime).toBe(0);
      });

      it('should handle play error', async () => {
        const error = new Error('Play failed');
        audioElement.play = vi.fn().mockRejectedValue(error);

        await expect(service.play()).rejects.toThrow('Failed to play audio');
      });
    });

    describe('Seek', () => {
      beforeEach(() => {
        service.initialize(audioElement);
        Object.defineProperty(audioElement, 'duration', {
          value: 180,
          writable: true,
          configurable: true,
        });
      });

      it('should seek to position', () => {
        service.seek(50);

        expect(audioElement.currentTime).toBe(50);
      });

      it('should clamp seek to duration', () => {
        service.seek(200);

        expect(audioElement.currentTime).toBe(180);
      });

      it('should not allow negative seek', () => {
        service.seek(-10);

        expect(audioElement.currentTime).toBe(0);
      });
    });

    describe('Volume control', () => {
      beforeEach(() => {
        service.initialize(audioElement);
      });

      it('should set volume', () => {
        service.setVolume(0.5);

        expect(audioElement.volume).toBe(0.5);
      });

      it('should clamp volume to 0-1', () => {
        service.setVolume(1.5);
        expect(audioElement.volume).toBe(1);

        service.setVolume(-0.5);
        expect(audioElement.volume).toBe(0);
      });

      it('should set muted', () => {
        service.setMuted(true);
        expect(audioElement.muted).toBe(true);

        service.setMuted(false);
        expect(audioElement.muted).toBe(false);
      });

      it('should get volume', () => {
        audioElement.volume = 0.75;
        expect(service.getVolume()).toBe(0.75);
      });

      it('should get muted state', () => {
        audioElement.muted = true;
        expect(service.isMuted()).toBe(true);
      });
    });

    describe('State getters', () => {
      beforeEach(() => {
        service.initialize(audioElement);
      });

      it('should get current time', () => {
        audioElement.currentTime = 45;
        expect(service.getCurrentTime()).toBe(45);
      });

      it('should get duration', () => {
        Object.defineProperty(audioElement, 'duration', {
          value: 180,
          writable: true,
          configurable: true,
        });
        expect(service.getDuration()).toBe(180);
      });

      it('should check if playing', () => {
        Object.defineProperty(audioElement, 'paused', {
          value: false,
          writable: true,
          configurable: true,
        });
        Object.defineProperty(audioElement, 'ended', {
          value: false,
          writable: true,
          configurable: true,
        });
        expect(service.isPlaying()).toBe(true);

        Object.defineProperty(audioElement, 'paused', {
          value: true,
          writable: true,
          configurable: true,
        });
        expect(service.isPlaying()).toBe(false);
      });
    });

    describe('Event callbacks', () => {
      beforeEach(() => {
        service.initialize(audioElement);
      });

      it('should call timeUpdate callback', () => {
        const callback = vi.fn();
        service.onTimeUpdate(callback);

        audioElement.currentTime = 10;
        audioElement.dispatchEvent(new Event('timeupdate'));

        expect(callback).toHaveBeenCalledWith(10);
      });

      it('should call durationChange callback', () => {
        const callback = vi.fn();
        service.onDurationChange(callback);

        Object.defineProperty(audioElement, 'duration', {
          value: 180,
          writable: true,
          configurable: true,
        });
        audioElement.dispatchEvent(new Event('loadedmetadata'));

        expect(callback).toHaveBeenCalledWith(180);
      });

      it('should call ended callback', () => {
        const callback = vi.fn();
        service.onEnded(callback);

        audioElement.dispatchEvent(new Event('ended'));

        expect(callback).toHaveBeenCalled();
      });

      it('should call error callback', () => {
        const callback = vi.fn();
        service.onError(callback);

        const errorEvent = new Event('error');
        Object.defineProperty(audioElement, 'error', {
          value: { message: 'Test error' },
          writable: true,
        });
        audioElement.dispatchEvent(errorEvent);

        expect(callback).toHaveBeenCalled();
      });

      it('should call play callback', () => {
        const callback = vi.fn();
        service.onPlay(callback);

        audioElement.dispatchEvent(new Event('play'));

        expect(callback).toHaveBeenCalled();
      });

      it('should call pause callback', () => {
        const callback = vi.fn();
        service.onPause(callback);

        audioElement.dispatchEvent(new Event('pause'));

        expect(callback).toHaveBeenCalled();
      });
    });

    describe('Cleanup', () => {
      it('should cleanup event listeners', () => {
        service.initialize(audioElement);
        const callback = vi.fn();
        service.onTimeUpdate(callback);

        service.cleanup();

        audioElement.dispatchEvent(new Event('timeupdate'));
        expect(callback).not.toHaveBeenCalled();
      });
    });
  });
});
