/**
 * save()/load() round-trip.
 *
 * The bar is not "the snapshot looks the same" — it's "the future is the same". A save
 * that restores visible state but drops a splitter cursor or the RNG position looks
 * perfect and then quietly diverges a few hundred ticks later. So every test here loads
 * a blob and then runs BOTH sims forward before comparing.
 */
import { describe, expect, it } from 'vitest';
import { createSim } from './index';
import { referenceFactory } from './reference';
import { grantAllResearch } from './testkit';
import type { GameData, Sim } 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;

function fresh(seed = 1337): Sim {
  const sim = createSim();
  sim.init(DATA, seed);
  grantAllResearch(sim, DATA); // the reference factory is a post-research build
  return sim;
}
function run(sim: Sim, ticks: number): void {
  for (let i = 0; i < ticks; i++) { sim.tick(); sim.drainEvents(); }
}

describe('save/load', () => {
  it('restores a running factory so both sims agree 1,000 ticks later', () => {
    const original = fresh();
    for (const c of referenceFactory(DATA)) original.enqueue(c);
    run(original, 1500); // mid-flight: belts loaded, a scram cycle already under way

    const restored = fresh();
    restored.load!(original.save!());
    expect(restored.snapshot().tick).toBe(original.snapshot().tick);

    run(original, 1000);
    run(restored, 1000);
    expect(restored.snapshot()).toEqual(original.snapshot());
    expect(restored.snapshot().shippedTotal['melt']).toBeGreaterThan(0); // not a dead factory
  });

  it('round-trips through JSON without drifting', () => {
    const a = fresh();
    for (const c of referenceFactory(DATA)) a.enqueue(c);
    run(a, 800);

    const blob = a.save!();
    expect(() => JSON.parse(blob)).not.toThrow();

    const b = fresh();
    b.load!(blob);
    // Saving the restored sim must reproduce the same bytes: nothing was lost in the gap.
    expect(b.save!()).toBe(blob);
  });

  it('survives a save/load/save/load chain', () => {
    const a = fresh();
    for (const c of referenceFactory(DATA)) a.enqueue(c);
    run(a, 600);

    let carried = fresh();
    carried.load!(a.save!());
    for (let i = 0; i < 3; i++) {
      run(a, 200);
      run(carried, 200);
      const next = fresh();
      next.load!(carried.save!());
      carried = next;
    }
    run(a, 500);
    run(carried, 500);
    expect(carried.snapshot()).toEqual(a.snapshot());
  });

  it('preserves splitter cursors and queues, not just what the snapshot shows', () => {
    const a = fresh();
    for (const c of referenceFactory(DATA)) a.enqueue(c);
    run(a, 1200); // long enough for the splitters to be mid-rotation

    const b = fresh();
    b.load!(a.save!());
    // A dropped round-robin cursor is invisible at rest and shows up as divergence later.
    run(a, 2000);
    run(b, 2000);
    expect(b.snapshot()).toEqual(a.snapshot());
  });

  it('preserves commands enqueued but not yet ticked', () => {
    const a = fresh();
    for (const c of referenceFactory(DATA)) a.enqueue(c);
    run(a, 100);
    a.enqueue({ kind: 'place', def: 'shipper', pos: { x: 20, y: 20 }, dir: 0 });

    const b = fresh();
    b.load!(a.save!()); // saved with that place still pending
    run(a, 5);
    run(b, 5);
    expect(b.snapshot().entities.length).toBe(a.snapshot().entities.length);
    expect(b.snapshot()).toEqual(a.snapshot());
  });

  it('refuses a save from an unknown version', () => {
    const sim = fresh();
    expect(() => sim.load!(JSON.stringify({ v: 99 }))).toThrow(/version/);
  });

  it('refuses a save referencing machines this GameData does not have', () => {
    const a = fresh();
    a.enqueue({ kind: 'place', def: 'demuxer', pos: { x: 0, y: 0 }, dir: 0 });
    run(a, 1);
    const blob = a.save!().replace(/"demuxer"/g, '"machine-from-the-future"');

    const b = fresh();
    expect(() => b.load!(blob)).toThrow(/machine-from-the-future/);
  });
});
