import { describe, expect, it } from 'vitest';
import type { Dir, EntityState, MachineDef, SimSnapshot, Vec2 } from '../contracts';
import { createRoster, entityAt, footprintOf, syncRoster } from './pick';

function def(id: string, w: number, h: number): MachineDef {
  return {
    id, name: id.toUpperCase(), codex: '', kind: 'crafter',
    footprint: { x: w, y: h }, recipes: [], powerDraw: 0, asset: id,
  };
}

function ent(id: number, defId: string, pos: Vec2, dir: Dir = 0): EntityState {
  return {
    id, def: defId, pos, dir, recipe: null, progress: 0,
    inputBuf: {}, outputBuf: {}, jammed: null, heat: 0,
  };
}

function snap(entities: EntityState[]): SimSnapshot {
  return {
    tick: 0, paused: false, entities, beltItems: [],
    bandwidth: { gen: 0, draw: 0, stored: 0, brownout: false },
    shippedTotal: {}, activeCommission: null, commissionProgress: {},
  };
}

const MACHINES = new Map<string, MachineDef>([
  ['reactor', def('reactor', 3, 3)],
  ['press', def('press', 2, 1)],
  ['belt', def('belt', 1, 1)],
]);

describe('footprintOf', () => {
  it('leaves N/S footprints alone', () => {
    expect(footprintOf(def('press', 2, 1), 0)).toEqual({ x: 2, y: 1 });
    expect(footprintOf(def('press', 2, 1), 2)).toEqual({ x: 2, y: 1 });
  });

  it('swaps width and height for E/W', () => {
    expect(footprintOf(def('press', 2, 1), 1)).toEqual({ x: 1, y: 2 });
    expect(footprintOf(def('press', 2, 1), 3)).toEqual({ x: 1, y: 2 });
  });
});

describe('entityAt', () => {
  const roster = createRoster();

  it('hits every tile of a footprint and nothing outside it', () => {
    syncRoster(roster, snap([ent(7, 'reactor', { x: 4, y: 4 })]));
    // pos is the origin corner (min x, min y): 4..6 inclusive on both axes.
    for (let x = 4; x <= 6; x++) {
      for (let y = 4; y <= 6; y++) expect(entityAt(roster, MACHINES, { x, y })).toBe(7);
    }
    for (const t of [{ x: 3, y: 4 }, { x: 7, y: 4 }, { x: 4, y: 3 }, { x: 4, y: 7 }]) {
      expect(entityAt(roster, MACHINES, t)).toBeNull();
    }
  });

  it('respects rotation when hit-testing', () => {
    syncRoster(roster, snap([ent(1, 'press', { x: 0, y: 0 }, 1)])); // 2x1 rotated -> 1x2
    expect(entityAt(roster, MACHINES, { x: 0, y: 1 })).toBe(1);
    expect(entityAt(roster, MACHINES, { x: 1, y: 0 })).toBeNull();
  });

  it('returns null on empty ground', () => {
    syncRoster(roster, snap([]));
    expect(entityAt(roster, MACHINES, { x: 0, y: 0 })).toBeNull();
  });

  it('prefers the later-placed unit when footprints overlap', () => {
    syncRoster(roster, snap([ent(1, 'belt', { x: 2, y: 2 }), ent(2, 'belt', { x: 2, y: 2 })]));
    expect(entityAt(roster, MACHINES, { x: 2, y: 2 })).toBe(2);
  });

  it('ignores an entity whose def is unknown rather than throwing', () => {
    syncRoster(roster, snap([ent(9, 'ghost-machine', { x: 0, y: 0 })]));
    expect(entityAt(roster, MACHINES, { x: 0, y: 0 })).toBeNull();
  });
});

describe('syncRoster', () => {
  it('copies values out rather than holding the snapshot entities', () => {
    const roster = createRoster();
    const e = ent(3, 'belt', { x: 1, y: 1 });
    syncRoster(roster, snap([e]));

    // CONTRACTS: sim may reuse its buffers. Mutating the source must not move our copy.
    e.pos.x = 99;
    e.id = 42;
    expect(roster.entries[0].x).toBe(1);
    expect(roster.entries[0].id).toBe(3);
  });

  it('reuses its backing array and reports a shrinking length', () => {
    const roster = createRoster();
    syncRoster(roster, snap([ent(1, 'belt', { x: 0, y: 0 }), ent(2, 'belt', { x: 1, y: 0 })]));
    const backing = roster.entries;
    expect(roster.length).toBe(2);

    syncRoster(roster, snap([ent(5, 'belt', { x: 9, y: 9 })]));
    expect(roster.entries).toBe(backing); // no per-frame allocation
    expect(roster.length).toBe(1);
    expect(roster.entries[0].id).toBe(5);
    // A stale slot must not be reachable through the reported length.
    expect(entityAt(roster, MACHINES, { x: 1, y: 0 })).toBeNull();
  });
});
