/**
 * The bloom loop — M3's signature topology, proved early.
 *
 * Two separate claims are tested here, because they fail for different reasons:
 *
 *  1. The loop MECHANIC: a closed belt circle has no terminal machine, so it ranks 0 and
 *     machines will push onto it. Cargo circulates forever instead of wedging, and the
 *     tracer doesn't spin trying to walk it.
 *
 *  2. DATA's grade recipes COMPOSE: a closed delta-wafer bus with splitter taps feeding
 *     one duplicator per grade escalates bloom-concentrate all the way to grade 5.
 *
 * On the topology: the codex says "any closed belt circle with a P-FRAME DUPLICATOR on it;
 * contents compound each lap". With DATA's recipes that exact build cannot work — each
 * grade is a distinct recipe and a machine runs one recipe, so a lone duplicator can never
 * consume its own output. The shape that DOES compose puts the circle underneath as a
 * shared wafer bus: unclaimed wafers keep going round, and every duplicator gets fed on a
 * later lap. The loop is what makes the taps fair. See NOTES round 3.
 */
import { describe, expect, it } from 'vitest';
import { createSim } from './index';
import { grantAllResearch } from './testkit';
import type { Command, Dir, 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';

const DATA = { items, machines, recipes, tech, commissions } as unknown as GameData;
const N: Dir = 0, E: Dir = 1, S: Dir = 2, W: Dir = 3;

function newSim(seed = 1337): Sim {
  const sim = createSim();
  sim.init(DATA, seed);
  grantAllResearch(sim, DATA); // the bloom duplicator is gated behind stream-bloom-loops
  return sim;
}
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);
  }
}

describe('closed belt circles', () => {
  it('accept cargo and circulate it forever without losing or wedging it', () => {
    const sim = newSim();
    const cmds: Command[] = [];
    const b = (x: number, y: number, dir: Dir) => cmds.push({ kind: 'place', def: 'belt', pos: { x, y }, dir });

    cmds.push({ kind: 'place', def: 'decode-asic', pos: { x: 0, y: 10 }, dir: N });
    cmds.push({ kind: 'place', def: 'seam-extractor', pos: { x: 0, y: 0 }, dir: N }); // 2x2 at (0,0)
    // A 4x4 ring the miner can reach: it feeds (2,0), which is part of the circle.
    for (let x = 2; x <= 4; x++) b(x, 0, E);
    b(5, 0, S);
    for (let y = 1; y <= 3; y++) b(5, y, S);
    b(5, 4, W);
    for (let x = 4; x >= 3; x--) b(x, 4, W);
    b(2, 4, N);
    for (let y = 3; y >= 1; y--) b(2, y, N); // (2,1) -> (2,0) closes the ring

    for (const c of cmds) sim.enqueue(c);
    run(sim, 400);
    const loaded = sim.snapshot().beltItems.length;
    expect(loaded).toBeGreaterThan(0); // rank 0: the miner was willing to push onto a loop

    // Cargo keeps moving: track one item and prove it changes tiles rather than parking.
    const tracked = sim.snapshot().beltItems[0].id;
    const seen = new Set<number>();
    for (let i = 0; i < 600; i++) {
      run(sim, 1);
      const it = sim.snapshot().beltItems.find((x) => x.id === tracked);
      if (it) seen.add(it.entity);
    }
    expect(seen.size).toBeGreaterThan(3); // it went round, it didn't sit at a dead end

    // And the ring fills to a stable holding pattern rather than deleting anything.
    const before = sim.snapshot().beltItems.length;
    run(sim, 2000);
    expect(sim.snapshot().beltItems.length).toBeGreaterThanOrEqual(before);
    expect(sim.snapshot().entities.find((e) => e.def === 'seam-extractor')!.jammed).not.toBeNull();
  });
});

/**
 * ore -> demuxer -> mv-flux -> [split] -+-> p-caster -> delta wafers -> the BUS (a closed
 *                                       |               circle, tapped by 5 splitters)
 *                                       +-> tap-bloom
 * tap-bloom -> concentrate -> grade1 -> grade2 -> grade3 -> grade4 -> grade5 -> uplink
 */
function bloomLoop(): Command[] {
  const cmds: Command[] = [];
  const P = (def: string, x: number, y: number, dir: Dir = N) =>
    cmds.push({ kind: 'place', def, pos: { x, y }, dir });
  const b = (x: number, y: number, dir: Dir) => P('belt', x, y, dir);
  const runX = (x0: number, x1: number, y: number, dir: Dir) => {
    const s = x0 <= x1 ? 1 : -1;
    for (let x = x0; ; x += s) { b(x, y, dir); if (x === x1) break; }
  };
  const runY = (y0: number, y1: number, x: number, dir: Dir) => {
    const s = y0 <= y1 ? 1 : -1;
    for (let y = y0; ; y += s) { b(x, y, dir); if (y === y1) break; }
  };
  const recipeAt = (x: number, y: number, recipe: string) =>
    cmds.push({ kind: 'setRecipeAt', pos: { x, y }, recipe });

  // Power: ASICs only. Heat is not what this test is about.
  for (let i = 0; i < 8; i++) P('decode-asic', -30 + i * 3, -12);

  // Feeder: ore -> demuxer. Luma and slurry are byproducts here; sell both.
  P('seam-extractor', -30, 0);
  P('seam-extractor', -30, -3);
  P('seam-extractor', -30, 3);
  runY(-2, -1, -28, S);
  runY(3, 1, -28, N);
  runX(-28, -27, 0, E);
  P('demuxer', -26, 0);
  runX(-24, -23, 0, E);
  P('shipper', -22, 0);
  runY(2, 3, -26, S);
  P('shipper', -27, 4);

  // mv-flux splits between the wafer press and the bloom tap. The tap only wants 4 of
  // every ~56, so it fills, blocks, and the splitter hands the rest to the p-caster.
  b(-25, -1, N);
  P('lane-splitter', -25, -2);
  b(-25, -3, N);
  P('p-caster', -26, -5);
  runX(-24, 14, -2, E);
  runY(-2, 1, 15, S);
  b(15, 2, W); // -> tap-bloom at (14,2), at the far end of the bus

  // Wafers leave the press, run the long way round, and merge into the bus's right edge.
  runX(-26, 17, -6, E);
  runY(-6, 9, 18, S);
  b(18, 10, W);
  b(17, 10, W); // -> (16,10), a bus belt

  // THE BUS: a closed circle, (0,4) to (16,14), with five splitter taps on its top edge.
  const taps = [2, 5, 8, 11, 14];
  for (let x = 0; x <= 15; x++) {
    if (taps.includes(x)) P('lane-splitter', x, 4);
    else b(x, 4, E);
  }
  b(16, 4, S);
  runY(5, 13, 16, S);
  b(16, 14, W);
  runX(15, 1, 14, W);
  b(0, 14, N);
  runY(13, 5, 0, N); // (0,5) -> (0,4): the circle closes

  // Each tap hands wafers north, out of the ring, to its duplicator.
  for (const x of taps) b(x, 3, N);

  // The escalator runs COUNTER-CURRENT to the wafer flow, and that ordering is load
  // bearing. Wafers join the ring at its right edge and reach the taps left-to-right, so
  // whoever taps first eats first. Grade 5 wants 8 wafers a craft and grade 4 wants 5,
  // while the bloom tap only wants 8 per finished grade-1 — put the tap first (measured
  // it) and it swallows half of everything, grade 3 backs up 'output full' behind a
  // starving grade 4, and 30,000 ticks yields exactly one grade 5. Hungriest first.
  P('bloom-duplicator', 14, 2);  // tap-bloom (default recipe), fed last
  b(14, 1, N);
  P('bloom-duplicator', 14, 0);
  recipeAt(14, 0, 'bloom-grade-1');
  runX(13, 12, 0, W);
  b(11, 0, S);
  b(11, 1, S);
  P('bloom-duplicator', 11, 2);
  recipeAt(11, 2, 'bloom-grade-2');
  runX(10, 9, 2, W);
  P('bloom-duplicator', 8, 2);
  recipeAt(8, 2, 'bloom-grade-3');
  runX(7, 6, 2, W);
  P('bloom-duplicator', 5, 2);
  recipeAt(5, 2, 'bloom-grade-4');
  runX(4, 3, 2, W);
  P('bloom-duplicator', 2, 2);
  recipeAt(2, 2, 'bloom-grade-5'); // hungriest, tapped first
  b(1, 2, W);
  P('shipper', -1, 1);

  return cmds;
}

describe('bloom loop', () => {
  it('escalates concentrate to grade 5 off a closed wafer bus', () => {
    const sim = newSim();
    for (const c of bloomLoop()) sim.enqueue(c);

    const evs: SimEvent[] = [];
    run(sim, 20000, evs);
    const snap = sim.snapshot();

    // Every rung of DATA's ladder actually ran.
    for (const g of ['tap-bloom', 'bloom-grade-1', 'bloom-grade-2', 'bloom-grade-3',
      'bloom-grade-4', 'bloom-grade-5']) {
      expect(evs.filter((e) => e.kind === 'crafted' && e.recipe === g).length,
        `${g} never crafted`).toBeGreaterThan(0);
    }
    expect(snap.shippedTotal['bloom-grade-5'] ?? 0).toBeGreaterThan(0);
  });

  it('keeps wafers circulating so the far taps get fed too', () => {
    const sim = newSim();
    for (const c of bloomLoop()) sim.enqueue(c);
    run(sim, 20000);
    const snap = sim.snapshot();

    // The last tap on the bus is the hungriest (8 wafers) and the furthest from the
    // injection point. It only ever eats because unclaimed wafers come round again.
    const grade5 = snap.entities.find((e) => e.pos.x === 2 && e.pos.y === 2)!;
    expect(grade5.recipe).toBe('bloom-grade-5');
    expect(grade5.jammed).not.toBe('output full');

    // Wafers are genuinely resident on the ring rather than all consumed on arrival.
    const ringIds = new Set(snap.entities.filter((e) =>
      (e.def === 'belt' || e.def === 'lane-splitter') && e.pos.y >= 4 && e.pos.y <= 14
      && e.pos.x >= 0 && e.pos.x <= 16).map((e) => e.id));
    const onRing = snap.beltItems.filter((i) => ringIds.has(i.entity));
    expect(onRing.length).toBeGreaterThan(0);
    expect(onRing.every((i) => i.item === 'delta-wafer')).toBe(true);
  });

  it('never wedges the head of the chain', () => {
    const sim = newSim();
    for (const c of bloomLoop()) sim.enqueue(c);
    run(sim, 20000);
    const demuxer = sim.snapshot().entities.find((e) => e.def === 'demuxer')!;
    expect(demuxer.jammed).not.toBe('output full');
  });
});
