import { describe, expect, it } from 'vitest';
import { escalation, resolveLedger, corruptionFor, ESCALATION_ITEMS } from './corruptionMap';
import { PARAMS } from './params';

const tally = (o: Record<string, number>) => new Map(Object.entries(o));
/** The param carrying the most pressure — "what does the player actually SEE". */
const dominant = (t: Record<string, number>) =>
  PARAMS.reduce((a, b) => (t[a] >= t[b] ? a : b));

describe('escalation curve', () => {
  it('is 0 when nothing has shipped and 1 at the tuned item count', () => {
    expect(escalation(0)).toBe(0);
    expect(escalation(-5)).toBe(0);
    expect(escalation(ESCALATION_ITEMS)).toBeCloseTo(1, 5);
  });

  it('clamps above the tuned count', () => {
    expect(escalation(10_000)).toBe(1);
  });

  it('is monotonic and front-loaded: the first 10 ships outrun the last 10', () => {
    let prev = -1;
    for (let n = 0; n <= 120; n += 5) {
      const e = escalation(n);
      expect(e).toBeGreaterThanOrEqual(prev);
      prev = e;
    }
    const early = escalation(10) - escalation(0);
    const late = escalation(100) - escalation(90);
    expect(early).toBeGreaterThan(late * 5);
  });
});

describe('item -> artifact legibility', () => {
  it('40 chroma slurry reads as chroma damage specifically', () => {
    const { targets } = resolveLedger(tally({ 'chroma-slurry': 40 }));
    expect(dominant(targets)).toBe('chroma');
    expect(targets.chroma).toBeGreaterThan(0.5);
  });

  it('melt reads as melt, not generic mush', () => {
    const { targets } = resolveLedger(tally({ melt: 40 }));
    expect(dominant(targets)).toBe('melt');
    expect(targets.melt).toBeGreaterThan(targets.mosh);
  });

  it('hf-dust reads as static', () => {
    expect(dominant(resolveLedger(tally({ 'hf-dust': 40 })).targets)).toBe('noise');
  });

  it('mixing shifts the blend toward whatever dominates the shipping', () => {
    const mixed = resolveLedger(tally({ 'chroma-slurry': 38, 'hf-dust': 2 })).targets;
    expect(dominant(mixed)).toBe('chroma');
    expect(mixed.noise).toBeGreaterThan(0); // the dust is still visible, just outvoted
  });

  it('unknown items degrade to a generic noise bump instead of vanishing', () => {
    const { targets, total } = resolveLedger(tally({ 'megaglitch-prototype': 20 }));
    expect(total).toBeGreaterThan(0);
    expect(dominant(targets)).toBe('noise');
    expect(corruptionFor('megaglitch-prototype').potency).toBeGreaterThan(0);
  });
});

describe('codex signatures read as themselves', () => {
  it.each([
    ['static-canister', 'noise'],       // bottled analog snow
    ['judder-cams', 'freeze'],          // 3:2 pulldown, machined
    ['macroblock-bricks', 'block'],     // the quantizer's confession
    ['coefficient-pack', 'block'],      // coeff overdrive reads as the grid
    ['ringing-halos', 'ring'],          // Gibbs phenomenon, canned
    ['gibbs-wraith', 'ring'],
    ['moire-crystal', 'moire'],         // interference made solid
    ['false-color-speckle', 'chroma'],
    ['mosquito-swarm', 'noise'],        // literal mosquito noise
    ['v-hold-roll', 'roll'],
    ['silver-frames', 'burn'],          // celluloid
    ['bloom-concentrate', 'mosh'],      // motion compounded on itself
  ])('%s reads as %s', (item, param) => {
    expect(dominant(resolveLedger(tally({ [item]: 40 })).targets)).toBe(param);
  });

  it('40 bricks look UNMISTAKABLY unlike 40 slurry (round 2 legibility bar)', () => {
    const bricks = resolveLedger(tally({ 'macroblock-bricks': 40 })).targets;
    const slurry = resolveLedger(tally({ 'chroma-slurry': 40 })).targets;
    // Each must dominate its own signature and carry ~none of the other's.
    expect(bricks.block).toBeGreaterThan(0.5);
    expect(slurry.block).toBe(0);
    expect(slurry.chroma).toBeGreaterThan(0.5);
    expect(bricks.chroma).toBe(0);
  });

  it('keeps the three new artifact classes distinct from each other', () => {
    const b = resolveLedger(tally({ 'macroblock-bricks': 40 })).targets;
    const r = resolveLedger(tally({ 'ringing-halos': 40 })).targets;
    const m = resolveLedger(tally({ 'moire-crystal': 40 })).targets;
    expect([b.ring, b.moire, r.block, r.moire, m.block, m.ring]).toEqual([0, 0, 0, 0, 0, 0]);
  });

  it('grades chroma subsampling by how much colour was thrown away', () => {
    expect(corruptionFor('chroma-420').potency).toBeGreaterThan(corruptionFor('chroma-422').potency);
    expect(corruptionFor('chroma-422').potency).toBeGreaterThan(corruptionFor('chroma-slurry').potency);
  });

  it('rates laser-primary gamut paint far above house-gray', () => {
    expect(corruptionFor('gamut-paint-rec2020').potency).toBeGreaterThan(
      corruptionFor('gamut-paint-rec709').potency * 3,
    );
  });
});

describe('survives whatever LANE-DATA ships', () => {
  it('every item in data/items.json produces sane params', async () => {
    const items = (await import('../../data/items.json')).default as Array<{ id: string }>;
    expect(items.length).toBeGreaterThan(0);
    for (const { id } of items) {
      const { targets, total } = resolveLedger(tally({ [id]: 25 }));
      expect(Number.isFinite(total), `${id} total`).toBe(true);
      for (const p of PARAMS) {
        expect(Number.isFinite(targets[p]), `${id}.${p}`).toBe(true);
        expect(targets[p], `${id}.${p}`).toBeGreaterThanOrEqual(0);
        expect(targets[p], `${id}.${p}`).toBeLessThanOrEqual(1);
      }
    }
  });
});

describe('anchor slabs sanitize', () => {
  it('shipping I-frames pulls corruption back down', () => {
    const dirty = resolveLedger(tally({ melt: 20 })).total;
    const cleaned = resolveLedger(tally({ melt: 20, 'anchor-slab': 5 })).total;
    expect(cleaned).toBeLessThan(dirty);
  });

  it('enough anchors fully sterilize THE SCREEN', () => {
    const { total, targets } = resolveLedger(tally({ melt: 10, 'anchor-slab': 50 }));
    expect(total).toBe(0);
    for (const p of PARAMS) expect(targets[p]).toBe(0);
  });

  it('anchors alone never drive corruption negative', () => {
    const { units, total } = resolveLedger(tally({ 'anchor-slab': 99 }));
    expect(units).toBe(0);
    expect(total).toBe(0);
  });
});

describe('repairs ease the ledger by one unit of the item (The Correction, v6)', () => {
  // A `repaired` event decrements the tally by one unit in index.ts; this pins the CONTRACT
  // that decrement is built on — "eases total corruption by its item's contribution".
  it("removing one unit eases net units by exactly that item's per-unit contribution", () => {
    const before = resolveLedger(tally({ melt: 20 }));
    const after = resolveLedger(tally({ melt: 19 }));
    expect(before.units - after.units).toBeCloseTo(corruptionFor('melt').potency, 5);
    expect(after.total).toBeLessThan(before.total);
  });

  it('correcting a hard-hitting item eases more than correcting a weak one', () => {
    const heavy =
      resolveLedger(tally({ 'explosive-hits': 20 })).units -
      resolveLedger(tally({ 'explosive-hits': 19 })).units;
    const light =
      resolveLedger(tally({ 'gamut-paint-rec709': 20 })).units -
      resolveLedger(tally({ 'gamut-paint-rec709': 19 })).units;
    expect(heavy).toBeGreaterThan(light);
  });

  it('a repair never drives corruption below sterile (theft floors at clean)', () => {
    // ship one, correct one → back to the NOTHING IS WRONG floor, never past it.
    const { units, total, targets } = resolveLedger(tally({ melt: 0 }));
    expect(units).toBe(0);
    expect(total).toBe(0);
    for (const p of PARAMS) expect(targets[p]).toBe(0);
  });
});

describe('param hygiene', () => {
  it('never emits a param outside 0..1, even shipping everything at once', () => {
    const everything = tally({
      melt: 500, 'chroma-slurry': 500, 'hf-dust': 500, 'delta-wafer': 500,
      'gop-crate': 500, 'luma-bars': 500, 'coefficient-pack': 500, 'mdat-ore': 500,
    });
    const { targets } = resolveLedger(everything);
    for (const p of PARAMS) {
      expect(targets[p]).toBeGreaterThanOrEqual(0);
      expect(targets[p]).toBeLessThanOrEqual(1);
    }
  });

  it('an empty ledger is fully sterile', () => {
    const { targets, total } = resolveLedger(tally({}));
    expect(total).toBe(0);
    for (const p of PARAMS) expect(targets[p]).toBe(0);
  });
});
