import { describe, expect, it } from 'vitest';
import { ARC, arcAt, arcIntensity, arcLabel, NIGHT_END_MIN, type ArcScales } from '../src/audio/arc';
import { VOICE_NAMES } from '../src/audio/mix';

const first = ARC[0]!;
const last = ARC[ARC.length - 1]!;

const mean = (a: number, b: number): number => (a + b) / 2;

const PLATEAU_MIN = 180;
const KICK_FULL_MIN = 120;

// The plateau is read straight off the keyframe, never sampled out of arcAt —
// a bound derived from the function under test cannot catch that function
// drifting. Kick is full by 11 PM by design; these three climb to midnight.
const plateau: ArcScales = ARC.find((k) => k.at === PLATEAU_MIN)!.scales;
const LATE_BUILDERS = ['hat', 'bass', 'pad'] as const;

describe('ARC keyframes', () => {
  // Every loop below iterates one of these two; a zero-length either would turn
  // the whole suite green by iterating nothing.
  it('has the keyframes and voices the rest of this suite loops over', () => {
    expect(ARC.length).toBe(7);
    expect(VOICE_NAMES.length).toBe(4);
    expect([...VOICE_NAMES].sort()).toEqual(['bass', 'hat', 'kick', 'pad']);
  });

  it('runs strictly ascending from minute 0 to the end of the night', () => {
    expect(first.at).toBe(0);
    expect(last.at).toBe(NIGHT_END_MIN);
    expect(NIGHT_END_MIN).toBe(360);
    for (let i = 1; i < ARC.length; i++) {
      expect(ARC[i]!.at).toBeGreaterThan(ARC[i - 1]!.at);
    }
  });

  it('keeps every voice scale inside 0..1 and carries a label', () => {
    for (const k of ARC) {
      expect(k.label.length).toBeGreaterThan(0);
      for (const v of VOICE_NAMES) {
        expect(k.scales[v]).toBeGreaterThanOrEqual(0);
        expect(k.scales[v]).toBeLessThanOrEqual(1);
      }
    }
  });
});

describe('arcAt', () => {
  it('returns the exact keyframe scales at each keyframe minute', () => {
    for (const k of ARC) {
      expect(arcAt(k.at)).toEqual(k.scales);
    }
  });

  // Every branch has to copy. Handing back a live keyframe means one caller
  // doing `arcAt(-1).bass = 0` silently rewrites ARC for the whole session, and
  // every subsequent 9 PM opens wrong.
  it('returns a fresh object on every branch, including both clamps', () => {
    // Clamp low.
    const low = arcAt(-1);
    expect(low).not.toBe(first.scales);
    low.bass = 0.123;
    expect(first.scales.bass).toBe(0);
    expect(arcAt(-1).bass).toBe(0);

    // Clamp high.
    const high = arcAt(NIGHT_END_MIN + 1);
    expect(high).not.toBe(last.scales);
    high.pad = 0.123;
    expect(last.scales.pad).toBe(0.6);
    expect(arcAt(NIGHT_END_MIN + 1).pad).toBe(0.6);

    // The edge minutes themselves take the clamp branches, not interpolation.
    const atFirst = arcAt(first.at);
    expect(atFirst).not.toBe(first.scales);
    atFirst.kick = 0.111;
    expect(arcAt(first.at).kick).toBe(0.8);

    const atLast = arcAt(last.at);
    expect(atLast).not.toBe(last.scales);
    atLast.kick = 0.111;
    expect(arcAt(last.at).kick).toBe(0.45);

    // A non-finite minute funnels into the clamp-low branch.
    const stalled = arcAt(NaN);
    expect(stalled).not.toBe(first.scales);
    stalled.bass = 0.9;
    expect(arcAt(NaN).bass).toBe(0);

    // Interpolation branch.
    const k = ARC[3]!;
    const s = arcAt(k.at);
    expect(s).not.toBe(k.scales);
    s.kick = 0;
    expect(k.scales.kick).toBe(1);
  });

  it('clamps outside the night instead of extrapolating', () => {
    expect(arcAt(-1)).toEqual(first.scales);
    expect(arcAt(-10_000)).toEqual(first.scales);
    expect(arcAt(NIGHT_END_MIN + 1)).toEqual(last.scales);
    expect(arcAt(10_000)).toEqual(last.scales);
  });

  it('never produces a negative or >1 gain at any minute, including past the end', () => {
    for (let t = -120; t <= NIGHT_END_MIN + 120; t++) {
      const s = arcAt(t);
      for (const v of VOICE_NAMES) {
        expect(s[v]).toBeGreaterThanOrEqual(0);
        expect(s[v]).toBeLessThanOrEqual(1);
      }
    }
  });

  it('interpolates linearly between keyframes', () => {
    // 30 is the midpoint of the 0 -> 60 span.
    const a = ARC[0]!.scales;
    const b = ARC[1]!.scales;
    const mid = arcAt(30);
    for (const v of VOICE_NAMES) expect(mid[v]).toBeCloseTo(mean(a[v], b[v]), 10);

    // 90 is the midpoint of 60 -> 120.
    const c = ARC[2]!.scales;
    const mid2 = arcAt(90);
    for (const v of VOICE_NAMES) expect(mid2[v]).toBeCloseTo(mean(b[v], c[v]), 10);

    // A quarter of the way across the 330 -> 360 close-down.
    const d = ARC[5]!.scales;
    const e = ARC[6]!.scales;
    const q = arcAt(337.5);
    for (const v of VOICE_NAMES) expect(q[v]).toBeCloseTo(d[v] + (e[v] - d[v]) * 0.25, 10);
  });

  it('survives a non-finite clock minute without emitting NaN', () => {
    for (const bad of [NaN, Infinity, -Infinity]) {
      const s: ArcScales = arcAt(bad);
      for (const v of VOICE_NAMES) expect(Number.isFinite(s[v])).toBe(true);
    }
    // A stalled clock reads as the start of the night, not as silence.
    expect(arcAt(NaN)).toEqual(first.scales);
  });
});

describe('the shape of the night', () => {
  it('opens with no bassline — the room is empty', () => {
    expect(arcAt(0).bass).toBe(0);
    expect(arcAt(0).pad).toBe(0);
    expect(arcAt(0).kick).toBeGreaterThan(0);
  });

  it('brings the bass up once there is a crowd to carry it', () => {
    expect(arcAt(60).bass).toBeGreaterThan(arcAt(0).bass);
    expect(arcAt(120).bass).toBeGreaterThan(arcAt(60).bass);
  });

  it('builds every voice through the first half of the night', () => {
    for (const v of VOICE_NAMES) {
      let prev = -Infinity;
      for (let t = 0; t <= 180; t += 5) {
        const s = arcAt(t)[v];
        expect(s).toBeGreaterThanOrEqual(prev);
        prev = s;
      }
      expect(arcAt(180)[v]).toBeGreaterThan(arcAt(0)[v]);
    }
  });

  // The build has to be a real build: strictly quieter than the plateau on the
  // way up, so a keyframe that arrives at full volume early — flattening the
  // climb the whole arc exists for — is a failure, not a pass.
  it('stays strictly under the midnight plateau on the way up', () => {
    for (const k of ARC) {
      if (k.at >= KICK_FULL_MIN) continue;
      for (const v of VOICE_NAMES) expect(k.scales[v]).toBeLessThan(plateau[v]);
    }

    const eleven = ARC.find((k) => k.at === KICK_FULL_MIN)!;
    expect(eleven.scales.kick).toBe(plateau.kick);
    for (const v of LATE_BUILDERS) expect(eleven.scales[v]).toBeLessThan(plateau[v]);

    for (let t = 0; t < KICK_FULL_MIN; t++) {
      for (const v of VOICE_NAMES) expect(arcAt(t)[v]).toBeLessThan(plateau[v]);
    }
    for (let t = KICK_FULL_MIN; t < PLATEAU_MIN; t++) {
      for (const v of LATE_BUILDERS) expect(arcAt(t)[v]).toBeLessThan(plateau[v]);
    }
  });

  it('holds every voice at its loudest across the midnight..2 AM window', () => {
    for (const v of VOICE_NAMES) {
      for (let t = PLATEAU_MIN; t <= 300; t += 10) expect(arcAt(t)[v]).toBeCloseTo(plateau[v], 10);
      for (let t = 301; t <= NIGHT_END_MIN; t++) expect(arcAt(t)[v]).toBeLessThan(plateau[v]);
    }
  });

  it('strips every voice back by lights up', () => {
    for (const v of VOICE_NAMES) {
      expect(arcAt(NIGHT_END_MIN)[v]).toBeLessThan(plateau[v]);
      expect(arcAt(NIGHT_END_MIN)[v]).toBeLessThan(arcAt(330)[v]);
    }
    // Low end goes first — that is what actually empties a floor.
    expect(arcAt(330).bass).toBeLessThan(arcAt(330).pad);
  });
});

describe('arcLabel', () => {
  it('names each keyframe at its minute and holds it until the next', () => {
    for (let i = 0; i < ARC.length; i++) {
      const k = ARC[i]!;
      expect(arcLabel(k.at)).toBe(k.label);
      const next = ARC[i + 1];
      if (next && next.at > k.at + 1) expect(arcLabel(k.at + 1)).toBe(k.label);
    }
  });

  it('reads the expected labels across the night', () => {
    expect(arcLabel(0)).toBe('EMPTY');
    expect(arcLabel(59)).toBe('EMPTY');
    expect(arcLabel(60)).toBe('FILLING');
    expect(arcLabel(119)).toBe('FILLING');
    expect(arcLabel(120)).toBe('BUILDING');
    expect(arcLabel(180)).toBe('PEAK');
    expect(arcLabel(299)).toBe('PEAK');
    expect(arcLabel(300)).toBe('PEAK');
    expect(arcLabel(330)).toBe('LAST DRINKS');
    expect(arcLabel(359)).toBe('LAST DRINKS');
    expect(arcLabel(NIGHT_END_MIN)).toBe('LIGHTS UP');
  });

  it('holds the edges before doors and after close', () => {
    expect(arcLabel(-30)).toBe(first.label);
    expect(arcLabel(NIGHT_END_MIN + 30)).toBe(last.label);
    expect(arcLabel(NaN)).toBe(first.label);
  });
});

describe('arcIntensity', () => {
  it('stays within 0..1 across and beyond the night', () => {
    for (let t = -60; t <= NIGHT_END_MIN + 60; t++) {
      const i = arcIntensity(t);
      expect(i).toBeGreaterThanOrEqual(0);
      expect(i).toBeLessThanOrEqual(1);
    }
  });

  it('is the mean of the four voice scalars', () => {
    for (const t of [0, 45, 120, 240, 345, 360]) {
      const s = arcAt(t);
      expect(arcIntensity(t)).toBeCloseTo((s.kick + s.hat + s.bass + s.pad) / 4, 10);
    }
  });

  it('rises from doors to peak', () => {
    let prev = -Infinity;
    for (let t = 0; t <= 180; t += 10) {
      const i = arcIntensity(t);
      expect(i).toBeGreaterThan(prev);
      prev = i;
    }
    expect(arcIntensity(180)).toBe(1);
    expect(arcIntensity(0)).toBeLessThan(0.5);
  });

  it('holds flat across the peak plateau', () => {
    for (let t = 180; t <= 300; t += 20) expect(arcIntensity(t)).toBeCloseTo(1, 10);
  });

  it('falls from the plateau to lights up', () => {
    let prev = Infinity;
    for (let t = 300; t <= NIGHT_END_MIN; t += 10) {
      const i = arcIntensity(t);
      expect(i).toBeLessThan(prev);
      prev = i;
    }
    expect(arcIntensity(NIGHT_END_MIN)).toBeLessThan(arcIntensity(300));
  });
});
