jest.mock('../../database/mariadb', () => ({
  missionHelpers: {},
  playerHelpers: {},
  simulationHelpers: {},
  logToFile: jest.fn(),
}));

const simulationRouter = require('../../database/routes/simulationRoutes');

const { runSimulation, DIFFICULTY, chooseEnemyAttackProfile, combatRound } = simulationRouter.__testables;

function makePlayer(name, overrides = {}) {
  return {
    name,
    chapter: 'Ultramarines',
    spec: 'Kill-Marine',
    WS: 48,
    BS: 52,
    S: 45,
    T: 45,
    Ag: 42,
    Int: 46,
    Per: 44,
    Wp: 46,
    Fel: 38,
    wounds: 20,
    maxWounds: 20,
    fate: 1,
    maxFate: 1,
    weapons: [
      { name: 'Godwyn Bolter', damage: '2d10+5', rof: 'S/3/6', clipSize: 28, ammoRemaining: 28, ammoType: 'Standard', specialAmmo: false, shotsFired: 0, class: 'Basic' },
      { name: 'Astartes Chainsword', damage: '1d10+6', class: 'Melee' },
    ],
    conditions: [],
    fateUsed: false,
    rolls: [],
    successCount: 0,
    failCount: 0,
    fateSpends: 0,
    heroMoments: [],
    shameList: [],
    combatProfile: 'balanced',
    ...overrides,
  };
}

function clonePlayer(player) {
  return {
    ...player,
    weapons: player.weapons.map(weapon => ({ ...weapon })),
    conditions: [...player.conditions],
    rolls: [...player.rolls],
    heroMoments: [...player.heroMoments],
    shameList: [...player.shameList],
  };
}

const mission = {
  id: 991,
  name: 'Secure the Gene-Vault Under Fire',
  scenes: [
    {
      title: 'Gene-Vault Push',
      type: 'combat',
      enemies: [
        { name: 'Cultist Fire Team', wounds: 8, bs: 38, ws: 35, damage: '1d10+3', ap: 1 },
        { name: 'Plague Marine', wounds: 18, bs: 42, ws: 48, damage: '1d10+6', ap: 4 },
      ],
      checks: [
        { name: 'Hold firing lane', characteristic: 'BS', modifier: 0 },
      ],
      battlemapState: {
        layout: {
          gridWidth: 40,
          gridHeight: 30,
          zones: [
            { id: 'extraction', tone: 'spawn' },
            { id: 'left-wing', tone: 'objective' },
            { id: 'right-wing', tone: 'objective' },
            { id: 'maintenance', tone: 'hazard' },
            { id: 'vault', tone: 'vault' },
          ],
          cover: Array.from({ length: 10 }, (_, index) => ({ id: `cover-${index}` })),
          hazards: [{ id: 'sludge' }, { id: 'vent' }],
          blockers: [{ id: 'door-1' }],
          stations: [
            { id: 'left-station', name: 'Alpha Stabilization' },
            { id: 'right-station', name: 'Beta Stabilization' },
            { id: 'maintenance-station', name: 'Gamma Stabilization' },
          ],
          spawnPoints: [
            { id: 'north-hatch', name: 'North maintenance hatch' },
            { id: 'east-corridor', name: 'East corrupted corridor' },
            { id: 'south-tunnel', name: 'South service tunnel' },
            { id: 'vault-breach', name: 'Vault breach point' },
          ],
        },
        objectives: {
          stations: [
            { id: 'left-station', active: false },
            { id: 'right-station', active: false },
            { id: 'maintenance-station', active: false },
          ],
          geneCoreSecuredRounds: 0,
          activeSpawnId: 'north-hatch',
        },
      },
    },
  ],
};

describe('battlemap-aware simulation', () => {
  test('runs 20 battlemap simulations with objective and reinforcement activity', () => {
    const basePlayers = [
      makePlayer('Chris', { Int: 48 }),
      makePlayer('Rafen', { WS: 54, BS: 45, combatProfile: 'melee' }),
      makePlayer('Leon', { BS: 56, Int: 44, combatProfile: 'ranged' }),
      makePlayer('Mikael', { Int: 52, Per: 48 }),
    ];

    const summaries = [];
    for (let index = 0; index < 20; index += 1) {
      const result = runSimulation(
        mission,
        basePlayers.map(clonePlayer),
        DIFFICULTY[2],
        { enemyProfile: 'balanced' }
      );
      summaries.push(result.scene_results[0]?.battlemap || null);
    }

    expect(summaries).toHaveLength(20);
    expect(summaries.every(summary => summary && summary.active)).toBe(true);
    expect(summaries.some(summary => summary.reinforcementWaves >= 1)).toBe(true);
    expect(summaries.some(summary => summary.positions && Object.keys(summary.positions).length >= 4)).toBe(true);
  });

  test('emits battlemap movement events for players and enemies while keeping combat on-map', () => {
    const result = runSimulation(
      mission,
      [
        makePlayer('Chris', { Int: 48 }),
        makePlayer('Rafen', { WS: 54, BS: 45, combatProfile: 'melee' }),
        makePlayer('Leon', { BS: 56, Int: 44, combatProfile: 'ranged' }),
        makePlayer('Mikael', { Int: 52, Per: 48 }),
      ].map(clonePlayer),
      DIFFICULTY[2],
      { enemyProfile: 'balanced' }
    );

    const events = result.scene_results[0]?.events || [];
    expect(events.some(event => event.type === 'battlemapMove' && event.side === 'enemy')).toBe(true);
    expect(events.some(event => event.type === 'battlemapMove' && event.side === 'player')).toBe(true);
    expect(events.some(event => event.type === 'reinforcement' || event.type === 'battlemapAttack' || event.type === 'enemyAttack')).toBe(true);
  });

  test('melee enemy profiles commit to melee even before contact', () => {
    const profile = chooseEnemyAttackProfile({ combatProfile: 'melee', ws: 55, bs: 35, range: 30 }, 12);
    expect(profile.mode).toBe('melee');
    expect(profile.stat).toBe('WS');
    expect(profile.range).toBe(2);
  });

  test('authored battlemaps eventually produce horde combat beats', () => {
    const basePlayers = [
      makePlayer('Chris', { Int: 48 }),
      makePlayer('Rafen', { WS: 60, BS: 40, combatProfile: 'melee', weapons: [{ name: 'Astartes Chainsword', damage: '1d10+6', class: 'Melee' }] }),
      makePlayer('Leon', { BS: 56, Int: 44, combatProfile: 'ranged' }),
      makePlayer('Mikael', { Int: 52, Per: 48 }),
    ];

    let sawHordeBeat = false;
    for (let index = 0; index < 25; index += 1) {
      const result = runSimulation(
        mission,
        basePlayers.map(clonePlayer),
        DIFFICULTY[2],
        { enemyProfile: 'melee' }
      );
      const events = result.scene_results[0]?.events || [];
      if (events.some((event) => event.type === 'horde_damage' || (event.type === 'reinforcement' && /horde/i.test(String(event.enemy || ''))))) {
        sawHordeBeat = true;
      }
      if (sawHordeBeat) break;
    }

    expect(sawHordeBeat).toBe(true);
  });

  test('combat round does not acquire or attack targets through a wall', () => {
    const players = [
      clonePlayer(makePlayer('Rafen', { combatProfile: 'melee', weapons: [{ name: 'Astartes Chainsword', damage: '1d10+6', class: 'Melee' }] })),
    ];
    const enemies = [
      { name: 'Plague Marine', wounds: 18, bs: 42, ws: 48, damage: '1d10+6', ap: 4, agBonus: 4, combatProfile: 'melee' },
    ];
    const rollFeed = [];
    const events = [];
    const battlemapContext = {
      hasAuthoredLayout: false,
      gridWidth: 20,
      gridHeight: 10,
      zones: [
        { id: 'left-room', x: 0, y: 0, w: 8, h: 10, lighting: 'bright', tone: 'objective' },
        { id: 'right-room', x: 12, y: 0, w: 8, h: 10, lighting: 'bright', tone: 'objective' },
      ],
      cover: [],
      blockers: [],
      hazards: [],
      positions: {
        'player:Rafen': { x: 4.5, y: 5.5 },
        'enemy:Plague Marine': { x: 15.5, y: 5.5 },
      },
    };

    combatRound(1, players, enemies, [], 'Sealed Bulkhead', rollFeed, events, DIFFICULTY[2], {
      battlemapContext,
      objectiveActors: new Set(),
    });

    expect(events.some((event) => event.type === 'enemyAttack')).toBe(false);
    expect(events.some((event) => event.type === 'battlemapAttack')).toBe(false);
  });

  // Regression: battlemap.positions is mutated in place throughout combat, so
  // by report time it holds end-of-fight positions, not where the fight
  // started. The replay UI must seed from a separate pre-round-1 snapshot
  // (startingPositions) or every token renders frozen at its final spot.
  test('reports a startingPositions snapshot distinct from the end-of-combat positions', () => {
    const result = runSimulation(
      mission,
      [
        makePlayer('Chris', { Int: 48 }),
        makePlayer('Rafen', { WS: 54, BS: 45, combatProfile: 'melee' }),
        makePlayer('Leon', { BS: 56, Int: 44, combatProfile: 'ranged' }),
        makePlayer('Mikael', { Int: 52, Per: 48 }),
      ].map(clonePlayer),
      DIFFICULTY[2],
      { enemyProfile: 'balanced' }
    );

    const battlemap = result.scene_results[0]?.battlemap;
    const events = result.scene_results[0]?.events || [];
    expect(battlemap.startingPositions).toBeTruthy();
    expect(Object.keys(battlemap.startingPositions).length).toBeGreaterThanOrEqual(4);

    // Every actor's very first logged battlemapMove should start from the
    // snapshot position, not from wherever they ended up. Reinforcements
    // spawned mid-fight legitimately have no pre-round-1 snapshot entry, so
    // those are skipped rather than asserted on.
    const seen = new Set();
    let checked = 0;
    for (const ev of events) {
      if (ev.type !== 'battlemapMove') continue;
      const name = ev.actor || ev.player;
      const key = `${ev.side === 'enemy' ? 'enemy' : 'player'}:${name}`;
      if (seen.has(key)) continue;
      seen.add(key);
      const start = battlemap.startingPositions[key];
      if (!start) continue;
      checked += 1;
      const travelled = Number(ev.travelled || 0);
      const moved = Math.hypot(Number(ev.x) - start.x, Number(ev.y) - start.y);
      // The logged destination should be reachable from the snapshot start
      // within the reported travel distance (small slack for path rounding).
      expect(moved).toBeLessThanOrEqual(travelled + 1.5);
    }
    expect(checked).toBeGreaterThan(0);
  });
});
