import { describe, expect, it, vi } from 'vitest';
import type { Command, SelectionState, UIBus, Vec2 } from '../contracts';
import { createSelection } from './selection';

/** A v4 host: typed selection, main.ts owns the state. */
function makeBus() {
  let state: SelectionState = null;
  const bus: UIBus = {
    dispatch: (_cmd: Command) => {},
    selectedBuild: () => (state && state.mode === 'build' ? { def: state.def, dir: state.dir } : null),
    pickTile: (_x: number, _y: number): Vec2 | null => null,
    selection: () => state,
    setSelection: (s) => { state = s; },
  };
  return { bus, read: () => state };
}

describe('createSelection (v4 typed protocol)', () => {
  it('publishes a typed build selection', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('quantizer');
    expect(read()).toEqual({ mode: 'build', def: 'quantizer', dir: 0 });
  });

  it('publishes {mode:"remove"} instead of a magic def string', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    expect(read()).toEqual({ mode: 'remove' });
  });

  it('publishes null when empty-handed', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('belt');
    sel.clear();
    expect(read()).toBeNull();
  });

  it('toggles a machine off when re-selected', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('belt');
    sel.select('belt');
    expect(sel.get()).toBeNull();
    expect(read()).toBeNull();
  });

  it('rotates N->E->S->W and wraps', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('belt');
    const dirs = [0, 0, 0, 0].map(() => {
      sel.rotate();
      return (read() as { dir: number }).dir;
    });
    expect(dirs).toEqual([1, 2, 3, 0]);
  });

  it('keeps the facing when swapping machines — you aim once, then build a line', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('belt');
    sel.rotate();
    sel.rotate();
    sel.select('demuxer');
    expect(read()).toEqual({ mode: 'build', def: 'demuxer', dir: 2 });
  });

  it('reports no build selection while removing, so nothing gets placed', () => {
    const { bus } = makeBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    expect(sel.get()).toBeNull();
    expect(sel.isRemoving()).toBe(true);
    expect(sel.isArmed()).toBe(true);
  });

  it('disarms remove when a machine is picked — you cannot build and demolish at once', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    sel.select('belt');
    expect(sel.isRemoving()).toBe(false);
    expect(read()).toEqual({ mode: 'build', def: 'belt', dir: 0 });
  });

  it('drops the held machine when remove is armed', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('belt');
    sel.toggleRemove();
    expect(sel.get()).toBeNull();
    expect(read()).toEqual({ mode: 'remove' });
  });

  it('toggles remove off again, leaving empty hands', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    sel.toggleRemove();
    expect(sel.isArmed()).toBe(false);
    expect(read()).toBeNull();
  });

  it('clear() disarms remove mode, which is what Esc rides on', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    sel.clear();
    expect(sel.isRemoving()).toBe(false);
    expect(read()).toBeNull();
  });

  it('does not rotate the wrecking ball', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    sel.rotate();
    expect(read()).toEqual({ mode: 'remove' });
  });

  // ---------------------------------------------------------------- v5 inspect mode
  it('publishes the inspected entity so the renderer can highlight it', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(42);
    expect(read()).toEqual({ mode: 'inspect', entity: 42 });
    expect(sel.isInspecting()).toBe(true);
  });

  it('does not count inspect as holding a tool — you can click another unit', () => {
    const { bus } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(42);
    expect(sel.isArmed()).toBe(false);
    expect(sel.get()).toBeNull();
  });

  it('moves the highlight when a different unit is inspected', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(1);
    sel.inspect(2);
    expect(read()).toEqual({ mode: 'inspect', entity: 2 });
  });

  it('stops highlighting when the inspector closes', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(42);
    sel.clearInspect();
    expect(read()).toBeNull();
    expect(sel.isInspecting()).toBe(false);
  });

  it('clearInspect is a no-op when we were not inspecting', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.select('belt');
    sel.clearInspect();
    expect(read()).toEqual({ mode: 'build', def: 'belt', dir: 0 }); // tool untouched
  });

  it('picking a tool supersedes the inspect highlight', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(42);
    sel.select('quantizer');
    expect(sel.isInspecting()).toBe(false);
    expect(read()).toEqual({ mode: 'build', def: 'quantizer', dir: 0 });
  });

  it('arming remove supersedes the inspect highlight', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(42);
    sel.toggleRemove();
    expect(sel.isInspecting()).toBe(false);
    expect(read()).toEqual({ mode: 'remove' });
  });

  it('clear() drops the inspect highlight too — that is what Esc rides on', () => {
    const { bus, read } = makeBus();
    const sel = createSelection(bus);
    sel.inspect(42);
    sel.clear();
    expect(read()).toBeNull();
  });

  it('re-inspecting the same entity does not churn the bus', () => {
    const { bus } = makeBus();
    let writes = 0;
    const spy: UIBus = { ...bus, setSelection: (s) => { writes++; bus.setSelection!(s); } };
    const sel = createSelection(spy);
    sel.inspect(42);
    sel.inspect(42);
    expect(writes).toBe(1);
  });

  it('notifies listeners on change, and not on a no-op clear', () => {
    const { bus } = makeBus();
    const sel = createSelection(bus);
    const cb = vi.fn();
    sel.onChange(cb);

    sel.select('belt');
    expect(cb).toHaveBeenCalledTimes(1);
    sel.rotate();
    expect(cb).toHaveBeenCalledTimes(2);
    sel.clear();
    expect(cb).toHaveBeenCalledTimes(3);

    sel.clear(); // already empty — nothing changed, nobody needs waking
    expect(cb).toHaveBeenCalledTimes(3);
  });
});

describe('legacy host fallback', () => {
  // selection/setSelection are optional in the contract; a host that predates them must
  // still get a selection rather than silently getting nothing.
  function legacyBus() {
    const bus: UIBus & { _sel?: unknown } = {
      _sel: null,
      dispatch: () => {},
      selectedBuild: () => null,
      pickTile: () => null,
    };
    return bus;
  }

  it('writes the old {def,dir} shape when setSelection is absent', () => {
    const bus = legacyBus();
    const sel = createSelection(bus);
    sel.select('belt');
    expect(bus._sel).toEqual({ def: 'belt', dir: 0 });
  });

  it('still speaks the deprecated __remove sentinel to a legacy host', () => {
    const bus = legacyBus();
    const sel = createSelection(bus);
    sel.toggleRemove();
    expect(bus._sel).toEqual({ def: '__remove', dir: 0 });
  });
});
