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

import fixStudioProjectState from './fixStudioProjectState';
import { ManualTiming } from './fixTiming';

describe('fixStudioProjectState', () => {
  it('should fix a messy project state with many issues', () => {
    // Create a state with multiple problems that need fixing
    const messyState = {
      // Missing amplitude - should default to 1.0
      // amplitude: undefined,

      // Invalid metronome
      metronome: {
        enabled: 'yes', // Wrong type
        amplitude: NaN,
      },

      // Old selection format with startBeats/endBeats
      selection: {
        startBeats: 4,
        endBeats: 8,
        trackIds: ['track1', 123, null], // Mixed types
        focusedTrackId: 'track1',
        focusedArea: 'invalid', // Invalid value
      },

      // Loop with invalid constraint
      loop: {
        enabled: true,
        startBeats: 10,
        endBeats: 5, // End before start
      },

      // Timing referencing non-existent track
      timing: {
        type: 'follow-track',
        trackId: 'nonexistent',
        fallbackBPS: 2.5,
      },

      // Invalid fades
      songFadeInBeats: -1,
      songFadeOutBeats: Infinity,

      // Tracks with multiple issues
      tracks: [
        {
          id: 'track1',
          name: 'Track 1',
          // Missing many fields
          clips: [
            {
              id: 'clip1',
              name: 'Clip 1',
              startBeats: 8, // Out of order
              endBeats: 12,
              clipId: 'content1',
              uploadId: null,
              transposition: 100, // Out of range
              warp: {
                speed: 1000, // Out of range
                markers: {
                  0: 0,
                  invalidKey: 123, // Invalid key
                  1: NaN, // Invalid value
                  2: 2,
                },
              },
            },
            {
              id: 'clip2',
              startBeats: 0, // Should sort before clip1
              endBeats: 4.00001, // Small gap to clip3
              clipId: 'content2',
              uploadId: null,
            },
            {
              id: 'clip3',
              startBeats: 4,
              endBeats: 8,
              clipId: null,
              uploadId: 'upload1', // Will be removed (not in keepUploads)
            },
            {
              id: 'duplicate',
              startBeats: 12,
              endBeats: 16,
              clipId: 'content3',
              uploadId: null,
            },
            {
              id: 'duplicate', // Duplicate ID
              startBeats: 16,
              endBeats: 20,
              clipId: 'content4',
              uploadId: null,
            },
          ],
          clipCreationIntents: [
            {
              id: 'intent2',
              possibleClipIds: ['c2'],
              queuedGenerationId: 'gen123', // Old format
              name: 'Intent 2',
              namePersists: false,
              startBeats: 8,
              startTrimmedBeats: 0,
              endTrimmedBeats: 0,
            },
            {
              id: 'intent1',
              possibleClipIds: ['c1'],
              name: 'Intent 1',
              namePersists: false,
              startBeats: 0,
              startTrimmedBeats: 0,
              endTrimmedBeats: 0,
            },
          ],
          // Old EQ format
          eq: [
            { frequency: 200, gain: 3, q: 1.0 },
            { frequency: 500, gain: -2, q: 1.5 },
          ],
        },
      ],
    };

    const result = fixStudioProjectState(messyState, {});

    // Verify top-level fields are fixed
    expect(result.amplitude).toBe(1.0);
    expect(result.metronome.enabled).toBe(false);
    expect(result.metronome.amplitude).toBe(1.0);

    // Selection should be migrated
    expect(result.selection.anchorBeats).toBe(4);
    expect(result.selection.focusBeats).toBe(8);
    expect(result.selection.trackIds).toEqual(['track1']); // Filtered non-strings
    expect(result.selection.focusedArea).toBe(null); // Invalid value fixed

    // Loop constraint fixed
    expect(result.loop.startBeats).toBe(10);
    expect(result.loop.endBeats).toBe(42); // 10 + 32

    // Timing converted to manual (trackId was invalid)
    expect(result.timing.type).toBe('manual');
    expect((result.timing as ManualTiming).bps).toBe(2.5); // Used fallbackBPS

    // Fades fixed
    expect(result.songFadeInBeats).toBe(0);
    expect(result.songFadeOutBeats).toBe(0);

    // Tracks fixed
    expect(result.tracks).toHaveLength(1);
    const track = result.tracks[0];

    expect(track.id).toBe('track1');
    expect(track.clips).toHaveLength(4); // clip3 removed (uploadId not in keepUploads)

    // Clips should be sorted
    expect(track.clips[0].startBeats).toBe(0);
    expect(track.clips[1].startBeats).toBe(8);
    expect(track.clips[2].startBeats).toBe(12);
    expect(track.clips[3].startBeats).toBe(16);

    // Gap should be closed
    expect(track.clips[0].endBeats).toBe(4);

    // Transposition clamped (clip1 was at index 0 in original, now sorted)
    const clip1 = track.clips.find((c) => c.name === 'Clip 1');
    expect(clip1?.transposition).toBe(24);

    // Warp speed clamped
    expect(clip1?.warp.speed).toBe(128);

    // Invalid warp markers removed
    expect(clip1?.warp.markers).toEqual({ 0: 0, 2: 2 });

    // Duplicate IDs fixed
    const duplicateClips = track.clips.filter(
      (c) =>
        c.id.startsWith('duplicate') ||
        c.clipId === 'content3' ||
        c.clipId === 'content4'
    );
    expect(duplicateClips).toHaveLength(2);
    expect(duplicateClips[0].id).not.toBe(duplicateClips[1].id);

    // ClipCreationIntents sorted and migrated
    expect(track.clipCreationIntents).toHaveLength(2);
    expect(track.clipCreationIntents[0].id).toBe('intent1');
    expect(track.clipCreationIntents[1].id).toBe('intent2');
    expect(track.clipCreationIntents[1].queuedGenerationIds).toEqual([]); // Cleared after migration

    // EQ migrated from old array format
    expect(track.eq.enabled).toBe(true);
    expect(track.eq.band2.type).toBe('peaking');
    expect(track.eq.band2.frequency).toBe(200);
    expect(track.eq.band2.gain).toBe(3);
    expect(track.eq.band3.frequency).toBe(500);
  });

  it('should handle serialized state with markersRegistry', () => {
    const serializedState = {
      markersRegistry: {
        warp_abc123: { 0: 0, 1: 1 },
      },
      tracks: [
        {
          id: 'track1',
          clips: [
            {
              id: 'clip1',
              startBeats: 0,
              endBeats: 4,
              clipId: 'content1',
              uploadId: null,
              warp: {
                awaitingAnalysis: false,
                speed: 1.0,
                enabled: true,
                markersHash: 'warp_abc123', // Reference to registry
              },
            },
          ],
        },
      ],
    };

    const result = fixStudioProjectState(serializedState, {});

    // Should deserialize markers from registry
    expect(result.tracks[0].clips[0].warp.markers).toEqual({ 0: 0, 1: 1 });
    expect((result.tracks[0].clips[0].warp as any).markersHash).toBeUndefined();
    expect('markersRegistry' in result).toBe(false);
  });
});
