// Invariant: no real skip. Every active participant at round start (still
// active at round end) gets a turn. Tracks per ACTUAL round (e.round), so
// rounds spanning pause/resume across loop iterations count correctly.
//
// Guards BUG-5 fix (slot-array turn order, no re-sort on wrap/resume).
// If this goes RED, turn order rotation is skipping participants again.
//
// New API: mutating funcs are async, take ctx last, write encounter + log
// internally, return newEnc. setup(ctx) returns the started encounter;
// loop bodies await each mutating call.

'use strict';

const shared = require('@ttrpg/shared');
const {
  buildCharacterParticipant, buildMonsterParticipant,
  startEncounter, nextTurn, togglePause, addParticipant, removeParticipant,
  toggleParticipantActive,
} = shared;
const { mockCtx } = require('./_helpers');

const nm = (enc) => (id) => {
  const f = enc.participants.find(p => p.id === id);
  return f ? f.name : id;
};

function setup(ctx) {
  const ps = [
    buildCharacterParticipant({ id: 'c1', name: 'Fighter', defaultMaxHp: 200, defaultInitMod: 2 }).participant,
    buildCharacterParticipant({ id: 'c2', name: 'Cleric', defaultMaxHp: 180, defaultInitMod: 1 }).participant,
    buildCharacterParticipant({ id: 'c3', name: 'Rogue', defaultMaxHp: 160, defaultInitMod: 3 }).participant,
    buildMonsterParticipant({ name: 'Goblin1', maxHp: 100, initMod: 2 }).participant,
    buildMonsterParticipant({ name: 'Goblin2', maxHp: 100, initMod: 2 }).participant,
    buildMonsterParticipant({ name: 'OrcBoss', maxHp: 500, initMod: 1 }).participant,
    buildMonsterParticipant({ name: 'Wolf', maxHp: 120, initMod: 3 }).participant,
    buildMonsterParticipant({ name: 'Merchant', maxHp: 150, initMod: 0, asNpc: true }).participant,
  ];
  let e = {
    name: 't', participants: ps, isStarted: false, isPaused: false,
    round: 0, currentTurnParticipantId: null, turnOrderIds: [],
  };
  return startEncounter(e, ctx); // async → Promise<newEnc>
}

describe('BUG-5: turn-order rotation never skips (deterministic)', () => {
  jest.setTimeout(15000);

  test('pure nextTurn: 0 skips across 100 rounds', async () => {
    const { ctx } = mockCtx();
    let e = await setup(ctx);
    let totalSkips = 0;
    for (let roundN = 1; roundN <= 100; roundN++) {
      const startRound = e.round;
      const activeAtStart = new Set(e.participants.filter(p => p.isActive).map(p => p.id));
      const acted = new Set();
      acted.add(e.currentTurnParticipantId);
      let guard = 0;
      const cap = e.participants.length + 1;
      while (e.round === startRound && guard < cap) {
        e = await nextTurn(e, ctx);
        if (e.round === startRound) acted.add(e.currentTurnParticipantId);
        guard++;
      }
      const skipped = [...activeAtStart].filter(id => {
        const p = e.participants.find(x => x.id === id);
        return p && p.isActive && !acted.has(id);
      });
      totalSkips += skipped.length;
    }
    expect(totalSkips).toBe(0);
  });

  test('with pause/resume + add/remove/toggle: 0 skips across ~540 rounds', async () => {
    const { ctx } = mockCtx();
    let e = await setup(ctx);
    const N = nm(e);
    let curRound = null;
    let activeAtRoundStart = new Set();
    let actedThisRound = new Set();
    const onRoundStart = (enc) => {
      curRound = enc.round;
      activeAtRoundStart = new Set(enc.participants.filter(p => p.isActive).map(p => p.id));
      actedThisRound = new Set();
      if (enc.currentTurnParticipantId) actedThisRound.add(enc.currentTurnParticipantId);
    };
    onRoundStart(e);

    let totalRealSkips = 0;
    let added = 0;
    let turns = 0;
    const MAX_TURNS = 2000;
    while (turns < MAX_TURNS && e.isStarted) {
      turns++;
      if (e.isPaused) e = await togglePause(e, ctx);
      if (turns % 7 === 0 && !e.isPaused) { e = await togglePause(e, ctx); continue; }
      const prevRound = e.round;
      e = await nextTurn(e, ctx);
      if (e.round !== prevRound) {
        const skipped = [...activeAtRoundStart].filter(id => {
          const p = e.participants.find(x => x.id === id);
          return p && p.isActive && !actedThisRound.has(id);
        });
        totalRealSkips += skipped.length;
        onRoundStart(e);
      } else {
        actedThisRound.add(e.currentTurnParticipantId);
      }
      if (turns % 9 === 0 && added < 8) {
        const b = buildMonsterParticipant({ name: `R${added + 1}`, maxHp: 120, initMod: 3 }).participant;
        b.id = `reinforce${added + 1}`;
        e = await addParticipant(e, b, ctx); added++;
      }
      if (turns % 13 === 0) {
        const cand = e.participants.filter(p => p.type === 'monster' && p.isActive && p.id !== e.currentTurnParticipantId);
        if (cand.length) e = await removeParticipant(e, cand[0].id, ctx);
      }
      if (turns % 17 === 0) {
        const cand = e.participants.filter(p => p.isActive && p.id !== e.currentTurnParticipantId);
        if (cand.length) {
          const t = cand[0];
          e = await toggleParticipantActive(e, t.id, ctx);
          e = await toggleParticipantActive(e, t.id, ctx);
        }
      }
    }
    expect(totalRealSkips).toBe(0);
  });
});
