import { describe, expect, it } from 'vitest';
import { createSim } from './index';
import { referenceFactory, referenceFactoryTech } from './reference';
import { grantResearch } from './testkit';
import type { GameData, Sim, SimEvent } from '../contracts';

import items from '../../data/items.json';
import machines from '../../data/machines.json';
import recipes from '../../data/recipes.json';
import tech from '../../data/tech.json';
import commissions from '../../data/commissions.json';
import seamsJson from '../../data/seams.json';

const DATA = { items, machines, recipes, tech, commissions } as unknown as GameData;

function build(): Sim {
  const sim = createSim();
  sim.init(DATA, 1337);
  // A post-research save: v4 gates two of this layout's ids. Grant exactly what it needs
  // and nothing else, so the test would notice if the layout started depending on more.
  grantResearch(sim, referenceFactoryTech(DATA));
  for (const cmd of referenceFactory(DATA)) sim.enqueue(cmd);
  return sim;
}

/** Runs `ticks` and returns how much melt was shipped during exactly that window. */
function meltIn(sim: Sim, ticks: number, firstAt?: { tick: number }): number {
  let melt = 0;
  for (let i = 0; i < ticks; i++) {
    sim.tick();
    for (const e of sim.drainEvents()) {
      if (e.kind === 'shipped' && e.item === 'melt') {
        melt += e.count;
        if (firstAt && firstAt.tick < 0) firstAt.tick = e.tick;
      }
    }
  }
  return melt;
}

describe('reference factory', () => {
  it('ships melt inside 2,000 ticks', () => {
    const sim = build();
    const first = { tick: -1 };
    const melt = meltIn(sim, 2000, first);
    expect(first.tick).toBeGreaterThan(0);
    expect(first.tick).toBeLessThanOrEqual(2000);
    expect(melt).toBeGreaterThan(0);
  });

  it('is still shipping melt at 20,000 ticks — the deadlock regression net', () => {
    const sim = build();
    meltIn(sim, 2000);
    const early = sim.snapshot().shippedTotal['melt'] ?? 0;
    meltIn(sim, 16000);
    // The window that matters: round 1's build had wedged solid long before here.
    const late = meltIn(sim, 2000);
    const total = sim.snapshot().shippedTotal['melt'] ?? 0;

    expect(early).toBeGreaterThan(0);
    expect(late).toBeGreaterThan(0);
    expect(total).toBeGreaterThan(early);
  });

  it('keeps the demuxer alive by voiding surplus rather than jamming', () => {
    const sim = build();
    meltIn(sim, 20000);
    const snap = sim.snapshot();
    const demuxer = snap.entities.find((e) => e.def === 'demuxer')!;
    // The whole point: the head of the chain never wedges.
    expect(demuxer.jammed).not.toBe('output full');
    // Surplus is being turned into product, not silently dropped.
    expect(snap.shippedTotal['macroblock-bricks'] ?? 0).toBeGreaterThan(0);
    expect(snap.shippedTotal['hf-dust'] ?? 0).toBeGreaterThan(0);
    // v4: the slab overflow tap is live, so the demo visibly ships anchor slabs without
    // anyone having to demolish the i-only assembler to see one.
    expect(snap.shippedTotal['anchor-slab'] ?? 0).toBeGreaterThan(0);
  });

  it('refuses to build against drifted data instead of deadlocking mysteriously', () => {
    const stripped = { ...DATA, recipes: DATA.recipes.filter((r) => r.id !== 'quantize-crime') };
    expect(() => referenceFactory(stripped)).toThrow(/quantize-crime/);
  });
});

describe('what the reference build costs in research', () => {
  it('names exactly the techs it needs, and needs at least one', () => {
    const needed = referenceFactoryTech(DATA);
    expect(needed.length).toBeGreaterThan(0); // v4 gating is live; this is a late-game build
    // Every named tech is real, and granting just these is enough to build the whole thing.
    for (const id of needed) expect(DATA.tech.some((t) => t.id === id)).toBe(true);

    const sim = createSim();
    sim.init(DATA, 1337);
    grantResearch(sim, needed);
    for (const cmd of referenceFactory(DATA)) sim.enqueue(cmd);
    sim.tick();
    const placed = sim.snapshot().entities.length;
    const asked = referenceFactory(DATA).filter((c) => c.kind === 'place').length;
    expect(placed, 'every placement in the layout must be legal once its techs are open')
      .toBe(asked);
  });

  it('is silently crippled without them — the demo hook must grant first', () => {
    const sim = createSim();
    sim.init(DATA, 1337); // cold start: nothing researched
    for (const cmd of referenceFactory(DATA)) sim.enqueue(cmd);
    sim.tick();
    const asked = referenceFactory(DATA).filter((c) => c.kind === 'place').length;
    // Gated placements are dropped without a word, which is the contract's rule — the
    // factory simply comes up missing its power and browns out.
    expect(sim.snapshot().entities.length).toBeLessThan(asked);
    expect(sim.snapshot().entities.some((e) => e.def === 'software-decoder')).toBe(false);
  });
});

describe('the research annex (v5)', () => {
  it('mines its own seam, bottles packs and completes a tech inside 20k', () => {
    const sim = build();
    const evs: SimEvent[] = [];
    for (let i = 0; i < 20000; i++) {
      sim.tick();
      for (const e of sim.drainEvents()) evs.push(e);
    }
    // The whole chain ran on real data: ore -> luma -> coefficients -> bricks AND halos
    // -> a bottled pack -> a lab -> a tech.
    for (const r of ['quantize-crime', 'skim-ringing', 'bottle-spatial-pack']) {
      expect(evs.filter((e) => e.kind === 'crafted' && e.recipe === r).length, `${r} never ran`)
        .toBeGreaterThan(0);
    }
    const done = evs.filter((e) => e.kind === 'researched');
    expect(done.length, 'the demo never finished a tech').toBeGreaterThan(0);
    expect(sim.snapshot().research!.unlocked.length).toBeGreaterThan(
      referenceFactoryTech(DATA).length, // more than just what was granted to build it
    );
  });

  it('every placement is legal — nothing silently collides or lands off-seam', () => {
    const sim = build();
    sim.tick();
    const asked = referenceFactory(DATA).filter((c) => c.kind === 'place').length;
    expect(sim.snapshot().entities.length).toBe(asked);
    // And no extractor is sitting on dead ground once seams are wired in.
    for (const e of sim.snapshot().entities.filter((q) => q.def === 'seam-extractor')) {
      expect(e.jammed, `extractor at ${e.pos.x},${e.pos.y}`).not.toBe('no seam');
    }
  });

  it('sits on DATA’s seams — both works are on real ore', () => {
    // Read the seam map straight off disk: it is not wired into GameData yet, so this is
    // the only way to check the layout against the ground DATA actually authored.
    const rects = (seamsJson as { seams: Array<{ rect: { x: number; y: number; w: number; h: number } }> }).seams;
    const extractors = referenceFactory(DATA).filter(
      (c) => c.kind === 'place' && c.def === 'seam-extractor',
    ) as Array<{ pos: { x: number; y: number } }>;
    expect(extractors.length).toBeGreaterThan(0);

    const fp = DATA.machines.find((m) => m.id === 'seam-extractor')!.footprint;
    for (const e of extractors) {
      const on = rects.some(({ rect: s }) =>
        e.pos.x + fp.x - 1 >= s.x && e.pos.x <= s.x + s.w - 1
        && e.pos.y + fp.y - 1 >= s.y && e.pos.y <= s.y + s.h - 1);
      expect(on, `extractor at ${e.pos.x},${e.pos.y} is not on any authored seam`).toBe(true);
    }
  });
});
