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

import { StudioClip } from './fixClip';
import { StudioProjectState } from './fixStudioProjectState';
import { StudioTrack } from './fixTrack';
import {
  SerializedProjectState,
  deserializeProjectState,
  isSerializedProjectState,
  maybeDeserializeProjectState,
  maybeDeserializeProjectStateInPlace,
  serializeProjectState,
} from './serialization';

// Helper to create a minimal valid clip
const createClip = (
  id: string,
  markers: Record<number, number>
): StudioClip => ({
  id,
  streaming: false,
  name: 'Clip',
  color: '#FF0000',
  transposition: 0,
  amplitude: 1.0,
  startBeats: 0,
  endBeats: 4,
  readStartBeats: 0,
  fadeInBeats: 0,
  fadeOutBeats: 0,
  mute: false,
  loop: {
    enabled: false,
    startBeats: 0,
    endBeats: 4,
  },
  warp: {
    awaitingAnalysis: false,
    speed: 1.0,
    enabled: true,
    markers,
  },
  clipId: id,
  uploadId: null,
});

// Helper to create a minimal valid track
const createTrack = (id: string, clips: StudioClip[]): StudioTrack => ({
  id,
  name: 'Track',
  height: 100,
  clips,
  clipCreationIntents: [],
  solo: false,
  mute: false,
  arm: false,
  amplitude: 1.0,
  balance: 0,
  color: '#FF0000',
  input: null,
  instrument: { type: 'song' },
  soloTakeLaneId: null,
  takeLanes: [],
  takeLanesExpanded: false,
  eq: {
    enabled: true,
    band1: {
      type: 'highpass',
      enabled: false,
      frequency: 60,
      gain: 0,
      q: 0.707,
    },
    band2: {
      type: 'lowshelf',
      enabled: true,
      frequency: 160,
      gain: 0,
      q: 0.707,
    },
    band3: {
      type: 'peaking',
      enabled: true,
      frequency: 450,
      gain: 0,
      q: 0.707,
    },
    band4: {
      type: 'peaking',
      enabled: true,
      frequency: 1200,
      gain: 0,
      q: 0.707,
    },
    band5: {
      type: 'highshelf',
      enabled: true,
      frequency: 3200,
      gain: 0,
      q: 0.707,
    },
    band6: {
      type: 'lowpass',
      enabled: false,
      frequency: 8800,
      gain: 0,
      q: 0.707,
    },
  },
});

describe('serialization', () => {
  describe('serializeProjectState', () => {
    it('should create markersRegistry for unique markers', () => {
      const markers1 = { 0: 0, 1: 1, 2: 2 };
      const markers2 = { 0: 0, 1: 1, 2: 2 }; // Same content
      const markers3 = { 0: 0, 1: 1.5, 2: 2 }; // Different content

      const clip1 = createClip('clip1', markers1);
      const clip2 = createClip('clip2', markers2);
      const clip3 = createClip('clip3', markers3);
      const track = createTrack('track1', [clip1, clip2, clip3]);

      const state = { tracks: [track], otherField: 'test' };
      const serialized = serializeProjectState(state);

      // Should have markersRegistry
      expect(serialized.markersRegistry).toBeDefined();
      expect(Object.keys(serialized.markersRegistry).length).toBeGreaterThan(0);

      // Should replace markers with markersHash
      expect(serialized.tracks[0].clips[0].warp.markers).toBeUndefined();
      expect(serialized.tracks[0].clips[0].warp.markersHash).toBeDefined();
      expect(typeof serialized.tracks[0].clips[0].warp.markersHash).toBe(
        'string'
      );

      // Clips with identical markers should have the same hash
      expect(serialized.tracks[0].clips[0].warp.markersHash).toBe(
        serialized.tracks[0].clips[1].warp.markersHash
      );

      // Clip with different markers should have different hash
      expect(serialized.tracks[0].clips[0].warp.markersHash).not.toBe(
        serialized.tracks[0].clips[2].warp.markersHash
      );

      // Should preserve other state fields
      expect(serialized.otherField).toBe('test');
    });

    it('should handle clips with empty markers', () => {
      const clip = createClip('clip1', {});
      const track = createTrack('track1', [clip]);
      const state = { tracks: [track] };

      const serialized = serializeProjectState(state);

      expect(serialized.markersRegistry).toBeDefined();
      expect(serialized.tracks[0].clips[0].warp.markersHash).toBeDefined();
    });

    it('should handle tracks with take lanes', () => {
      const markers = { 0: 0, 1: 1 };
      const mainClip = createClip('main1', markers);
      const takeLaneClip = createClip('take1', markers);

      const track = createTrack('track1', [mainClip]);
      track.takeLanes = [
        {
          id: 'takeLane1',
          name: 'Take Lane',
          height: 100,
          clips: [takeLaneClip],
          clipCreationIntents: [],
        },
      ];

      const state = { tracks: [track] };
      const serialized = serializeProjectState(state);

      // Both clips should reference the same hash
      expect(serialized.tracks[0].clips[0].warp.markersHash).toBe(
        serialized.tracks[0].takeLanes[0].clips[0].warp.markersHash
      );
    });
  });

  describe('deserializeProjectState', () => {
    it('should restore markers from markersRegistry', () => {
      const markers = { 0: 0, 1: 1, 2: 2 };
      const clip = createClip('clip1', markers);
      const track = createTrack('track1', [clip]);
      const state = { tracks: [track] };

      const serialized = serializeProjectState(state);
      const deserialized = deserializeProjectState(serialized);

      // Markers should be restored
      expect(deserialized.tracks[0].clips[0].warp.markers).toEqual(markers);
      expect(
        (deserialized.tracks[0].clips[0].warp as any).markersHash
      ).toBeUndefined();
    });

    it('should handle backward compatibility with direct markers', () => {
      const clip = createClip('clip1', { 0: 0, 1: 1 });
      const track = createTrack('track1', [clip]);
      const state = { tracks: [track] };

      // Simulate old format (no serialization, direct markers)
      const deserialized = deserializeProjectState(
        state as any as SerializedProjectState
      );

      expect(deserialized.tracks[0].clips[0].warp.markers).toEqual({
        0: 0,
        1: 1,
      });
    });

    it('should handle missing markersHash gracefully', () => {
      const serialized: SerializedProjectState = {
        markersRegistry: {},
        tracks: [
          {
            ...createTrack('track1', []),
            clips: [
              {
                ...createClip('clip1', {}),
                warp: {
                  awaitingAnalysis: false,
                  speed: 1.0,
                  enabled: true,
                  // No markers, no markersHash
                },
              } as any,
            ],
          },
        ],
      };

      const deserialized = deserializeProjectState(serialized);

      // Should fallback to empty markers
      expect(deserialized.tracks[0].clips[0].warp.markers).toEqual({});
    });
  });

  describe('round-trip', () => {
    it('should preserve state through serialize -> deserialize', () => {
      const markers1 = { 0: 0, 1: 1, 2: 2 };
      const markers2 = { 0: 0, 1: 1.5 };

      const clip1 = createClip('clip1', markers1);
      const clip2 = createClip('clip2', markers2);
      const clip3 = createClip('clip3', markers1); // Duplicate of clip1

      const track = createTrack('track1', [clip1, clip2, clip3]);
      const state = {
        tracks: [track],
        amplitude: 1.0,
        selection: { anchorBeats: 0, focusBeats: 4 },
      };

      const serialized = serializeProjectState(state);
      const deserialized = deserializeProjectState(serialized);

      // Core state should be preserved
      expect(deserialized.amplitude).toBe(1.0);
      expect(deserialized.selection).toEqual({ anchorBeats: 0, focusBeats: 4 });

      // Markers should be restored correctly
      expect(deserialized.tracks[0].clips[0].warp.markers).toEqual(markers1);
      expect(deserialized.tracks[0].clips[1].warp.markers).toEqual(markers2);
      expect(deserialized.tracks[0].clips[2].warp.markers).toEqual(markers1);

      // After deduplication in a real scenario, clips 0 and 2 would share the same markers object
      // But in this test they won't since we're not calling deduplicateWarpMarkers
    });
  });

  describe('isSerializedProjectState', () => {
    it('should return true for serialized state', () => {
      const serialized: SerializedProjectState = {
        markersRegistry: {},
        tracks: [],
      };

      expect(isSerializedProjectState(serialized)).toBe(true);
    });

    it('should return false for regular state', () => {
      const regular = { tracks: [], timing: {} };

      expect(isSerializedProjectState(regular)).toBe(false);
    });

    it('should return false for null/undefined', () => {
      expect(isSerializedProjectState(null)).toBe(false);
      expect(isSerializedProjectState(undefined)).toBe(false);
    });
  });

  describe('maybeDeserializeProjectState', () => {
    it('should deserialize serialized state', () => {
      const markers = { 0: 0, 1: 1 };
      const clip = createClip('clip1', markers);
      const track = createTrack('track1', [clip]);
      const state = { tracks: [track] };

      const serialized = serializeProjectState(state);
      const deserialized = maybeDeserializeProjectState(serialized);

      expect(deserialized.tracks[0].clips[0].warp.markers).toEqual(markers);
    });

    it('should pass through non-serialized state', () => {
      const state = { tracks: [], timing: {} };
      const result = maybeDeserializeProjectState(
        state as any as StudioProjectState
      );

      expect(result).toBe(state); // Should be the same reference
    });
  });

  describe('maybeDeserializeProjectStateInPlace', () => {
    it('should mutate the original object', () => {
      const markers = { 0: 0, 1: 1 };
      const clip = createClip('clip1', markers);
      const track = createTrack('track1', [clip]);
      const state = { tracks: [track] };

      const serialized = serializeProjectState(state);
      const originalRef = serialized;

      const result = maybeDeserializeProjectStateInPlace(serialized);

      // Should mutate in place
      expect(result).toBe(originalRef);

      // markersRegistry should be removed
      expect('markersRegistry' in result).toBe(false);

      // Markers should be restored
      expect(result.tracks[0].clips[0].warp.markers).toEqual(markers);
    });

    it('should pass through non-serialized state unchanged', () => {
      const state = { tracks: [], timing: {} };
      const originalRef = state;

      const result = maybeDeserializeProjectStateInPlace(
        state as any as StudioProjectState
      );

      expect(result).toBe(originalRef);
    });
  });
});
