import { describe, it, expect } from 'vitest';
import { resampleLinear, downmixToMono, toMono16k } from './resample';

describe('resampleLinear', () => {
  it('returns an equal-valued but distinct copy when fromRate === toRate', () => {
    const input = new Float32Array([0.1, -0.2, 0.3, 0.4]);
    const out = resampleLinear(input, 16000, 16000);

    expect(out).not.toBe(input); // different instance
    expect(Array.from(out)).toEqual(Array.from(input)); // same values
    // Mutating the copy must not affect the original.
    out[0] = 999;
    expect(input[0]).toBeCloseTo(0.1, 6);
  });

  it('returns empty for empty input regardless of rates', () => {
    expect(resampleLinear(new Float32Array(0), 8000, 16000).length).toBe(0);
    expect(resampleLinear(new Float32Array(0), 16000, 16000).length).toBe(0);
    expect(resampleLinear(new Float32Array(0), 44100, 16000).length).toBe(0);
  });

  it('upsamples 4 samples from 8000 to 16000 into 8 samples', () => {
    const input = new Float32Array([0, 1, 2, 3]);
    const out = resampleLinear(input, 8000, 16000);
    expect(out.length).toBe(8); // round(4 * 16000 / 8000) = 8
  });

  it('output length matches Math.round(len * toRate / fromRate) when downsampling', () => {
    const input = new Float32Array(10);
    const out = resampleLinear(input, 16000, 8000);
    expect(out.length).toBe(5); // round(10 * 8000 / 16000) = 5
  });

  it('keeps a linear ramp monotonic (non-decreasing) after upsampling', () => {
    const n = 16;
    const input = new Float32Array(n);
    for (let i = 0; i < n; i++) input[i] = i; // 0..15 ramp
    const out = resampleLinear(input, 8000, 16000);

    expect(out.length).toBe(32);
    for (let i = 1; i < out.length; i++) {
      expect(out[i]!).toBeGreaterThanOrEqual(out[i - 1]!);
    }
    // Linear interpolation of a perfect ramp preserves endpoints.
    expect(out[0]!).toBeCloseTo(0, 6);
  });

  it('linearly interpolates the midpoint correctly', () => {
    // [0, 10] upsampled x2 -> first output stays 0, then a value between 0 and 10.
    const input = new Float32Array([0, 10]);
    const out = resampleLinear(input, 8000, 16000);
    expect(out.length).toBe(4);
    expect(out[0]!).toBeCloseTo(0, 6);
    // srcPos for i=1 is 1 * (8000/16000) = 0.5 -> midpoint between 0 and 10.
    expect(out[1]!).toBeCloseTo(5, 6);
  });

  it('never reads past the end (clamps tail) when upsampling', () => {
    const input = new Float32Array([1, 2, 3]);
    const out = resampleLinear(input, 1000, 3000);
    // No NaN / undefined leakage at the tail.
    for (let i = 0; i < out.length; i++) {
      expect(Number.isFinite(out[i]!)).toBe(true);
    }
    expect(out[0]!).toBeCloseTo(1, 6);
  });

  it('handles a single-sample input', () => {
    const input = new Float32Array([0.42]);
    const out = resampleLinear(input, 8000, 16000);
    expect(out.length).toBe(2);
    for (let i = 0; i < out.length; i++) {
      expect(out[i]!).toBeCloseTo(0.42, 6);
    }
  });
});

describe('downmixToMono', () => {
  it('returns empty Float32Array for no channels', () => {
    const out = downmixToMono([]);
    expect(out).toBeInstanceOf(Float32Array);
    expect(out.length).toBe(0);
  });

  it('returns a distinct copy for a single channel', () => {
    const ch = new Float32Array([0.1, 0.2, 0.3]);
    const out = downmixToMono([ch]);
    expect(out).not.toBe(ch);
    expect(Array.from(out)).toEqual(Array.from(ch));
    out[0] = 999;
    expect(ch[0]).toBeCloseTo(0.1, 6);
  });

  it('averages two constant channels (1.0 and 0.0) to all 0.5', () => {
    const a = new Float32Array([1, 1, 1, 1]);
    const b = new Float32Array([0, 0, 0, 0]);
    const out = downmixToMono([a, b]);
    expect(out.length).toBe(4);
    for (let i = 0; i < out.length; i++) {
      expect(out[i]!).toBeCloseTo(0.5, 6);
    }
  });

  it('averages three channels sample-wise', () => {
    const a = new Float32Array([3, 6, 9]);
    const b = new Float32Array([0, 0, 0]);
    const c = new Float32Array([0, 3, 0]);
    const out = downmixToMono([a, b, c]);
    expect(out[0]!).toBeCloseTo(1, 6); // (3+0+0)/3
    expect(out[1]!).toBeCloseTo(3, 6); // (6+0+3)/3
    expect(out[2]!).toBeCloseTo(3, 6); // (9+0+0)/3
  });
});

describe('toMono16k', () => {
  it('downmixes then doubles the length when source is 8000 Hz', () => {
    const a = new Float32Array([1, 1, 1, 1]);
    const b = new Float32Array([0, 0, 0, 0]);
    const out = toMono16k([a, b], 8000);
    expect(out.length).toBe(8); // round(4 * 16000 / 8000) = 8
    // Constant 0.5 mono signal stays 0.5 after resampling.
    for (let i = 0; i < out.length; i++) {
      expect(out[i]!).toBeCloseTo(0.5, 6);
    }
  });

  it('returns empty for no channels', () => {
    expect(toMono16k([], 8000).length).toBe(0);
  });

  it('returns same length when source is already 16000 Hz', () => {
    const a = new Float32Array([0.2, 0.4, 0.6]);
    const out = toMono16k([a], 16000);
    expect(out.length).toBe(3);
    expect(Array.from(out)).toEqual(Array.from(a));
  });
});
