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

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

import { getTimelineInsertionSpecs } from './insertion';
import { applyKnownTrackInsertionSpec } from './insertion';
import {
  createTestAlignmentSpec,
  createTestClip,
  createTestProjectState,
  createTestStudioClip,
  createTestTrack,
} from './testHelpers';
import type { ClipArrangementPackage } from './types';
import type { StudioClip, StudioProjectState } from './types';

describe('getTimelineInsertionSpecs', () => {
  let clip: Clip;
  let studioClip: StudioClip;
  let state: StudioProjectState;
  let arrangementPackage: ClipArrangementPackage;

  beforeEach(() => {
    clip = createTestClip();
    studioClip = createTestStudioClip();
    state = createTestProjectState();
    arrangementPackage = {
      clip,
      studioClip,
      alignmentSpec: undefined,
    };
  });

  it('returns empty array when no tracks exist', async () => {
    arrangementPackage.alignmentSpec = createTestAlignmentSpec({
      historyClipId: 'test-clip-1',
      historyEndSeconds: 2,
    });
    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toEqual([]);
  });

  it('returns empty array when no matches found in tracks', async () => {
    arrangementPackage.alignmentSpec = createTestAlignmentSpec({
      historyClipId: 'test-clip-1',
      historyEndSeconds: 2,
    });
    const track = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'different-clip-id',
          startBeats: 0,
          endBeats: 4,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [track] });

    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toEqual([]);
  });

  it('returns empty array when alignment spec is undefined', async () => {
    const track = createTestTrack();
    state = createTestProjectState({ tracks: [track] });

    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toEqual([]);
  });

  it('returns insertion specs for history-only alignment', async () => {
    arrangementPackage.alignmentSpec = createTestAlignmentSpec({
      historyClipId: 'test-clip-1',
      historyEndSeconds: 2,
    });
    const track = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'test-clip-1',
          startBeats: 0,
          endBeats: 4,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [track] });

    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toHaveLength(1);
    expect(result[0]).toEqual({
      replacementStartBeats: 2,
      replacementEndBeats: Infinity,
      trackId: track.id,
      clip: clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 6,
      },
    });
  });

  it('handles multiple matches with history-only alignment', async () => {
    arrangementPackage.alignmentSpec = createTestAlignmentSpec({
      historyClipId: 'test-clip-1',
      historyEndSeconds: 2,
    });
    const track1 = createTestTrack({
      id: 'track-1',
      clips: [
        createTestStudioClip({
          clipId: 'test-clip-1',
          startBeats: 0,
          endBeats: 4,
        }),
      ],
    });
    const track2 = createTestTrack({
      id: 'track-2',
      clips: [
        createTestStudioClip({
          clipId: 'test-clip-1',
          startBeats: 8,
          endBeats: 12,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [track1, track2] });

    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toHaveLength(2);
    expect(result).toEqual([
      {
        replacementStartBeats: 2,
        replacementEndBeats: Infinity,
        trackId: track1.id,
        clip: clip,
        studioClip: {
          ...studioClip,
          clipId: clip.id,
          startBeats: 2,
          endBeats: 6,
        },
      },
      {
        replacementStartBeats: 10,
        replacementEndBeats: Infinity,
        trackId: track2.id,
        clip: clip,
        studioClip: {
          ...studioClip,
          clipId: clip.id,
          startBeats: 10,
          endBeats: 14,
        },
      },
    ]);
  });

  it('handles future-only alignment', async () => {
    arrangementPackage.alignmentSpec = createTestAlignmentSpec({
      futureClipId: 'test-clip-1',
      futureStartSeconds: 2,
    });
    const track = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'test-clip-1',
          startBeats: 4,
          endBeats: 8,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [track] });

    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toHaveLength(1);
    expect(result[0]).toEqual({
      replacementStartBeats: 2,
      replacementEndBeats: 6,
      trackId: track.id,
      clip: clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 4,
        endBeats: 8,
      },
    });
  });

  it('handles history-and-future alignment', async () => {
    arrangementPackage.alignmentSpec = createTestAlignmentSpec({
      historyClipId: 'test-clip-1',
      historyEndSeconds: 2,
      futureClipId: 'test-clip-2',
      futureStartSeconds: 2,
    });
    const track = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'test-clip-1',
          startBeats: 0,
          endBeats: 4,
        }),
        createTestStudioClip({
          clipId: 'test-clip-2',
          startBeats: 6,
          endBeats: 10,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [track] });

    const result = getTimelineInsertionSpecs(arrangementPackage, null, state);
    expect(result).toHaveLength(1);
    expect(result[0]).toEqual({
      replacementStartBeats: 2,
      replacementEndBeats: 8,
      trackId: track.id,
      clip: clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 6,
      },
    });
  });
});

describe('applyKnownTrackInsertionSpec', () => {
  let clip: Clip;
  let studioClip: StudioClip;
  let state: StudioProjectState;
  let track: ReturnType<typeof createTestTrack>;

  beforeEach(() => {
    clip = createTestClip();
    studioClip = createTestStudioClip();
    track = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'existing-clip-1',
          startBeats: 0,
          endBeats: 4,
        }),
        createTestStudioClip({
          clipId: 'existing-clip-2',
          startBeats: 6,
          endBeats: 10,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [track] });
  });

  it('replaces content in the specified time range', () => {
    const spec = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      trackId: track.id,
      clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 4,
      } as StudioClip,
    };

    const newState = applyKnownTrackInsertionSpec(spec)(state);
    const updatedTrack = newState.tracks.find((t) => t.id === track.id)!;

    expect(updatedTrack.clips).toHaveLength(3);
    expect(updatedTrack.clips[0]).toEqual({
      ...track.clips[0],
      endBeats: 2, // Truncated to end at replacement start
    });
    expect(updatedTrack.clips[1]).toEqual(spec.studioClip); // New clip inserted
    expect(updatedTrack.clips[2]).toEqual(track.clips[1]); // Second clip unchanged
  });

  it('moves content after insertion point when new clip has different duration', () => {
    const spec = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      trackId: track.id,
      clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 6, // Longer than replacement range
      } as StudioClip,
    };

    const newState = applyKnownTrackInsertionSpec(spec)(state);
    const updatedTrack = newState.tracks.find((t) => t.id === track.id)!;

    expect(updatedTrack.clips).toHaveLength(3);
    expect(updatedTrack.clips[0]).toEqual({
      ...track.clips[0],
      endBeats: 2, // Truncated to end at replacement start
    });
    expect(updatedTrack.clips[1]).toEqual(spec.studioClip); // New clip inserted
    expect(updatedTrack.clips[2]).toEqual({
      ...track.clips[1],
      startBeats: 8, // Moved by 2 beats (6-4)
      endBeats: 12,
    });
  });

  it('updates selection state to focus on inserted clip', () => {
    const spec = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      trackId: track.id,
      clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 4,
      } as StudioClip,
    };

    const newState = applyKnownTrackInsertionSpec(spec)(state);

    expect(newState.selection).toEqual({
      ...state.selection,
      focusedTrackId: track.id,
      focusedArea: 'timeline',
      focusBeats: 2,
      anchorBeats: 4,
      trackIds: [track.id],
    });
  });

  it('splits clip that contains replacement range', () => {
    const overlappingTrack = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'test-studio-clip-1',
          startBeats: 1,
          endBeats: 5,
          readStartBeats: 0,
          loop: {
            enabled: true,
            startBeats: 0,
            endBeats: 10,
          },
        }),
      ],
    });
    state = createTestProjectState({ tracks: [overlappingTrack] });

    const spec = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      trackId: overlappingTrack.id,
      clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 4,
      } as StudioClip,
    };

    const newState = applyKnownTrackInsertionSpec(spec)(state);
    const updatedTrack = newState.tracks.find(
      (t) => t.id === overlappingTrack.id
    )!;

    expect(updatedTrack.clips).toHaveLength(3);
    expect(updatedTrack.clips[0]).toEqual({
      ...overlappingTrack.clips[0],
      endBeats: 2, // First part of split clip
    });
    expect(updatedTrack.clips[1]).toEqual(spec.studioClip); // New inserted clip
    expect(updatedTrack.clips[2]).toEqual({
      ...overlappingTrack.clips[0],
      id: expect.any(String), // New UUID for second part
      clipId: 'test-studio-clip-1', // Original clip ID preserved
      startBeats: 4,
      endBeats: 5,
      readStartBeats: 3, // Original readStartBeats (0) + 3 beats offset
    });
  });

  it('handles multiple overlapping clips', () => {
    const multiTrack = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'clip-1',
          startBeats: 1,
          endBeats: 3, // Overlaps start of replacement
          readStartBeats: 0,
          loop: {
            enabled: true,
            startBeats: 0,
            endBeats: 10,
          },
        }),
        createTestStudioClip({
          clipId: 'clip-2',
          startBeats: 3,
          endBeats: 5, // Overlaps end of replacement
          readStartBeats: 0,
          loop: {
            enabled: true,
            startBeats: 0,
            endBeats: 10,
          },
        }),
      ],
    });
    state = createTestProjectState({ tracks: [multiTrack] });

    const spec = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      trackId: multiTrack.id,
      clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 4,
      } as StudioClip,
    };

    const newState = applyKnownTrackInsertionSpec(spec)(state);
    const updatedTrack = newState.tracks.find((t) => t.id === multiTrack.id)!;

    expect(updatedTrack.clips).toHaveLength(3);
    expect(updatedTrack.clips[0]).toEqual({
      ...multiTrack.clips[0],
      endBeats: 2, // Truncated at replacement start
    });
    expect(updatedTrack.clips[1]).toEqual(spec.studioClip);
    expect(updatedTrack.clips[2]).toEqual({
      ...multiTrack.clips[1],
      id: expect.any(String), // New UUID for second part
      startBeats: 4,
      endBeats: 5, // Original endBeats (5) - original startBeats (3) + new startBeats (4)
      readStartBeats: 1, // Original readStartBeats (0) + 1 beat offset
    });
  });

  it('preserves clip that starts exactly at replacement end', () => {
    const exactTrack = createTestTrack({
      clips: [
        createTestStudioClip({
          clipId: 'exact-clip',
          startBeats: 4, // Starts exactly at replacement end
          endBeats: 6,
        }),
      ],
    });
    state = createTestProjectState({ tracks: [exactTrack] });

    const spec = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      trackId: exactTrack.id,
      clip,
      studioClip: {
        ...studioClip,
        clipId: clip.id,
        startBeats: 2,
        endBeats: 4,
      } as StudioClip,
    };

    const newState = applyKnownTrackInsertionSpec(spec)(state);
    const updatedTrack = newState.tracks.find((t) => t.id === exactTrack.id)!;

    expect(updatedTrack.clips).toHaveLength(2);
    expect(updatedTrack.clips[0]).toEqual(spec.studioClip);
    expect(updatedTrack.clips[1]).toEqual(exactTrack.clips[0]); // Unchanged
  });
});
