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

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

import { AlignmentMatch } from './findAlignmentMatches';
import getGridOffsetBeats, {
  alignEndGridOffset,
  alignStartGridOffset,
} from './getGridOffsetBeats';
import { createTestClip, createTestStudioClip } from './testHelpers';
import { StudioClip } from './types';

describe('getGridOffsetBeats', () => {
  it('returns 0 when startBeats equals readStartBeats', () => {
    const studioClip = createTestStudioClip({
      startBeats: 2,
      readStartBeats: 2,
    });
    expect(getGridOffsetBeats(studioClip)).toBe(0);
  });

  it('returns positive offset when startBeats is after readStartBeats', () => {
    const studioClip = createTestStudioClip({
      startBeats: 2.25,
      readStartBeats: 2,
    });
    expect(getGridOffsetBeats(studioClip)).toBe(0.25);
  });

  it('returns negative offset when startBeats is before readStartBeats', () => {
    const studioClip = createTestStudioClip({
      startBeats: 1.75,
      readStartBeats: 2,
    });
    expect(getGridOffsetBeats(studioClip)).toBe(-0.25);
  });

  it('normalizes large positive offsets to negative', () => {
    const studioClip = createTestStudioClip({
      startBeats: 2.75,
      readStartBeats: 2,
    });
    expect(getGridOffsetBeats(studioClip)).toBe(-0.25);
  });

  it('normalizes large negative offsets to positive', () => {
    const studioClip = createTestStudioClip({
      startBeats: 1.25,
      readStartBeats: 2,
    });
    expect(getGridOffsetBeats(studioClip)).toBe(0.25);
  });

  it('handles offsets crossing beat boundaries', () => {
    const studioClip = createTestStudioClip({
      startBeats: 3.75,
      readStartBeats: 2,
    });
    expect(getGridOffsetBeats(studioClip)).toBe(-0.25);
  });
});

describe('alignStartGridOffset', () => {
  let clip: Clip;
  let studioClip: StudioClip;

  beforeEach(() => {
    clip = createTestClip({
      metadata: {
        task: 'generate',
        duration: 4,
      },
    });
    studioClip = createTestStudioClip({
      startBeats: 2,
      readStartBeats: 2,
      warp: {
        awaitingAnalysis: false,
        speed: 1,
        enabled: true,
        markers: {
          '0': 0,
          '4': 4,
        },
      },
    });
  });

  it('returns unchanged studioClip when target offset equals current offset', () => {
    const result = alignStartGridOffset(0, studioClip, clip);
    expect(result).toEqual(studioClip);
  });

  it('adjusts readStartBeats to match target offset', () => {
    const result = alignStartGridOffset(0.25, studioClip, clip);
    expect(result).toEqual({
      ...studioClip,
      readStartBeats: 1.75,
    });
  });

  it('handles negative target offsets', () => {
    const result = alignStartGridOffset(-0.25, studioClip, clip);
    expect(result).toEqual({
      ...studioClip,
      readStartBeats: 2.25,
    });
  });

  it('wraps around when adjustment would exceed clip bounds', () => {
    const result = alignStartGridOffset(0.75, studioClip, clip);
    expect(result).toEqual({
      ...studioClip,
      readStartBeats: 1.25,
    });
  });

  it('returns unchanged studioClip when adjustment would exceed clip bounds and wrapping is not possible', () => {
    // Set up a clip that's too short to allow wrapping
    const shortClip = createTestClip({
      metadata: {
        task: 'generate',
        duration: 0.5,
      },
    });
    const shortStudioClip = createTestStudioClip({
      startBeats: 2,
      readStartBeats: 0.25,
      warp: {
        awaitingAnalysis: false,
        speed: 1,
        enabled: true,
        markers: {
          '0': 0,
          '1': 1,
        },
      },
    });

    const result = alignStartGridOffset(0.4, shortStudioClip, shortClip);
    expect(result).toEqual(shortStudioClip);
  });

  it('handles edge case where adjustment would put readStartBeats exactly at clip boundary', () => {
    const result = alignStartGridOffset(0.5, studioClip, clip);
    expect(result).toEqual({
      ...studioClip,
      readStartBeats: 1.5,
    });
  });

  it('preserves other studioClip properties when adjusting readStartBeats', () => {
    const customStudioClip = createTestStudioClip({
      startBeats: 2,
      readStartBeats: 2,
      color: '#FF0000',
      amplitude: 0.8,
      transposition: 2,
      warp: {
        awaitingAnalysis: false,
        speed: 1,
        enabled: true,
        markers: {
          '0': 0,
          '4': 4,
        },
      },
    });

    const result = alignStartGridOffset(0.25, customStudioClip, clip);
    expect(result).toEqual({
      ...customStudioClip,
      readStartBeats: 1.75,
    });
  });
});

describe('alignEndGridOffset', () => {
  let clip: Clip;
  let studioClip: StudioClip;
  let match: AlignmentMatch;

  beforeEach(() => {
    clip = createTestClip({
      metadata: {
        task: 'generate',
        duration: 8,
      },
    });
    studioClip = createTestStudioClip({
      startBeats: 2,
      endBeats: 4,
      readStartBeats: 2,
      warp: {
        awaitingAnalysis: false,
        speed: 1,
        enabled: true,
        markers: {
          '0': 0,
          '4': 4,
        },
      },
    });
    match = {
      replacementStartBeats: 2,
      replacementEndBeats: 4,
      historyClipGridOffset: 0.25,
      futureClipGridOffset: 0.5,
    };
  });

  it('returns unchanged studioClip when futureClipGridOffset is null', () => {
    const result = alignEndGridOffset(
      { ...match, futureClipGridOffset: null },
      studioClip,
      clip
    );
    expect(result).toEqual(studioClip);
  });

  it('adjusts endBeats to match target offset when future clip has different offset', () => {
    const result = alignEndGridOffset(match, studioClip, clip);
    expect(result).toEqual({
      ...studioClip,
      endBeats: 3.75, // Adjusted to match history clip's 0.25 offset
    });
  });

  it('handles negative grid offset changes', () => {
    const result = alignEndGridOffset(
      {
        ...match,
        historyClipGridOffset: 0.5,
        futureClipGridOffset: 0.25,
      },
      studioClip,
      clip
    );
    expect(result).toEqual({
      ...studioClip,
      endBeats: 4.25, // Adjusted to match history clip's 0.5 offset
    });
  });

  it('handles large positive grid offset changes by wrapping around', () => {
    const result = alignEndGridOffset(
      {
        ...match,
        historyClipGridOffset: 0.75,
        futureClipGridOffset: 0.25,
      },
      studioClip,
      clip
    );
    expect(result).toEqual({
      ...studioClip,
      endBeats: 4.5, // Adjusted to match history clip's 0.75 offset
    });
  });

  it('handles large negative grid offset changes by wrapping around', () => {
    const result = alignEndGridOffset(
      {
        ...match,
        historyClipGridOffset: 0.25,
        futureClipGridOffset: 0.75,
      },
      studioClip,
      clip
    );
    expect(result).toEqual({
      ...studioClip,
      endBeats: 3.5, // Adjusted to match history clip's 0.25 offset
    });
  });

  it('returns unchanged studioClip when adjustment would exceed clip bounds and wrapping is not possible', () => {
    // Set up a clip that's too short to allow wrapping
    const shortClip = createTestClip({
      metadata: {
        task: 'generate',
        duration: 0.5,
      },
    });
    const shortStudioClip = createTestStudioClip({
      startBeats: 2,
      endBeats: 2.25,
      readStartBeats: 0.25,
      warp: {
        awaitingAnalysis: false,
        speed: 1,
        enabled: true,
        markers: {
          '0': 0,
          '1': 1,
        },
      },
    });

    const result = alignEndGridOffset(
      {
        ...match,
        replacementStartBeats: 2,
        replacementEndBeats: 2.5,
      },
      shortStudioClip,
      shortClip
    );
    expect(result).toEqual(shortStudioClip);
  });

  it('preserves other studioClip properties when adjusting endBeats', () => {
    const customStudioClip = createTestStudioClip({
      startBeats: 2,
      endBeats: 4,
      readStartBeats: 2,
      color: '#FF0000',
      amplitude: 0.8,
      transposition: 2,
      warp: {
        awaitingAnalysis: false,
        speed: 1,
        enabled: true,
        markers: {
          '0': 0,
          '4': 4,
        },
      },
    });

    const result = alignEndGridOffset(match, customStudioClip, clip);
    expect(result).toEqual({
      ...customStudioClip,
      endBeats: 3.75,
    });
  });

  it('handles edge case where adjustment would put readEndBeats exactly at clip boundary', () => {
    const result = alignEndGridOffset(
      {
        ...match,
        historyClipGridOffset: 0.5,
        futureClipGridOffset: 0,
      },
      studioClip,
      clip
    );
    expect(result).toEqual({
      ...studioClip,
      endBeats: 4.5,
    });
  });
});
