import assert from 'node:assert/strict';
import { test } from 'node:test';

import { describe } from '../adapter';
import { Engine, isHeldOut, worldForSeed } from '../engine';
import { initState, play, replay } from '../game';
import { thorough, closeBlind, inaction } from '../policies';

/**
 * The seam, exercised the way the shell exercises it: a fresh board per
 * seed, one reply at a time through `play()`, and a recording through
 * `replay()`. The engine's own conformance is the port's business; these
 * check that the board never shows what it must not and always agrees with
 * the engine it wraps.
 */

const SEEDS = Array.from({ length: 48 }, (_, i) => i);

test('a fresh board shows the screen, the free-screen hours, and no answer', () => {
  for (const seed of SEEDS) {
    const state = initState(seed);
    assert.equal(state.seed, seed);
    assert.equal(state.reveal, null, `seed ${seed} leaked the label before a disposition`);
    assert.equal(state.status, 'playing');
    assert.equal(state.turnsSpent, 0);
    assert.equal(state.minutesSpent, 30);
    assert.equal(state.lookups.length, 0);
    assert.equal(state.screen.summary.length, 12);
    assert.ok(state.shown.includes(state.screen.alert.id));
    assert.equal(state.screen.screen_order, isHeldOut(seed) ? 'kyc_first' : 'alert_first');
  }
});

test('the thorough analyst, played through play(), reaches the right call on every seed', () => {
  for (const seed of SEEDS) {
    let state = initState(seed);
    let guard = 0;
    while (state.status === 'playing' && guard < 12) {
      // The policy reads the engine's view; rebuild the same view off the board.
      const view = {
        screen: state.screen,
        lookups: state.lookups,
        turns_spent: state.turnsSpent,
        turns_left: 8 - state.turnsSpent,
        rejected: state.rejected,
        minutes_spent: state.minutesSpent,
        last_rejection: state.lastRejection,
      };
      state = play(state, thorough(view));
      guard += 1;
    }
    assert.equal(state.status === 'closed' || state.status === 'escalated', true, `seed ${seed} never decided`);
    assert.equal(state.outcome, 'solved', `seed ${seed}: thorough got it wrong`);
    assert.ok(state.reveal, `seed ${seed} has no reveal after the episode ended`);
    assert.equal(state.reveal?.label, worldForSeed(seed).label);
    assert.equal(state.reveal?.referenceMinutes, worldForSeed(seed).reference_minutes);
    assert.equal(state.rejected, 0);
    // Playing on after the end is a no-op, never a new board.
    assert.equal(play(state, '{"action":"lookup","what":"documents"}'), state);
  }
});

test('replay() agrees with the engine step for step and one snapshot per reply', () => {
  for (const seed of SEEDS.slice(0, 16)) {
    const engine = new Engine(worldForSeed(seed));
    const replies: string[] = [];
    while (!engine.done) {
      const reply = thorough(engine.view());
      replies.push(reply);
      engine.step(reply);
    }
    const snapshots = replay(seed, replies);
    assert.equal(snapshots.length, replies.length);
    const final = snapshots[snapshots.length - 1]!;
    assert.equal(final.minutesSpent, engine.minutes);
    assert.equal(final.turnsSpent, engine.turns);
    assert.deepEqual(final.disposition, engine.disposition);
    assert.deepEqual(final.shown.sort(), [...engine.shown].sort());
    // A fixture's own reference wins over the world's for the reveal.
    assert.equal(replay(seed, replies, { referenceMinutes: 99 }).at(-1)?.reveal?.referenceMinutes, 99);
  }
});

test('refusals cost a turn, are announced, and eight of them abort', () => {
  let state = initState(3);
  state = play(state, inaction());
  assert.equal(state.turnsSpent, 1);
  assert.equal(state.rejected, 1);
  assert.equal(state.minutesSpent, 33);
  assert.ok(state.lastRejection);
  assert.equal(state.reveal, null);
  assert.match(describe(state, initState(3), 0).announce, /refused/);

  for (let i = 1; i < 8; i += 1) state = play(state, inaction());
  assert.equal(state.status, 'aborted');
  assert.equal(state.outcome, 'aborted');
  assert.ok(state.reveal, 'an aborted episode still reveals the answer');
  assert.match(describe(state, null, 7).announce, /stays in the queue/);
});

test('citing an id never shown is refused, and a blind close is scored against the label', () => {
  const state = play(initState(5), '{"action":"close","reason":"DOCUMENTED_SOURCE_OF_FUNDS","cites":["doc.probate_letter"],"note":""}');
  assert.equal(state.rejected, 1);
  assert.match(state.lastRejection ?? '', /not been shown/);

  const blind = play(initState(5), closeBlind());
  assert.equal(blind.status, 'closed');
  assert.equal(blind.outcome, worldForSeed(5).label === 'benign' ? 'solved' : 'failed');
  const step = describe(blind, initState(5), 0);
  assert.equal(step.caption, 'closed');
  assert.match(step.announce, /citing 1 id/);
});
