import { describe, it, expect } from 'vitest';
import { decodeWav, encodeWav } from './wav';

/** Build a minimal, valid WAV byte buffer by hand for decode tests. */
function buildWav(opts: {
  audioFormat: number;
  numChannels: number;
  sampleRate: number;
  bitsPerSample: number;
  /** Raw bytes for the data chunk body. */
  data: Uint8Array;
  /** When true, write the data chunk before the fmt chunk. */
  dataFirst?: boolean;
  /** Extra unknown chunks (id must be 4 chars) inserted after fmt. */
  extraChunks?: { id: string; body: Uint8Array }[];
}): Uint8Array {
  const { audioFormat, numChannels, sampleRate, bitsPerSample, data } = opts;
  const blockAlign = (numChannels * bitsPerSample) / 8;
  const byteRate = sampleRate * blockAlign;

  const fmtBody = new Uint8Array(16);
  const fmtView = new DataView(fmtBody.buffer);
  fmtView.setUint16(0, audioFormat, true);
  fmtView.setUint16(2, numChannels, true);
  fmtView.setUint32(4, sampleRate, true);
  fmtView.setUint32(8, byteRate, true);
  fmtView.setUint16(12, blockAlign, true);
  fmtView.setUint16(14, bitsPerSample, true);

  type Chunk = { id: string; body: Uint8Array };
  const fmtChunk: Chunk = { id: 'fmt ', body: fmtBody };
  const dataChunk: Chunk = { id: 'data', body: data };

  const chunks: Chunk[] = [];
  if (opts.dataFirst) {
    chunks.push(dataChunk, fmtChunk);
  } else {
    chunks.push(fmtChunk);
    if (opts.extraChunks) chunks.push(...opts.extraChunks);
    chunks.push(dataChunk);
  }

  // Compute total size: 12-byte descriptor + each chunk (8-byte header + body
  // + pad byte for odd-length bodies).
  let bodyTotal = 0;
  for (const c of chunks) {
    bodyTotal += 8 + c.body.length + (c.body.length % 2);
  }
  const total = 12 + bodyTotal;

  const out = new Uint8Array(total);
  const view = new DataView(out.buffer);
  const writeTag = (off: number, tag: string) => {
    for (let i = 0; i < 4; i++) view.setUint8(off + i, tag.charCodeAt(i));
  };

  writeTag(0, 'RIFF');
  view.setUint32(4, total - 8, true);
  writeTag(8, 'WAVE');

  let off = 12;
  for (const c of chunks) {
    writeTag(off, c.id);
    view.setUint32(off + 4, c.body.length, true);
    out.set(c.body, off + 8);
    off += 8 + c.body.length;
    if (c.body.length % 2 === 1) off += 1; // pad byte (already zero)
  }

  return out;
}

describe('encodeWav / decodeWav round-trip', () => {
  it('round-trips mono 16 kHz samples within int16 tolerance', () => {
    const n = 2048;
    const input = new Float32Array(n);
    for (let i = 0; i < n; i++) {
      input[i] = Math.sin((2 * Math.PI * 440 * i) / 16000) * 0.9;
    }

    const encoded = encodeWav(input, 16000);
    const decoded = decodeWav(encoded);

    expect(decoded.sampleRate).toBe(16000);
    expect(decoded.channelData.length).toBe(1);

    const out = decoded.channelData[0]!;
    expect(out.length).toBe(n);
    for (let i = 0; i < n; i++) {
      expect(Math.abs(out[i]! - input[i]!)).toBeLessThanOrEqual(1 / 32768);
    }
  });

  it('produces a 44-byte header + 2 bytes per sample', () => {
    const input = new Float32Array([0, 0.5, -0.5, 1, -1]);
    const encoded = encodeWav(input, 16000);
    expect(encoded.byteLength).toBe(44 + input.length * 2);
  });

  it('clamps out-of-range samples to [-1, 1]', () => {
    const input = new Float32Array([2, -2, 5, -9]);
    const decoded = decodeWav(encodeWav(input, 16000));
    const out = decoded.channelData[0]!;
    expect(out[0]!).toBeCloseTo(1, 4);
    expect(out[1]!).toBeCloseTo(-1, 4);
    expect(out[2]!).toBeCloseTo(1, 4);
    expect(out[3]!).toBeCloseTo(-1, 4);
  });

  it('handles an empty sample buffer', () => {
    const decoded = decodeWav(encodeWav(new Float32Array(0), 16000));
    expect(decoded.sampleRate).toBe(16000);
    expect(decoded.channelData.length).toBe(1);
    expect(decoded.channelData[0]!.length).toBe(0);
  });

  it('preserves a non-16k sample rate', () => {
    const decoded = decodeWav(encodeWav(new Float32Array([0.1, -0.1]), 44100));
    expect(decoded.sampleRate).toBe(44100);
  });
});

describe('decodeWav stereo de-interleaving', () => {
  it('splits an interleaved 16-bit stereo data chunk into 2 channels', () => {
    // 3 frames; L = {100, 300, 500}, R = {-100, -300, -500} (int16 values).
    const lVals = [100, 300, 500];
    const rVals = [-100, -300, -500];
    const data = new Uint8Array(3 * 2 * 2);
    const dv = new DataView(data.buffer);
    for (let f = 0; f < 3; f++) {
      dv.setInt16(f * 4, lVals[f]!, true);
      dv.setInt16(f * 4 + 2, rVals[f]!, true);
    }

    const wav = buildWav({
      audioFormat: 1,
      numChannels: 2,
      sampleRate: 16000,
      bitsPerSample: 16,
      data,
    });

    const decoded = decodeWav(wav);
    expect(decoded.sampleRate).toBe(16000);
    expect(decoded.channelData.length).toBe(2);

    const left = decoded.channelData[0]!;
    const right = decoded.channelData[1]!;
    expect(left.length).toBe(3);
    expect(right.length).toBe(3);

    for (let f = 0; f < 3; f++) {
      expect(left[f]!).toBeCloseTo(lVals[f]! / 0x7fff, 5);
      expect(right[f]!).toBeCloseTo(rVals[f]! / 0x8000, 5);
    }
  });

  it('decodes chunks in any order (data before fmt)', () => {
    const data = new Uint8Array(4);
    const dv = new DataView(data.buffer);
    dv.setInt16(0, 16383, true); // ~0.5
    dv.setInt16(2, -16384, true); // -0.5
    const wav = buildWav({
      audioFormat: 1,
      numChannels: 1,
      sampleRate: 8000,
      bitsPerSample: 16,
      data,
      dataFirst: true,
    });

    const decoded = decodeWav(wav);
    expect(decoded.sampleRate).toBe(8000);
    expect(decoded.channelData.length).toBe(1);
    expect(decoded.channelData[0]![0]!).toBeCloseTo(16383 / 0x7fff, 4);
    expect(decoded.channelData[0]![1]!).toBeCloseTo(-16384 / 0x8000, 4);
  });

  it('skips unknown chunks (including odd-length ones)', () => {
    const data = new Uint8Array(2);
    new DataView(data.buffer).setInt16(0, 12345, true);
    const wav = buildWav({
      audioFormat: 1,
      numChannels: 1,
      sampleRate: 16000,
      bitsPerSample: 16,
      data,
      extraChunks: [
        { id: 'LIST', body: new Uint8Array([1, 2, 3]) }, // odd length -> pad byte
        { id: 'fact', body: new Uint8Array([0, 0, 0, 0]) },
      ],
    });

    const decoded = decodeWav(wav);
    expect(decoded.channelData.length).toBe(1);
    expect(decoded.channelData[0]!.length).toBe(1);
    expect(decoded.channelData[0]![0]!).toBeCloseTo(12345 / 0x7fff, 4);
  });

  it('decodes 32-bit IEEE float (format 3) data', () => {
    // mono, 2 samples: 0.25 and -0.75
    const data = new Uint8Array(8);
    const dv = new DataView(data.buffer);
    dv.setFloat32(0, 0.25, true);
    dv.setFloat32(4, -0.75, true);
    const wav = buildWav({
      audioFormat: 3,
      numChannels: 1,
      sampleRate: 16000,
      bitsPerSample: 32,
      data,
    });

    const decoded = decodeWav(wav);
    expect(decoded.channelData.length).toBe(1);
    expect(decoded.channelData[0]![0]!).toBeCloseTo(0.25, 6);
    expect(decoded.channelData[0]![1]!).toBeCloseTo(-0.75, 6);
  });
});

describe('decodeWav error handling', () => {
  it('throws on a tiny non-WAVE buffer', () => {
    expect(() => decodeWav(new Uint8Array([1, 2, 3]))).toThrow();
  });

  it("throws when 'RIFF' magic is missing", () => {
    const wav = buildWav({
      audioFormat: 1,
      numChannels: 1,
      sampleRate: 16000,
      bitsPerSample: 16,
      data: new Uint8Array([0, 0]),
    });
    wav[0] = 'X'.charCodeAt(0);
    expect(() => decodeWav(wav)).toThrow(/RIFF/);
  });

  it("throws when the form is not 'WAVE'", () => {
    const wav = buildWav({
      audioFormat: 1,
      numChannels: 1,
      sampleRate: 16000,
      bitsPerSample: 16,
      data: new Uint8Array([0, 0]),
    });
    wav[8] = 'A'.charCodeAt(0);
    wav[9] = 'V'.charCodeAt(0);
    wav[10] = 'I'.charCodeAt(0);
    wav[11] = ' '.charCodeAt(0);
    expect(() => decodeWav(wav)).toThrow(/WAVE/);
  });

  it('throws on an unsupported sample format (8-bit PCM)', () => {
    const wav = buildWav({
      audioFormat: 1,
      numChannels: 1,
      sampleRate: 16000,
      bitsPerSample: 8,
      data: new Uint8Array([0, 0]),
    });
    expect(() => decodeWav(wav)).toThrow(/Unsupported/);
  });

  it("throws when the 'data' chunk is missing", () => {
    // Build a RIFF/WAVE with only a fmt chunk.
    const fmtBody = new Uint8Array(16);
    const fmtView = new DataView(fmtBody.buffer);
    fmtView.setUint16(0, 1, true);
    fmtView.setUint16(2, 1, true);
    fmtView.setUint32(4, 16000, true);
    fmtView.setUint32(8, 32000, true);
    fmtView.setUint16(12, 2, true);
    fmtView.setUint16(14, 16, true);

    const total = 12 + 8 + 16;
    const out = new Uint8Array(total);
    const view = new DataView(out.buffer);
    const writeTag = (off: number, tag: string) => {
      for (let i = 0; i < 4; i++) view.setUint8(off + i, tag.charCodeAt(i));
    };
    writeTag(0, 'RIFF');
    view.setUint32(4, total - 8, true);
    writeTag(8, 'WAVE');
    writeTag(12, 'fmt ');
    view.setUint32(16, 16, true);
    out.set(fmtBody, 20);

    expect(() => decodeWav(out)).toThrow(/data/);
  });
});
