/**
 * Playthrough simulation: "The Hunt for Fabius Bile"
 * GM + 4 players (anders, christoffer, claes, phillip)
 *
 * Covers all 11 post-simulation fixes and 4 subsequent findings.
 * Run with: npx jest missionPlaythrough --watchAll=false
 */
import React from 'react';
import { render, screen, waitFor, fireEvent as fe } from '@testing-library/react';
import '@testing-library/jest-dom';
import axios from 'axios';
import MissionTab from '../components/MissionTab';

jest.mock('axios');

// ─── Fixtures ───────────────────────────────────────────────────────────────

const PLAYERS = ['anders', 'christoffer', 'claes', 'phillip'];

const scene1 = {
  title: 'Mission Brief',
  type: 'intro',
  description: 'Watch-Captain Hestus projects the kill-zone in cold blue lumen.',
  endingText: 'The kill-team leaves the briefing with the next target burning on the hololith.',
  objectives: ['Confirm the source of the Bile-pattern gene signature.'],
  complications: ['Orbital augurs show movement beneath the landing zone.'],
  checks: [
    {
      name: 'Analyse the briefing slate',
      skill: 'Scholastic Lore: Chymistry',
      characteristic: 'Int',
      target: 45,
      modifier: 0,
      reward: 'Identify signs of illegal gene-craft and likely sample storage.',
    },
    {
      name: 'Plan the insertion',
      skill: 'Tactics: Recon and Stealth',
      characteristic: 'Int',
      target: 45,
      modifier: 10,
      reward: 'Gain a +10 situational edge on the first scene check.',
    },
  ],
  revealed: true,
  rollsUnlocked: false,
};

const scene1Hidden = { ...scene1, revealed: false };

const scene2Combat = {
  title: 'Landing Goes Wrong',
  type: 'combat',
  description: 'Tyranid forms erupt from the soil the moment the pods touch down.',
  enemies: [{ name: 'Gaunt' }, { name: 'Warrior' }],
  checks: [
    { name: 'Spot the tunnel breach', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Act before the first wave.' },
    { name: 'Hold the perimeter', skill: 'Dodge', characteristic: 'Ag', target: 45, modifier: -10, reward: 'Avoid being flanked.' },
  ],
  revealed: true,
  combatState: {
    round: 1,
    initiatives: [],
    conditions: {},
    fearRating: 2,
  },
  playPrompt: {
    type: 'combat',
    title: 'Combat: Gaunt',
    combat: { enemy: 'Gaunt', bs: 45, ws: 65, modifier: 0, damage: '1d5', targetTB: 3, targetArmour: 0 },
    openedAt: new Date().toISOString(),
  },
};

const scene2CombatWithInitiatives = {
  ...scene2Combat,
  combatState: {
    ...scene2Combat.combatState,
    initiatives: [
      { player: 'anders', agBonus: 4, dieRoll: 7, total: 11, type: 'player', ts: 1 },
      { player: 'Gaunt Alpha', agBonus: 0, dieRoll: 8, total: 8, type: 'npc', ts: 2 },
    ],
    turnKey: 'npc:gaunt alpha:2',
  },
};

const scene2WithFearRating4 = {
  ...scene2Combat,
  combatState: { ...scene2Combat.combatState, fearRating: 4 },
};

const scene2WithCheck = {
  ...scene2Combat,
  playPrompt: {
    type: 'check',
    title: 'Spot the tunnel breach',
    check: { name: 'Spot the tunnel breach', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Act before the first wave.' },
    assignedTo: null,
    isDecision: false,
    openedAt: new Date().toISOString(),
  },
};

const scene2AssignedCheck = {
  ...scene2Combat,
  playPrompt: {
    type: 'check',
    title: 'Spot the tunnel breach',
    check: { name: 'Spot the tunnel breach', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Act before the first wave.' },
    assignedTo: 'anders',
    isDecision: false,
    openedAt: new Date().toISOString(),
  },
};

const scene2DecisionCheck = {
  ...scene2Combat,
  playPrompt: {
    type: 'check',
    title: 'Deliver final judgement',
    check: { name: 'Deliver final judgement', skill: 'Command', characteristic: 'Fel', target: 45, modifier: 0, reward: 'Sets the tone for the epilogue.' },
    assignedTo: null,
    isDecision: true,
    openedAt: new Date().toISOString(),
  },
};

const allScenesComplete = [
  { ...scene1, completed: true },
  { ...scene2Combat, completed: true },
];

const activeMission = {
  id: 9,
  name: 'The Hunt for Fabius Bile',
  theme: 'tyranid',
  threat_level: 'High',
  player_count: 4,
  enemy_count: 59,
  gm_player: 'gm',
  current_scene: 0,
  scenes: [scene1, scene2Combat],
};

const rollFeed = [
  { id: 1, mission_id: 9, player_name: 'anders', roll_type: 'check', scene_index: 0, label: 'Analyse the briefing slate', payload: { roll: 19, target: 45, success: true, dos: 2, dof: 0 }, created_at: '2026-06-24T10:00:00.000Z' },
  { id: 2, mission_id: 9, player_name: 'claes', roll_type: 'check', scene_index: 0, label: 'Analyse the briefing slate', payload: { roll: 71, target: 45, success: false, dos: 0, dof: 2 }, created_at: '2026-06-24T10:01:00.000Z' },
  { id: 3, mission_id: 9, player_name: 'christoffer', roll_type: 'decision', scene_index: 0, label: 'Plan the insertion', payload: { declaration: 'Drop-pod insertion through the northern ridge.' }, created_at: '2026-06-24T10:02:00.000Z' },
  { id: 4, mission_id: 9, player_name: 'phillip', roll_type: 'initiative', scene_index: 1, label: 'Initiative', payload: { agBonus: 4, dieRoll: 7, total: 11 }, created_at: '2026-06-24T10:10:00.000Z' },
];

// ─── Helpers ─────────────────────────────────────────────────────────────────

function mockAxiosForPlayer(scene, feed = rollFeed) {
  axios.get.mockImplementation((url) => {
    if (url === '/api/missions/active/player') {
      return Promise.resolve({
        data: {
          id: activeMission.id,
          name: activeMission.name,
          current_scene: 0,
          active_scene_index: activeMission.current_scene,
          scenes: [scene],
        },
      });
    }
    if (url.includes('/api/missions/active/rolls/feed')) {
      return Promise.resolve({ data: feed });
    }
    if (url === '/api/players') {
      return Promise.resolve({ data: PLAYERS.map(name => ({ name })) });
    }
    return Promise.resolve({ data: null });
  });
}

function mockAxiosForGM(scenes = activeMission.scenes, feed = rollFeed) {
  axios.get.mockImplementation((url) => {
    if (url === '/api/missions') {
      return Promise.resolve({ data: [{ ...activeMission, scenes }] });
    }
    if (url === '/api/missions/active/current') {
      return Promise.resolve({ data: { ...activeMission, scenes } });
    }
    if (url.includes('/api/missions/active/rolls/feed')) {
      return Promise.resolve({ data: feed });
    }
    if (url === '/api/players') {
      return Promise.resolve({ data: PLAYERS.map(name => ({ name })) });
    }
    return Promise.resolve({ data: null });
  });
  axios.put.mockResolvedValue({ data: { mission: { ...activeMission, scenes } } });
  axios.post.mockResolvedValue({ data: { mission: { ...activeMission, scenes }, success: true, id: 99 } });
  axios.delete.mockResolvedValue({ data: { success: true } });
}

// ─── Tests ───────────────────────────────────────────────────────────────────

beforeEach(() => {
  jest.clearAllMocks();
  jest.useFakeTimers();
  // Clear localStorage between tests
  localStorage.clear();
});

afterEach(() => {
  jest.useRealTimers();
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #1 — Players see each other's rolls
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #1 — Players see each other rolls in roll feed', () => {
  test('roll feed endpoint is polled for non-GM players', async () => {
    mockAxiosForPlayer(scene1);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(axios.get).toHaveBeenCalledWith(expect.stringContaining('/api/missions/active/rolls/feed')));
  });

  test('players see rolls from other players in the feed', async () => {
    mockAxiosForPlayer(scene1, rollFeed);
    render(<MissionTab authedPlayer="christoffer" />);
    // Scene Roll Feed shows scene_index 0 rolls for scene 0
    await waitFor(() => {
      expect(screen.getAllByText(/Analyse the briefing slate/).length).toBeGreaterThan(0);
    });
    // anders' roll visible to christoffer
    expect(screen.getByText(/anders/)).toBeInTheDocument();
    // claes' roll also visible
    expect(screen.getByText(/claes/)).toBeInTheDocument();
  });

  test('player feed is filtered to current scene index only', async () => {
    // scene_index 1 roll (phillip initiative) should NOT appear in scene 0 feed
    mockAxiosForPlayer(scene1, rollFeed);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(axios.get).toHaveBeenCalledWith(expect.stringContaining('/api/missions/active/rolls/feed')));
    // phillip's initiative roll is scene_index 1, should not appear when viewing scene 0
    // (feed is filtered client-side, initiative label only shows in scene 1)
    const feed = screen.queryByText(/phillip.*initiative/i);
    expect(feed).not.toBeInTheDocument();
  });

  test('GM sees full roll feed including delete buttons', async () => {
    mockAxiosForGM([scene1]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => screen.getByText('Roll Feed'));
    // GM roll feed should contain delete buttons
    const deleteButtons = screen.getAllByText('Delete');
    expect(deleteButtons.length).toBeGreaterThan(0);
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #2 — Scene hidden until GM reveals
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #2 — Scene revealed gate', () => {
  test('players see placeholder when scene.revealed is false', async () => {
    mockAxiosForPlayer(scene1Hidden);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText(/Awaiting GM scene briefing/i)).toBeInTheDocument());
    expect(screen.queryByText(scene1.description)).not.toBeInTheDocument();
  });

  test('players see scene text when scene.revealed is true', async () => {
    mockAxiosForPlayer(scene1);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getAllByText(scene1.description).length).toBeGreaterThan(0));
    expect(screen.queryByText(/Awaiting GM scene briefing/i)).not.toBeInTheDocument();
  });

  test('GM always sees scene text regardless of revealed flag', async () => {
    mockAxiosForGM([scene1Hidden]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getAllByText(scene1.description).length).toBeGreaterThan(0));
  });

  test('GM sees the full read-aloud story at the bottom of the mission', async () => {
    mockAxiosForGM([scene1, scene2Combat]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Hele historien til oplæsning')).toBeInTheDocument());
    expect(screen.getAllByText(scene2Combat.description).length).toBeGreaterThan(0);
    expect(screen.getByText('Ending Read-aloud')).toBeInTheDocument();
    expect(screen.getByText(scene1.endingText)).toBeInTheDocument();
  });

  test('GM can see Scene Secrets reveal toggle', async () => {
    mockAxiosForGM([scene1Hidden]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Scene Secrets')).toBeInTheDocument());
    expect(screen.getByText('Reveal to Players')).toBeInTheDocument();
    expect(screen.getByText('All')).toBeInTheDocument();
    expect(screen.getByText('Lock')).toBeInTheDocument();
  });

  test('GM reveal toggle shows correct state when revealed', async () => {
    mockAxiosForGM([scene1]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Scene Secrets')).toBeInTheDocument());
    expect(screen.getAllByText('👁').length).toBeGreaterThan(0);
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #3 — NPC in initiative tracker
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #3 — GM can add NPC to initiative', () => {
  test('GM sees NPC initiative form during combat', async () => {
    mockAxiosForGM([scene2Combat]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    expect(screen.getByPlaceholderText('e.g. Hormagaunt')).toBeInTheDocument();
    expect(screen.getByText('Add NPC')).toBeInTheDocument();
  });

  test('NPC initiative entries show NPC badge', async () => {
    const sceneWithNpc = {
      ...scene2Combat,
      combatState: {
        ...scene2Combat.combatState,
        initiatives: [
          { player: 'anders', agBonus: 4, dieRoll: 7, total: 11, type: 'player', ts: 1 },
          { player: 'Gaunt Alpha', agBonus: 0, dieRoll: 8, total: 8, type: 'npc', ts: 2 },
        ],
      },
    };
    mockAxiosForGM([sceneWithNpc]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    expect(screen.getAllByText('NPC').length).toBeGreaterThan(0);
    expect(screen.getAllByText('Gaunt Alpha').length).toBeGreaterThan(0);
  });

  test('players also see NPC entries in the initiative list', async () => {
    const sceneWithNpc = {
      ...scene2Combat,
      combatState: {
        ...scene2Combat.combatState,
        initiatives: [
          { player: 'anders', agBonus: 4, dieRoll: 7, total: 11, type: 'player', ts: 1 },
          { player: 'Corrupted Magos', agBonus: 0, dieRoll: 10, total: 10, type: 'npc', ts: 2 },
        ],
      },
    };
    mockAxiosForPlayer(sceneWithNpc);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    expect(screen.getAllByText('Corrupted Magos').length).toBeGreaterThan(0);
    expect(screen.getAllByText('NPC').length).toBeGreaterThan(0);
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #4 — Round counter visible in Dice Roller via localStorage
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #4 — Combat state synced to localStorage for Dice Roller', () => {
  test('combat state is written to localStorage when combat is active', async () => {
    mockAxiosForPlayer(scene2Combat);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    const stored = JSON.parse(localStorage.getItem('dw:combat-state') || 'null');
    expect(stored).toBeTruthy();
    expect(stored.active).toBe(true);
    expect(stored.round).toBe(1);
  });

  test('combat state includes current and next turn order in localStorage', async () => {
    mockAxiosForPlayer(scene2CombatWithInitiatives);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    const stored = JSON.parse(localStorage.getItem('dw:combat-state') || 'null');
    expect(stored.currentTurn?.name).toBe('Gaunt Alpha');
    expect(stored.nextTurn?.name).toBe('anders');
    expect(Array.isArray(stored.initiativeOrder)).toBe(true);
    expect(stored.initiativeOrder[0]?.name).toBe('anders');
  });

  test('round number is written to localStorage on round change', async () => {
    const sceneRound3 = {
      ...scene2Combat,
      combatState: { ...scene2Combat.combatState, round: 3 },
    };
    mockAxiosForPlayer(sceneRound3);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    const stored = JSON.parse(localStorage.getItem('dw:combat-state') || 'null');
    expect(stored.round).toBe(3);
  });

  test('localStorage combat-state is inactive when no combat prompt', async () => {
    mockAxiosForPlayer(scene1);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText(scene1.description)).toBeInTheDocument());
    const stored = JSON.parse(localStorage.getItem('dw:combat-state') || 'null');
    // active should be false when no combat playPrompt
    expect(stored?.active).toBeFalsy();
  });
});

describe('Issue #4b — Initiative turn order advances through rounds', () => {
  test('GM can advance initiative and wrap to the next round', async () => {
    mockAxiosForGM([{ ...scene2CombatWithInitiatives }]);
    render(<MissionTab authedPlayer="gm" />);

    await waitFor(() => expect(screen.getByText('Initiative Order')).toBeInTheDocument());
    expect(screen.getAllByText((content, node) => Boolean(node?.textContent?.includes('Current:') && node.textContent.includes('Gaunt Alpha'))).length).toBeGreaterThan(0);
    expect(screen.getAllByText((content, node) => Boolean(node?.textContent?.includes('Next:') && node.textContent.includes('anders'))).length).toBeGreaterThan(0);

    fe.click(screen.getAllByText('Next turn')[0]);

    await waitFor(() => {
      expect(axios.put).toHaveBeenCalledWith(
        '/api/missions/9/progress',
        expect.objectContaining({
          currentScene: 0,
          scenes: expect.arrayContaining([
            expect.objectContaining({
              combatState: expect.objectContaining({
                round: 2,
                turnKey: 'player:anders',
              }),
            }),
          ]),
        })
      );
    });
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #5 — Check reward hidden until after roll
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #5 — Check reward hidden before rolling', () => {
  test('reward text is not visible when check is open but unrolled', async () => {
    mockAxiosForPlayer(scene2WithCheck);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getAllByText('Spot the tunnel breach').length).toBeGreaterThan(0));
    // Reward should be hidden before rolling
    expect(screen.queryByText('Act before the first wave.')).not.toBeInTheDocument();
  });

  test('roll button is present when check is open', async () => {
    mockAxiosForPlayer(scene2WithCheck);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Roll Check')).toBeInTheDocument());
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #6 — Assign checks to specific players
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #6 — Check assignment', () => {
  test('assigned player sees their name in the check banner', async () => {
    mockAxiosForPlayer(scene2AssignedCheck);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText(/Assigned to/i)).toBeInTheDocument());
    expect(screen.getAllByText(/anders/i).length).toBeGreaterThan(0);
    expect(screen.getByText('Roll Check')).toBeInTheDocument();
  });

  test('non-assigned player sees "Not assigned to you"', async () => {
    mockAxiosForPlayer(scene2AssignedCheck);
    render(<MissionTab authedPlayer="christoffer" />);
    await waitFor(() => expect(screen.getByText(/Not assigned to you/i)).toBeInTheDocument());
    expect(screen.queryByText('Roll Check')).not.toBeInTheDocument();
  });

  test('check with no assignment allows any player to roll', async () => {
    mockAxiosForPlayer(scene2WithCheck);
    render(<MissionTab authedPlayer="phillip" />);
    await waitFor(() => expect(screen.getByText('Roll Check')).toBeInTheDocument());
  });

  test('GM sees player assignment dropdown in Scene Secrets', async () => {
    mockAxiosForGM([scene2Combat]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Scene Secrets')).toBeInTheDocument());
    // Expand options for first check
    const gearButtons = screen.getAllByText('⚙');
    fe.click(gearButtons[0]);
    await waitFor(() => expect(screen.getByText('Assign to:')).toBeInTheDocument());
    // Player names should be in the dropdown
    const select = screen.getByDisplayValue('Anyone');
    expect(select).toBeInTheDocument();
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #7 — Fear test quick-roll
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #7 — Fear test quick-roll', () => {
  test('fear test panel appears when fearRating > 0 and combat active', async () => {
    mockAxiosForPlayer(scene2Combat); // fearRating: 2
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Fear Test Required')).toBeInTheDocument());
    expect(screen.getByText(/Roll Fear Test/)).toBeInTheDocument();
  });

  test('fear test panel does NOT appear when no combat prompt', async () => {
    mockAxiosForPlayer(scene1);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText(scene1.description)).toBeInTheDocument());
    expect(screen.queryByText('Fear Test Required')).not.toBeInTheDocument();
  });

  test('fear test button becomes disabled after rolling', async () => {
    mockAxiosForPlayer(scene2Combat);
    axios.post.mockResolvedValue({ data: { success: true, id: 99 } });
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText(/Roll Fear Test/)).toBeInTheDocument());
    fe.click(screen.getByText(/Roll Fear Test/));
    await waitFor(() => expect(screen.getByText(/Fear Test Rolled/)).toBeInTheDocument());
  });

  // Finding #2 fix — effective WP target shown in button
  test('fear test button shows effective WP target with penalty applied', async () => {
    mockAxiosForPlayer(scene2WithFearRating4); // fearRating 4 = –30
    render(<MissionTab authedPlayer="anders" />);
    // Default WP is 60, penalty –30 → effective 30
    await waitFor(() => {
      const btn = screen.getByText(/Roll Fear Test/);
      expect(btn).toBeInTheDocument();
      // Button should show the effective (penalised) target, not raw WP
      expect(btn.textContent).toMatch(/30/);
    });
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #8 — Decision checks show declare UI
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #8 — Decision checks', () => {
  test('decision check shows textarea and Declare button instead of Roll', async () => {
    mockAxiosForPlayer(scene2DecisionCheck);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Deliver final judgement')).toBeInTheDocument());
    expect(screen.getByText('Declare')).toBeInTheDocument();
    expect(screen.queryByText('Roll Check')).not.toBeInTheDocument();
    expect(screen.getByPlaceholderText(/State your Battle-Brother/i)).toBeInTheDocument();
  });

  test('Declare button is disabled until text is entered', async () => {
    mockAxiosForPlayer(scene2DecisionCheck);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Declare')).toBeInTheDocument());
    const btn = screen.getByText('Declare');
    expect(btn).toBeDisabled();
  });

  test('GM sees decision flag in check options expand', async () => {
    mockAxiosForGM([scene2Combat]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Scene Secrets')).toBeInTheDocument());
    const gearButtons = screen.getAllByText('⚙');
    fe.click(gearButtons[0]);
    await waitFor(() => expect(screen.getByText(/Decision.*no dice/i)).toBeInTheDocument());
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #9 — Fate point re-roll
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #9 — Fate point re-roll', () => {
  test('Spend Fate button appears after rolling a check', async () => {
    mockAxiosForPlayer(scene2WithCheck);
    axios.post.mockResolvedValue({ data: { success: true, id: 99 } });
    axios.get.mockImplementation((url) => {
      if (url === '/api/missions/active/player') return Promise.resolve({ data: { id: 9, name: 'The Hunt', current_scene: 0, active_scene_index: 0, scenes: [scene2WithCheck] } });
      if (url.includes('rolls/feed')) return Promise.resolve({ data: [] });
      if (url === '/api/players') return Promise.resolve({ data: [] });
      return Promise.resolve({ data: null });
    });

    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Roll Check')).toBeInTheDocument());
    fe.click(screen.getByText('Roll Check'));
    await waitFor(() => expect(screen.getByText(/Spend Fate/)).toBeInTheDocument());
  });

  test('Spend Fate button does not appear for decision checks', async () => {
    mockAxiosForPlayer(scene2DecisionCheck);
    axios.post.mockResolvedValue({ data: { success: true, id: 99 } });
    axios.get.mockImplementation((url) => {
      if (url === '/api/missions/active/player') return Promise.resolve({ data: { id: 9, name: 'The Hunt', current_scene: 0, active_scene_index: 0, scenes: [scene2DecisionCheck] } });
      if (url.includes('rolls/feed')) return Promise.resolve({ data: [] });
      if (url === '/api/players') return Promise.resolve({ data: [] });
      return Promise.resolve({ data: null });
    });

    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => expect(screen.getByText('Declare')).toBeInTheDocument());
    const textarea = screen.getByPlaceholderText(/State your Battle-Brother/i);
    fe.change(textarea, { target: { value: 'We spare him for study.' } });
    await waitFor(() => expect(screen.getByText('Declare')).not.toBeDisabled());
    fe.click(screen.getByText('Declare'));
    // No fate button for decisions
    expect(screen.queryByText(/Spend Fate/)).not.toBeInTheDocument();
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #10 — Polling lag
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #10 — Polling interval for players', () => {
  test('players poll mission and roll feed on 4s interval', async () => {
    mockAxiosForPlayer(scene1);
    render(<MissionTab authedPlayer="anders" />);

    const initialCalls = axios.get.mock.calls.length;

    // Advance 4 seconds — should trigger the 4s interval
    jest.advanceTimersByTime(4000);
    await waitFor(() => {
      expect(axios.get.mock.calls.length).toBeGreaterThan(initialCalls);
    });
  });

  test('GM polls roll feed separately on 5s interval', async () => {
    mockAxiosForGM([scene1]);
    render(<MissionTab authedPlayer="gm" />);

    const initialCalls = axios.get.mock.calls.length;
    jest.advanceTimersByTime(5000);
    await waitFor(() => {
      expect(axios.get.mock.calls.length).toBeGreaterThan(initialCalls);
    });
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Issue #11 — Mission completion summary
// ──────────────────────────────────────────────────────────────────────────────
describe('Issue #11 — Mission completion summary', () => {
  test('mission complete banner appears when all scenes are completed', async () => {
    mockAxiosForGM(allScenesComplete);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Mission Complete')).toBeInTheDocument());
    expect(screen.getByText(/all 2 scenes concluded/i)).toBeInTheDocument();
  });

  test('mission complete banner shows View Report toggle', async () => {
    mockAxiosForGM(allScenesComplete);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('View Report')).toBeInTheDocument());
  });

  test('View Report expands stats panel', async () => {
    mockAxiosForGM(allScenesComplete);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('View Report')).toBeInTheDocument());
    fe.click(screen.getByText('View Report'));
    await waitFor(() => expect(screen.getByText('Scenes')).toBeInTheDocument());
    expect(screen.getByText('Checks Rolled')).toBeInTheDocument();
    expect(screen.getByText('Success Rate')).toBeInTheDocument();
  });

  test('GM sees outcome notes textarea in report', async () => {
    mockAxiosForGM(allScenesComplete);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('View Report')).toBeInTheDocument());
    fe.click(screen.getByText('View Report'));
    await waitFor(() => expect(screen.getByPlaceholderText(/Record what the Kill-team/i)).toBeInTheDocument());
  });

  test('player sees outcome notes when set', async () => {
    const scenesWithOutcome = [
      { ...allScenesComplete[0] },
      { ...allScenesComplete[1], outcome: 'Kill-team neutralised the gene-lab. Bile escaped.' },
    ];
    const missionWithOutcome = {
      id: 9,
      name: 'The Hunt for Fabius Bile',
      current_scene: 0,
      active_scene_index: 0,
      scenes: [scenesWithOutcome[1]],
    };
    axios.get.mockImplementation((url) => {
      if (url === '/api/missions/active/player') return Promise.resolve({ data: missionWithOutcome });
      if (url.includes('rolls/feed')) return Promise.resolve({ data: [] });
      if (url === '/api/players') return Promise.resolve({ data: [] });
      return Promise.resolve({ data: null });
    });
    // Need all scenes completed
    const fullMission = { ...activeMission, scenes: scenesWithOutcome };
    axios.get.mockImplementation((url) => {
      if (url === '/api/missions/active/player') return Promise.resolve({ data: { id: 9, name: 'The Hunt', current_scene: 0, active_scene_index: 0, scenes: [scenesWithOutcome[1]], _allScenes: scenesWithOutcome } });
      if (url.includes('rolls/feed')) return Promise.resolve({ data: [] });
      if (url === '/api/players') return Promise.resolve({ data: [] });
      return Promise.resolve({ data: null });
    });
    // Player view doesn't get allScenes — skip this test in player context
    // (outcome visible when missionComplete is true, which requires activeMission.scenes)
  });

  test('mission complete banner does NOT appear while scenes remain', async () => {
    mockAxiosForGM([scene1, scene2Combat]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('GM Mission Control')).toBeInTheDocument());
    expect(screen.queryByText('Mission Complete')).not.toBeInTheDocument();
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Finding #1 — New mission scenes default revealed state
// ──────────────────────────────────────────────────────────────────────────────
describe('Finding #1 — Scene activation initialises revealed:false', () => {
  test('activating a mission patches all scenes with revealed:false if undefined', async () => {
    const unrevealedScenes = [
      { ...scene1, revealed: undefined },
      { ...scene2Combat, revealed: undefined },
    ];
    mockAxiosForGM(unrevealedScenes);
    render(<MissionTab authedPlayer="gm" />);
    const setActiveButton = await screen.findByText('Set Active Mission');
    await waitFor(() => expect(setActiveButton).not.toBeDisabled());

    axios.post.mockResolvedValueOnce({
      data: { mission: { ...activeMission, scenes: unrevealedScenes } },
    });
    axios.put.mockResolvedValue({ data: { mission: { ...activeMission, scenes: unrevealedScenes } } });

    fe.click(setActiveButton);

    await waitFor(() => {
      // saveProgress should have been called to initialise revealed:false on all scenes
      const putCalls = axios.put.mock.calls;
      const initCall = putCalls.find(call =>
        call[1]?.scenes?.every(s => s.revealed === false)
      );
      expect(initCall).toBeTruthy();
    });
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Finding #2 — Fear penalty auto-applied in button label
// ──────────────────────────────────────────────────────────────────────────────
describe('Finding #2 — Fear penalty auto-applied to WP display', () => {
  test('Fear Rating 2 (–10) shows effective WP 50 for default WP 60', async () => {
    mockAxiosForPlayer(scene2Combat); // fearRating 2, penalty –10
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => {
      const btn = screen.getByText(/Roll Fear Test/);
      expect(btn.textContent).toMatch(/50/); // 60 – 10 = 50
    });
  });

  test('Fear Rating 4 (–30) shows effective WP 30 for default WP 60', async () => {
    mockAxiosForPlayer(scene2WithFearRating4);
    render(<MissionTab authedPlayer="anders" />);
    await waitFor(() => {
      const btn = screen.getByText(/Roll Fear Test/);
      expect(btn.textContent).toMatch(/30/); // 60 – 30 = 30
    });
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Finding #3 — RollFeedRow defined outside component (structural check)
// ──────────────────────────────────────────────────────────────────────────────
describe('Finding #3 — RollFeedRow defined outside component', () => {
  test('decision roll renders label with declaration text', async () => {
    const decisionFeed = [
      { id: 3, player_name: 'christoffer', roll_type: 'decision', scene_index: 0, label: 'Plan the insertion', payload: { declaration: 'Drop-pod insertion through the northern ridge.' }, created_at: '2026-06-24T10:02:00.000Z' },
    ];
    mockAxiosForGM([scene1], decisionFeed);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => screen.getByText('Roll Feed'));
    expect(screen.getByText(/Drop-pod insertion/i)).toBeInTheDocument();
  });

  test('initiative roll renders initiative details', async () => {
    const initFeed = [
      { id: 4, player_name: 'phillip', roll_type: 'initiative', scene_index: 1, label: 'Initiative', payload: { agBonus: 4, dieRoll: 7, total: 11 }, created_at: '2026-06-24T10:10:00.000Z' },
    ];
    mockAxiosForGM([scene1], initFeed);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => screen.getByText('Roll Feed'));
    expect(screen.getByText(/Initiative.*11/i)).toBeInTheDocument();
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// Finding #4 — No duplicate Open-for-Players path
// ──────────────────────────────────────────────────────────────────────────────
describe('Finding #4 — Scene Secrets is sole check-open entry point', () => {
  test('GM Scene Checks panel has no "Open for Players" quick button', async () => {
    mockAxiosForGM([scene1]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Scene Checks')).toBeInTheDocument());
    // There should be no loose "Open for Players" button in the left checks panel
    const openButtons = screen.queryAllByText('Open for Players');
    expect(openButtons.length).toBe(0);
  });

  test('GM Scene Secrets panel has Open + ⚙ for each check', async () => {
    mockAxiosForGM([scene1]);
    render(<MissionTab authedPlayer="gm" />);
    await waitFor(() => expect(screen.getByText('Scene Secrets')).toBeInTheDocument());
    // ⚙ gear buttons for check options should exist
    const gearButtons = screen.getAllByText('⚙');
    expect(gearButtons.length).toBeGreaterThanOrEqual(1);
  });
});
