import { describe, expect, it } from 'vitest';
import {
  aggregatePcmPeaks,
  decodePcmSamples,
  MAX_WAVEFORM_BUCKET_COUNT,
  WaveformCancellationError,
  WaveformDataError,
} from '../../src/waveform';

describe('waveform PCM aggregation', () => {
  it('uses deterministic bucket boundaries for signed 16-bit PCM', () => {
    const samples = new Int16Array([
      -32_768, -16_384, 0, 16_384, 32_767, 8_192, -8_192, 0,
    ]);

    const peaks = aggregatePcmPeaks(
      {
        container: 'raw',
        data: samples,
        encoding: 's16le',
        sampleRate: 8_000,
      },
      { bucketCount: 4 }
    );

    expect(peaks.sampleCount).toBe(8);
    expect(peaks.bucketCount).toBe(4);
    expect([...peaks.minimum]).toEqual([-1, 0, 0.25, -0.25]);
    expect([...peaks.maximum]).toEqual([-0.5, 0.5, 32_767 / 32_768, 0]);
    expect([...peaks.rms]).toEqual(
      expect.arrayContaining([
        expect.closeTo(Math.sqrt(0.625), 6),
        expect.closeTo(Math.sqrt(0.125), 6),
        expect.closeTo(Math.sqrt(((32_767 / 32_768) ** 2 + 0.25 ** 2) / 2), 6),
        expect.closeTo(Math.sqrt(0.03125), 6),
      ])
    );
  });

  it('never creates empty buckets when fewer samples than requested', () => {
    const peaks = aggregatePcmPeaks(
      {
        container: 'raw',
        data: float32Bytes([0.1, -0.2, 0.3]),
        encoding: 'f32le',
        sampleRate: 10,
      },
      { bucketCount: 100 }
    );

    expect(peaks.bucketCount).toBe(3);
    expect([...peaks.minimum]).toEqual([
      expect.closeTo(0.1, 6),
      expect.closeTo(-0.2, 6),
      expect.closeTo(0.3, 6),
    ]);
    expect([...peaks.maximum]).toEqual([...peaks.minimum]);
  });

  it('assigns every sample exactly once when bucket sizes are uneven', () => {
    const peaks = aggregatePcmPeaks(
      {
        container: 'raw',
        data: new Int16Array([
          1_000, 2_000, 3_000, -4_000, -5_000, -6_000, 7_000, 8_000, 9_000,
          10_000,
        ]),
        encoding: 's16le',
        sampleRate: 10,
      },
      { bucketCount: 3 }
    );

    expect([...peaks.minimum]).toEqual([
      1_000 / 32_768,
      -6_000 / 32_768,
      7_000 / 32_768,
    ]);
    expect([...peaks.maximum]).toEqual([
      3_000 / 32_768,
      -4_000 / 32_768,
      10_000 / 32_768,
    ]);
  });

  it('handles finite float PCM above full scale without RMS overflow', () => {
    const maximumFloat = 3.4028234663852886e38;
    const peaks = aggregatePcmPeaks(
      {
        container: 'raw',
        data: float32Bytes([maximumFloat, -maximumFloat]),
        encoding: 'f32le',
        sampleRate: 8_000,
      },
      { bucketCount: 1 }
    );

    expect(peaks.minimum[0]).toBe(-maximumFloat);
    expect(peaks.maximum[0]).toBe(maximumFloat);
    expect(peaks.rms[0]).toBe(maximumFloat);
    expect(Number.isFinite(peaks.rms[0])).toBe(true);
  });

  it('rejects malformed, non-finite, and resource-exceeding PCM', () => {
    expect(() =>
      aggregatePcmPeaks(
        {
          container: 'raw',
          data: new Uint8Array([0]),
          encoding: 's16le',
          sampleRate: 8_000,
        },
        { bucketCount: 1 }
      )
    ).toThrow(WaveformDataError);

    expect(() =>
      aggregatePcmPeaks(
        {
          container: 'raw',
          data: float32Bytes([Number.NaN]),
          encoding: 'f32le',
          sampleRate: 8_000,
        },
        { bucketCount: 1 }
      )
    ).toThrow(/not finite/u);

    expect(() =>
      aggregatePcmPeaks(
        {
          container: 'raw',
          data: new Int16Array([1, 2, 3]),
          encoding: 's16le',
          sampleRate: 8_000,
        },
        { bucketCount: 1, maxSamples: 2 }
      )
    ).toThrow(/sample analysis limit/u);

    expect(() =>
      aggregatePcmPeaks(
        {
          container: 'raw',
          data: new Int16Array([1]),
          encoding: 's16le',
          sampleRate: 8_000,
        },
        { bucketCount: MAX_WAVEFORM_BUCKET_COUNT + 1 }
      )
    ).toThrow(RangeError);
  });

  it('parses FFmpeg-style mono RIFF/WAVE PCM and rejects stereo', () => {
    const wav = createWaveFile({
      samples: new Int16Array([-32_768, 0, 16_384, 32_767]),
      sampleRate: 8_000,
      channels: 1,
      addOddJunkChunk: true,
    });

    const decoded = decodePcmSamples({ container: 'wav', data: wav });
    expect(decoded.encoding).toBe('s16le');
    expect(decoded.sampleRate).toBe(8_000);
    expect([...decoded.samples]).toEqual([-1, 0, 0.5, 32_767 / 32_768]);

    const stereo = createWaveFile({
      samples: new Int16Array([1, 2]),
      sampleRate: 8_000,
      channels: 2,
    });
    expect(() => decodePcmSamples({ container: 'wav', data: stereo })).toThrow(
      /requires mono/u
    );
  });

  it('parses little-endian float PCM from a sliced WAVE buffer', () => {
    const wav = createFloatWaveFile([1.25, -0.5], 16_000);
    const enclosing = new Uint8Array(wav.byteLength + 6);
    enclosing.set(wav, 3);
    const sliced = enclosing.subarray(3, 3 + wav.byteLength);

    const decoded = decodePcmSamples({ container: 'wav', data: sliced });
    expect(decoded.encoding).toBe('f32le');
    expect(decoded.sampleRate).toBe(16_000);
    expect([...decoded.samples]).toEqual([1.25, -0.5]);
  });

  it('rejects truncated WAVE chunks', () => {
    const wav = createWaveFile({
      samples: new Int16Array([1, 2]),
      sampleRate: 8_000,
      channels: 1,
    });
    expect(() =>
      decodePcmSamples({
        container: 'wav',
        data: wav.subarray(0, wav.byteLength - 1),
      })
    ).toThrow(/truncated|invalid size/u);
  });

  it('supports cooperative cancellation and bounded progress callbacks', () => {
    const pcm = new Int16Array(40_000);
    let cancelled = false;
    const progress: number[] = [];

    expect(() =>
      aggregatePcmPeaks(
        {
          container: 'raw',
          data: pcm,
          encoding: 's16le',
          sampleRate: 8_000,
        },
        {
          bucketCount: 100,
          isCancelled: () => cancelled,
          onProgress: (value) => {
            progress.push(value);
            cancelled = true;
          },
        }
      )
    ).toThrow(WaveformCancellationError);
    expect(progress[0]).toBe(0);
    expect(progress.every((value) => value >= 0 && value <= 1)).toBe(true);
  });

  it('reports completion for empty PCM without allocating peak buckets', () => {
    const progress: number[] = [];
    const peaks = aggregatePcmPeaks(
      {
        container: 'raw',
        data: new Uint8Array(),
        encoding: 's16le',
        sampleRate: 8_000,
      },
      { bucketCount: 10, onProgress: (value) => progress.push(value) }
    );

    expect(peaks.bucketCount).toBe(0);
    expect(peaks.durationSeconds).toBe(0);
    expect(progress).toEqual([1]);
  });
});

function float32Bytes(values: readonly number[]): Uint8Array {
  const bytes = new Uint8Array(values.length * 4);
  const view = new DataView(bytes.buffer);
  values.forEach((value, index) => view.setFloat32(index * 4, value, true));
  return bytes;
}

function createWaveFile(options: {
  readonly samples: Int16Array;
  readonly sampleRate: number;
  readonly channels: number;
  readonly addOddJunkChunk?: boolean;
}): Uint8Array {
  const junkBytes = options.addOddJunkChunk ? 10 : 0;
  const result = new Uint8Array(44 + junkBytes + options.samples.byteLength);
  const view = new DataView(result.buffer);
  writeAscii(result, 0, 'RIFF');
  view.setUint32(4, result.byteLength - 8, true);
  writeAscii(result, 8, 'WAVE');
  writeAscii(result, 12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, options.channels, true);
  view.setUint32(24, options.sampleRate, true);
  view.setUint32(
    28,
    options.sampleRate * options.channels * Int16Array.BYTES_PER_ELEMENT,
    true
  );
  view.setUint16(32, options.channels * Int16Array.BYTES_PER_ELEMENT, true);
  view.setUint16(34, 16, true);
  let dataHeaderOffset = 36;
  if (options.addOddJunkChunk) {
    writeAscii(result, 36, 'JUNK');
    view.setUint32(40, 1, true);
    result[44] = 0x2a;
    result[45] = 0;
    dataHeaderOffset += junkBytes;
  }
  writeAscii(result, dataHeaderOffset, 'data');
  view.setUint32(dataHeaderOffset + 4, options.samples.byteLength, true);
  const sampleOffset = dataHeaderOffset + 8;
  for (let index = 0; index < options.samples.length; index += 1) {
    view.setInt16(
      sampleOffset + index * Int16Array.BYTES_PER_ELEMENT,
      options.samples[index] ?? 0,
      true
    );
  }
  return result;
}

function createFloatWaveFile(
  samples: readonly number[],
  sampleRate: number
): Uint8Array {
  const result = new Uint8Array(44 + samples.length * 4);
  const view = new DataView(result.buffer);
  writeAscii(result, 0, 'RIFF');
  view.setUint32(4, result.byteLength - 8, true);
  writeAscii(result, 8, 'WAVE');
  writeAscii(result, 12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, 3, true);
  view.setUint16(22, 1, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, sampleRate * 4, true);
  view.setUint16(32, 4, true);
  view.setUint16(34, 32, true);
  writeAscii(result, 36, 'data');
  view.setUint32(40, samples.length * 4, true);
  samples.forEach((sample, index) =>
    view.setFloat32(44 + index * 4, sample, true)
  );
  return result;
}

function writeAscii(target: Uint8Array, offset: number, text: string): void {
  for (let index = 0; index < text.length; index += 1) {
    target[offset + index] = text.charCodeAt(index);
  }
}
