/**
 * Parity mites v1 — THE CORRECTION's first unit (codex §5).
 *
 * The design points these tests protect:
 *  - Mites are driven by CORRUPTION shipped (tier-2 product), not by a timer. Ship nothing
 *    and none arrive; the immune system only notices you once you're making product.
 *  - They correct corruption, never raw material — tier-0/1 cargo is invisible to them.
 *    That's the balance as much as the fiction, and it's the first thing that would rot.
 *  - A FIREWALL is the counter, recognised by recipe SHAPE (empty inputs, cans a mite),
 *    exactly like the mosquito trap — so DATA's real firewall lights up with zero code here.
 *
 * Fixture-based: DATA carries no firewall machine or `parity-mite` item yet (see NOTES round
 * 6), so both are modelled here the way the orders describe. The sim keys off shape and tier,
 * not ids, so the real data will drop straight in.
 */
import { describe, expect, it } from 'vitest';
import { createSim } from './index';
import { FIREWALL_RADIUS, MITE_ITEM, MITE_MAX, MITE_SPAWN_PER } from './constants';
import type {
  Command, Dir, GameData, ItemDef, MachineDef, RecipeDef, Sim, SimEvent, WildlifeState,
} from '../contracts';

const N: Dir = 0, E: Dir = 1;

const item = (id: string, tier: number): ItemDef =>
  ({ id, name: id, codex: 'fixture', tier, color: '#fff' });
const mach = (m: Partial<MachineDef> & Pick<MachineDef, 'id' | 'kind'>): MachineDef => ({
  name: m.id, codex: 'fixture', footprint: { x: 1, y: 1 }, recipes: [], powerDraw: 0,
  asset: 'fixture', ...m,
});
const rec = (r: Partial<RecipeDef> & Pick<RecipeDef, 'id' | 'machine' | 'outputs'>): RecipeDef => ({
  inputs: {}, ticks: 1, bandwidth: 0, ...r,
});

// raw = tier-0 (raw material: mites must ignore it). product = tier-2 (corruption: mites eat).
const FIXTURE: GameData = {
  items: [item('raw', 0), item('product', 2), item(MITE_ITEM, 2)],
  machines: [
    mach({ id: 'mine', kind: 'extractor', recipes: ['dig'] }),
    mach({ id: 'belt', kind: 'belt', beltSpeed: 3 }),
    mach({ id: 'ship', kind: 'shipper' }),
    mach({ id: 'gen', kind: 'power', powerGen: 100 }),
    mach({ id: 'forge', kind: 'crafter', recipes: ['make'] }),
    // The firewall: no inputs, cans a live mite. The sim spots it by recipe shape.
    mach({ id: 'firewall', kind: 'crafter', recipes: ['burn'], powerDraw: 1 }),
  ],
  recipes: [
    rec({ id: 'dig', machine: 'mine', outputs: { raw: 1 }, ticks: 4 }),
    rec({ id: 'make', machine: 'forge', inputs: { raw: 1 }, outputs: { product: 1 }, ticks: 4 }),
    rec({ id: 'burn', machine: 'firewall', inputs: {}, outputs: { [MITE_ITEM]: 1 }, ticks: 20 }),
  ],
  tech: [],
  commissions: [{ id: 'c1', flavor: 'product', wants: { product: 99999 }, rewardBandwidth: 1 }],
};

function newSim(seed = 1): Sim {
  const sim = createSim();
  sim.init(FIXTURE, seed);
  return sim;
}
const place = (def: string, x: number, y: number, dir: Dir = N): Command =>
  ({ kind: 'place', def, pos: { x, y }, dir });
function run(sim: Sim, ticks: number, sink?: SimEvent[]): void {
  for (let i = 0; i < ticks; i++) {
    sim.tick();
    const evs = sim.drainEvents();
    if (sink) for (const e of evs) sink.push(e);
  }
}
const mites = (sim: Sim): WildlifeState[] =>
  sim.snapshot().wildlife!.filter((w) => w.kind === 'parity-mite');

/**
 * A factory near origin that ships tier-2 product: mine -> raw belt -> forge -> a long
 * product belt -> shipper. The raw belt sits right at origin where mites converge, so
 * "mites ignore raw" is a real test, not a vacuous one.
 */
function corruptionFactory(sim: Sim, opts: { firewall?: boolean } = {}): void {
  sim.enqueue(place('gen', 0, 12));
  sim.enqueue(place('mine', 0, 0));
  sim.enqueue(place('belt', 1, 0, E)); // carries RAW (tier-0)
  sim.enqueue(place('forge', 2, 0));
  for (let x = 3; x <= 7; x++) sim.enqueue(place('belt', x, 0, E)); // carries PRODUCT (tier-2)
  sim.enqueue(place('ship', 8, 0));
  if (opts.firewall) sim.enqueue(place('firewall', 5, 2)); // guards the whole short product line
}

describe('parity mites — corruption drives them', () => {
  it('do not appear until enough corruption has shipped', () => {
    const sim = newSim();
    corruptionFactory(sim);
    const evs: SimEvent[] = [];
    // Run until the first mite spawns, tracking corruption at that moment.
    let shippedAtSpawn = -1;
    for (let i = 0; i < 20000 && shippedAtSpawn < 0; i++) {
      run(sim, 1, evs);
      if (mites(sim).length > 0) shippedAtSpawn = sim.snapshot().shippedTotal['product'] ?? 0;
    }
    expect(shippedAtSpawn, 'no mite ever spawned').toBeGreaterThanOrEqual(0);
    // A mite only arrives once corruption crosses the threshold — never on tick 0.
    expect(shippedAtSpawn).toBeGreaterThanOrEqual(MITE_SPAWN_PER);
  });

  it('a factory that ships NO corruption is never noticed', () => {
    // Same machines, but the product belt is severed before the shipper: nothing ships.
    const sim = newSim();
    sim.enqueue(place('gen', 0, 12));
    sim.enqueue(place('mine', 0, 0));
    sim.enqueue(place('belt', 1, 0, E));
    sim.enqueue(place('forge', 2, 0));
    // product piles in the forge and on a stub belt; none reaches a shipper.
    sim.enqueue(place('belt', 3, 0, E));
    run(sim, 8000);
    expect(sim.snapshot().shippedTotal['product'] ?? 0).toBe(0);
    expect(mites(sim)).toHaveLength(0);
  });
});

describe('parity mites — what they correct', () => {
  it('seek a tier-2 product on a belt and repair it out of existence', () => {
    const sim = newSim();
    corruptionFactory(sim);
    const evs: SimEvent[] = [];
    run(sim, 6000, evs);

    const repairs = evs.filter((e) => e.kind === 'repaired');
    expect(repairs.length, 'no product was ever repaired').toBeGreaterThan(0);
    // Every correction is a product (tier-2), and the event carries the item and a position.
    for (const r of repairs) {
      expect(r.kind).toBe('repaired');
      if (r.kind === 'repaired') {
        expect(r.item).toBe('product');
        expect(r.pos).toBeDefined();
      }
    }
  });

  it('THE BALANCE POINT: never touch tier-0/1 cargo', () => {
    const sim = newSim();
    corruptionFactory(sim);
    const evs: SimEvent[] = [];
    // The raw belt at origin is thick with tier-0 items the whole run, and mites pass right
    // over it to reach the product line — yet raw is never corrected.
    run(sim, 12000, evs);
    const raw = evs.filter((e) => e.kind === 'repaired' && e.item === 'raw');
    expect(raw).toHaveLength(0);
    // And there really WERE raw items sitting under the mites' path (not vacuously true).
    expect(sim.snapshot().beltItems.some((b) => b.item === 'raw')).toBe(true);
  });
});

describe('parity mites — escalation and cap', () => {
  it('scale with corruption but never exceed MITE_MAX', () => {
    const sim = newSim();
    corruptionFactory(sim);
    let peak = 0;
    for (let i = 0; i < 40000; i++) {
      run(sim, 1);
      peak = Math.max(peak, mites(sim).length);
    }
    // Deep into a heavily-corrupting run the patrol saturates the cap, and never breaches it.
    expect(peak).toBe(MITE_MAX);
    const shipped = sim.snapshot().shippedTotal['product'] ?? 0;
    expect(shipped).toBeGreaterThan(MITE_SPAWN_PER * MITE_MAX); // corruption was well past the cap
  });
});

describe('the firewall — the counter', () => {
  it('cans a mite that patrols into range, and the catch is cargo', () => {
    const sim = newSim();
    corruptionFactory(sim, { firewall: true });
    const evs: SimEvent[] = [];
    run(sim, 12000, evs);

    // A firewall in range of the product line cans mites: the canned mite is a real item.
    const fw = sim.snapshot().entities.find((e) => e.def === 'firewall')!;
    expect(fw.outputBuf[MITE_ITEM] ?? 0).toBeGreaterThan(0);
    // Those catches show up as wildlife cleared events for parity-mites.
    const caught = evs.filter(
      (e) => e.kind === 'wildlife' && e.on === 'cleared' && e.wildlife === 'parity-mite',
    );
    expect(caught.length).toBeGreaterThan(0);
  });

  it('with no mites in the world, a firewall just waits — it never cans air', () => {
    // Sever the product line before the shipper: nothing ships, so corruption stays 0 and no
    // mite ever arrives. The firewall has nothing to catch and holds honestly at 'starved'.
    const sim = newSim();
    sim.enqueue(place('gen', 0, 12));
    sim.enqueue(place('mine', 0, 0));
    sim.enqueue(place('belt', 1, 0, E));
    sim.enqueue(place('forge', 2, 0));
    sim.enqueue(place('belt', 3, 0, E));
    sim.enqueue(place('firewall', 6, 0));
    run(sim, 12000);
    expect(mites(sim)).toHaveLength(0); // precondition: nothing to catch
    const fw = sim.snapshot().entities.find((e) => e.def === 'firewall')!;
    expect(fw.outputBuf[MITE_ITEM] ?? 0).toBe(0);
    expect(fw.jammed).toBe('starved'); // an idle firewall is waiting, not broken
  });

  it('guarding the approach cancels corrections — the counter works', () => {
    const guarded = newSim(7);
    corruptionFactory(guarded, { firewall: true });
    const gEvs: SimEvent[] = [];
    run(guarded, 20000, gEvs);

    const open = newSim(7);
    corruptionFactory(open);
    const oEvs: SimEvent[] = [];
    run(open, 20000, oEvs);

    // Same seed, same layout but for the firewall. The firewall cans mites on the approach,
    // so far fewer products are corrected mid-belt than on the undefended line.
    const guardedRepairs = gEvs.filter((e) => e.kind === 'repaired').length;
    const openRepairs = oEvs.filter((e) => e.kind === 'repaired').length;
    expect(openRepairs).toBeGreaterThan(0); // the undefended line really was being corrected
    expect(guardedRepairs).toBeLessThan(openRepairs);
    // And the guard did its job by canning mites, not by luck.
    const fw = guarded.snapshot().entities.find((e) => e.def === 'firewall')!;
    expect(fw.outputBuf[MITE_ITEM] ?? 0).toBeGreaterThan(0);
  });

  it('FIREWALL_RADIUS is a real, bounded reach', () => {
    expect(FIREWALL_RADIUS).toBeGreaterThan(0);
    expect(Number.isFinite(FIREWALL_RADIUS)).toBe(true);
  });
});

describe('parity mites — determinism and persistence', () => {
  it('two sims with the same seed run the same wave', () => {
    const build = (): Sim => {
      const sim = newSim(4242);
      corruptionFactory(sim, { firewall: true });
      run(sim, 10000);
      return sim;
    };
    const a = build();
    const b = build();
    expect(mites(a).length + (a.snapshot().shippedTotal[MITE_ITEM] ? 1 : 0)).toBeGreaterThanOrEqual(0);
    expect(b.snapshot()).toEqual(a.snapshot());
  });

  it('survives save/load mid-wave, and the future still matches', () => {
    const a = newSim(77);
    corruptionFactory(a);
    run(a, 7000);
    expect(mites(a).length, 'no wave to persist').toBeGreaterThan(0);

    const b = newSim(77);
    b.load(a.save());
    expect(b.snapshot().wildlife).toEqual(a.snapshot().wildlife);

    run(a, 5000);
    run(b, 5000);
    expect(b.snapshot()).toEqual(a.snapshot());
  });
});
