import { beforeEach, describe, expect, it, vi } from 'vitest';

import { Clip } from '@/state/clipStore';

import { constructStemsZipPayload } from './downloadStemsAsZip';

// Mock dependencies
vi.mock('../components/edit2025/getTimingFromDownbeats', () => ({
  getDownbeatsFromTiming: vi.fn(() => [
    [0, 0],
    [10, 10],
  ]),
}));

vi.mock('../components/studio/createStudioClipSync', () => ({
  default: vi.fn((clipData) => ({
    clip: {
      id: clipData.id,
      type: 'clip',
      trackId: 'track-id',
      startBeats: 0,
      endBeats: 10,
      trimStartBeats: 0,
      trimEndBeats: 10,
      fadeInBeats: 0,
      fadeOutBeats: 0,
      volume: 1,
      url: `https://example.com/${clipData.id}.mp3`,
    },
  })),
}));

vi.mock('../components/studio/createTrack', () => ({
  default: vi.fn(() => ({
    id: 'track-' + Math.random(),
    name: 'Untitled Track',
    clips: [],
    volume: 1,
    pan: 0,
    muted: false,
    soloed: false,
  })),
}));

vi.mock('../components/studio/defaultState', () => ({
  default: {
    tracks: [],
    timing: {
      type: 'manual',
      bps: 2,
      bpsAutomation: [],
      lockBPS: false,
    },
  },
}));

vi.mock('../components/studio/selectors', () => ({
  getSongStartBeats: vi.fn(() => 0),
  getSongEndBeats: vi.fn(() => 10),
  getDerivedTiming: vi.fn(() => ({
    bps: 2,
    bpsAutomation: [],
    lockBPS: false,
  })),
}));

vi.mock('./snap', () => ({
  default: vi.fn((value) => value),
}));

describe('constructStemsZipPayload', () => {
  const mockClips: Clip[] = [
    {
      id: 'clip-1',
      title: 'Vocals Track',
      metadata: {
        stem_type_group_name: 'Vocals',
        duration: 30,
      },
      user_id: 'user-1',
    } as Clip,
    {
      id: 'clip-2',
      title: 'Drums Track',
      metadata: {
        stem_type_group_name: 'Drums',
        duration: 30,
      },
      user_id: 'user-1',
    } as Clip,
    {
      id: 'clip-3',
      title: 'Bass Track',
      metadata: {
        stem_type_group_name: 'Bass',
        duration: 30,
      },
      user_id: 'user-1',
    } as Clip,
  ];

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

  describe('Basic Functionality', () => {
    it('should construct a valid payload with default parameters', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result).toBeDefined();
      expect(result.title).toBe('Test Song');
      expect(result.lyrics).toBe('');
      expect(result.tags).toBe('');
      expect(result.negative_tags).toBe('');
      expect(result.style_summary).toBe('');
      expect(result.caption).toBe('');
      expect(result.web_client_pathname).toBe('/stems');
      expect(result.format).toBe('wav_s16');
    });

    it('should set the correct title', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'My Amazing Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.title).toBe('My Amazing Song');
    });

    it('should include the correct pathname', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/custom/path',
      });

      expect(result.web_client_pathname).toBe('/custom/path');
    });

    it('should set project_id to undefined', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.project_id).toBeUndefined();
    });
  });

  describe('Format Handling', () => {
    it('should convert wav format to wav_s16', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        format: 'wav',
      });

      expect(result.format).toBe('wav_s16');
    });

    it('should keep mp3 format as mp3', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        format: 'mp3',
      });

      expect(result.format).toBe('mp3');
    });
  });

  describe('Downbeats Handling', () => {
    it('should use fallback downbeats when not provided', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      // Should still construct a valid state
      expect(result.state).toBeDefined();
      expect(result.state.timing).toBeDefined();
    });

    it('should use provided downbeats', () => {
      const downbeats: [number, number][] = [
        [0, 0],
        [2, 1],
        [4, 2],
        [6, 3],
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
      });

      expect(result.state).toBeDefined();
      expect(result.downbeats).toBeDefined();
    });

    it('should handle empty downbeats array', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats: [],
      });

      expect(result.state).toBeDefined();
    });
  });

  describe('Tempo Locking', () => {
    it('should use manual timing when useTempoLocked is false and no downbeats', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        format: 'wav',
        useTempoLocked: false,
      });

      expect(result.state.timing.type).toBe('manual');
    });

    it('should use follow-track timing when downbeats provided but not tempo-locked', () => {
      const downbeats: [number, number][] = [
        [0, 0],
        [2, 1],
        [4, 2],
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
        format: 'wav',
        useTempoLocked: false,
      });

      // Note: The final state.timing is always set to 'manual' after getDerivedTiming,
      // but the important thing is that it should not append BPM to the title
      expect(result.state.timing.type).toBe('manual');
      expect(result.title).toBe('Test Song');
      expect(result.title).not.toMatch(/BPM/);
    });

    it('should use manual timing with BPM when tempo-locked', () => {
      const downbeats: [number, number][] = [
        [0, 0],
        [2, 1],
        [4, 2],
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
        format: 'wav',
        useTempoLocked: true,
      });

      expect(result.state.timing.type).toBe('manual');
      expect(result.state.timing).toHaveProperty('lockBPS');
    });

    it('should append BPM to title when tempo-locked', () => {
      const downbeats: [number, number][] = [
        [0, 0],
        [2, 1], // 1 beat in 2 seconds = 0.5 BPS = 30 BPM
        [4, 2],
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
        format: 'wav',
        useTempoLocked: true,
      });

      // Title should include BPM calculation (bps * 60)
      expect(result.title).toMatch(/\d+BPM/);
    });

    it('should not append BPM to title when not tempo-locked', () => {
      const downbeats: [number, number][] = [
        [0, 0],
        [2, 1],
        [4, 2],
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
        format: 'wav',
        useTempoLocked: false,
      });

      expect(result.title).toBe('Test Song');
      expect(result.title).not.toMatch(/BPM/);
    });
  });

  describe('Track Naming', () => {
    it('should map Vocals stem type to Lead Vocals', () => {
      const clips: Clip[] = [
        {
          id: 'clip-1',
          title: 'Some Title',
          metadata: {
            stem_type_group_name: 'Vocals',
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('Lead Vocals');
    });

    it('should map Backing_Vocals stem type to Backing Vocals', () => {
      const clips: Clip[] = [
        {
          id: 'clip-1',
          title: 'Some Title',
          metadata: {
            stem_type_group_name: 'Backing_Vocals',
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('Backing Vocals');
    });

    it('should map FX stem type to Other', () => {
      const clips: Clip[] = [
        {
          id: 'clip-1',
          title: 'Some Title',
          metadata: {
            stem_type_group_name: 'FX',
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('Other');
    });

    it('should use stem type with underscore replacement for unmapped types', () => {
      const clips: Clip[] = [
        {
          id: 'clip-1',
          title: 'Some Title',
          metadata: {
            stem_type_group_name: 'Custom_Instrument',
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('Custom Instrument');
    });

    it('should use clip title when no stem type is provided', () => {
      const clips: Clip[] = [
        {
          id: 'clip-1',
          title: 'My Custom Track',
          metadata: {
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('My Custom Track');
    });

    it('should fallback to Track N when no stem type or title', () => {
      const clips: Clip[] = [
        {
          id: 'clip-1',
          title: '',
          metadata: {
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
        {
          id: 'clip-2',
          metadata: {
            duration: 30,
          },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('Track 1');
      expect(result.state.tracks[1].name).toBe('Track 2');
    });

    it('should handle all standard stem types correctly', () => {
      const stemTypes = [
        { name: 'Drums', expected: 'Drums' },
        { name: 'Bass', expected: 'Bass' },
        { name: 'Guitar', expected: 'Guitar' },
        { name: 'Keyboard', expected: 'Keyboard' },
        { name: 'Percussion', expected: 'Percussion' },
        { name: 'Strings', expected: 'Strings' },
        { name: 'Synth', expected: 'Synth' },
        { name: 'Brass', expected: 'Brass' },
        { name: 'Woodwinds', expected: 'Woodwinds' },
      ];

      stemTypes.forEach(({ name, expected }) => {
        const clips: Clip[] = [
          {
            id: 'clip-1',
            metadata: {
              stem_type_group_name: name,
              duration: 30,
            },
            user_id: 'user-1',
          } as Clip,
        ];

        const result = constructStemsZipPayload({
          rootClipName: 'Test Song',
          clips,
          durationSeconds: 30,
          pathname: '/stems',
        });

        expect(result.state.tracks[0].name).toBe(expected);
      });
    });
  });

  describe('Multiple Clips', () => {
    it('should create a track for each clip', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks).toHaveLength(mockClips.length);
    });

    it('should handle single clip', () => {
      const singleClip = [mockClips[0]];
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: singleClip,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks).toHaveLength(1);
    });

    it('should handle many clips', () => {
      const manyClips: Clip[] = Array.from({ length: 10 }, (_, i) => ({
        id: `clip-${i}`,
        title: `Track ${i}`,
        metadata: {
          duration: 30,
        },
        user_id: 'user-1',
      })) as Clip[];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: manyClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks).toHaveLength(10);
    });

    it('should preserve clip order in tracks', () => {
      const orderedClips: Clip[] = [
        {
          id: 'clip-a',
          title: 'Track A',
          metadata: { duration: 30 },
          user_id: 'user-1',
        } as Clip,
        {
          id: 'clip-b',
          title: 'Track B',
          metadata: { duration: 30 },
          user_id: 'user-1',
        } as Clip,
        {
          id: 'clip-c',
          title: 'Track C',
          metadata: { duration: 30 },
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: orderedClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks[0].name).toBe('Track A');
      expect(result.state.tracks[1].name).toBe('Track B');
      expect(result.state.tracks[2].name).toBe('Track C');
    });
  });

  describe('Validation', () => {
    it('should throw error when clips array is empty', () => {
      expect(() => {
        constructStemsZipPayload({
          rootClipName: 'Test Song',
          clips: [],
          durationSeconds: 30,
          pathname: '/stems',
        });
      }).toThrow('Cannot create stems zip: no clips provided');
    });

    it('should throw with exact error message for empty clips', () => {
      expect(() => {
        constructStemsZipPayload({
          rootClipName: 'My Song',
          clips: [],
          durationSeconds: 60,
          pathname: '/test',
        });
      }).toThrow('Cannot create stems zip: no clips provided');
    });

    it('should not throw when there is at least one clip', () => {
      expect(() => {
        constructStemsZipPayload({
          rootClipName: 'Test Song',
          clips: [mockClips[0]],
          durationSeconds: 30,
          pathname: '/stems',
        });
      }).not.toThrow();
    });
  });

  describe('Edge Cases', () => {
    it('should handle zero duration', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 0,
        pathname: '/stems',
      });

      expect(result).toBeDefined();
    });

    it('should handle very long duration', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 3600,
        pathname: '/stems',
      });

      expect(result).toBeDefined();
    });

    it('should handle special characters in title', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song™ (Remix) [2024] <3',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.title).toBe('Test Song™ (Remix) [2024] <3');
    });

    it('should handle empty title', () => {
      const result = constructStemsZipPayload({
        rootClipName: '',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.title).toBe('');
    });

    it('should handle clips with missing metadata', () => {
      const clipsWithoutMetadata: Clip[] = [
        {
          id: 'clip-1',
          title: 'Track 1',
          user_id: 'user-1',
        } as Clip,
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: clipsWithoutMetadata,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.tracks).toHaveLength(1);
      expect(result.state.tracks[0].name).toBe('Track 1');
    });
  });

  describe('State Structure', () => {
    it('should include correct state structure', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state).toHaveProperty('tracks');
      expect(result.state).toHaveProperty('timing');
      expect(result.state.timing).toHaveProperty('type', 'manual');
    });

    it('should include start_beats and end_beats', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result).toHaveProperty('start_beats');
      expect(result).toHaveProperty('end_beats');
    });

    it('should include downbeats in result', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result).toHaveProperty('downbeats');
      expect(Array.isArray(result.downbeats)).toBe(true);
    });

    it('should set timing type to manual in final state', () => {
      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result.state.timing.type).toBe('manual');
    });
  });

  describe('BPS Calculation Edge Cases', () => {
    it('should handle downbeats with single point when tempo-locked', () => {
      const downbeats: [number, number][] = [[0, 0]];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
        format: 'wav',
        useTempoLocked: true,
      });

      expect(result.state.timing.type).toBe('manual');
    });

    it('should handle downbeats with zero duration when tempo-locked', () => {
      const downbeats: [number, number][] = [
        [0, 0],
        [0, 1],
      ];

      const result = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
        downbeats,
        format: 'wav',
        useTempoLocked: true,
      });

      expect(result.state.timing.type).toBe('manual');
    });
  });

  describe('Return Value Immutability', () => {
    it('should return a new object each time', () => {
      const result1 = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });
      const result2 = constructStemsZipPayload({
        rootClipName: 'Test Song',
        clips: mockClips,
        durationSeconds: 30,
        pathname: '/stems',
      });

      expect(result1).not.toBe(result2);
    });
  });
});
