// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import GameConfigScreen from './GameConfigScreen';

const mockApi = vi.hoisted(() => ({ getPlayerSessions: vi.fn() }));
vi.mock('../api', () => ({ api: mockApi }));

beforeEach(() => {
  mockApi.getPlayerSessions.mockResolvedValue({
    player: 'amarot', session_count: 2,
    session_names: ['amarot — session 1', 'amarot — session 2'],
  });
});
afterEach(() => { cleanup(); vi.clearAllMocks(); });

const sessionInput = () => screen.getByTestId('game-session-name') as HTMLInputElement;

/** France THT / Matpower live behind Configure settings' mode toggle. */
const openModePicker = () => {
  fireEvent.click(screen.getByTestId('game-settings-toggle'));
  fireEvent.click(screen.getByTestId('game-mode-picker-toggle'));
};

describe('GameConfigScreen landing', () => {
  it('keeps Start disabled until a player name is entered', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    expect(screen.getByTestId('game-start')).toBeDisabled();
  });

  it('auto-fills the session name from the player + next session index', async () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
    expect(mockApi.getPlayerSessions).toHaveBeenCalledWith('amarot');
  });

  it('falls back to session 1 when the backend is unreachable', async () => {
    mockApi.getPlayerSessions.mockRejectedValue(new Error('offline'));
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'zoe' } });
    await waitFor(() => expect(sessionInput().value).toBe('zoe — session 1'));
  });

  it('does not overwrite a session name the user typed', async () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.change(sessionInput(), { target: { value: 'My custom run' } });
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await new Promise((r) => setTimeout(r, 400));
    expect(sessionInput().value).toBe('My custom run');
    // The names are still fetched (they drive the duplicate block), but the
    // typed name is preserved.
    expect(mockApi.getPlayerSessions).toHaveBeenCalledWith('amarot');
  });

  it('auto-suggests the first FREE index, skipping gaps in the recorded names', async () => {
    // Recorded {1, 3} → the count-plus-one heuristic would collide on 3; the
    // names-based suggestion fills the gap and picks 2.
    mockApi.getPlayerSessions.mockResolvedValue({
      player: 'amarot', session_count: 2,
      session_names: ['amarot — session 1', 'amarot — session 3'],
    });
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 2'));
  });

  it('suggests the next index after finishing a session (no collision)', async () => {
    // Sessions 1-3 already recorded → the next free index is 4, never a
    // re-suggested 3 (the reported bug).
    mockApi.getPlayerSessions.mockResolvedValue({
      player: 'amarot', session_count: 3,
      session_names: ['amarot — session 1', 'amarot — session 2', 'amarot — session 3'],
    });
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 4'));
  });

  it('blocks Start and shows an error when the name collides with an existing session', async () => {
    const onStart = vi.fn();
    render(<GameConfigScreen onStart={onStart} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
    // Type a name that already exists (case-insensitive).
    fireEvent.change(sessionInput(), { target: { value: 'Amarot — Session 1' } });
    await waitFor(() => expect(screen.getByTestId('game-session-name-error')).toBeInTheDocument());
    expect(screen.getByTestId('game-start')).toBeDisabled();
    fireEvent.click(screen.getByTestId('game-start'));
    expect(onStart).not.toHaveBeenCalled();
  });

  it('re-enables Start once the colliding name is changed to a free one', async () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
    fireEvent.change(sessionInput(), { target: { value: 'amarot — session 2' } });
    await waitFor(() => expect(screen.getByTestId('game-start')).toBeDisabled());
    fireEvent.change(sessionInput(), { target: { value: 'amarot — session 9' } });
    expect(screen.queryByTestId('game-session-name-error')).toBeNull();
    expect(screen.getByTestId('game-start')).not.toBeDisabled();
  });

  it('lists the configured studies and shows the network preview', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    expect(screen.getByTestId('game-studies-summary').querySelectorAll('li').length)
      .toBeGreaterThan(0);
    expect(screen.getByTestId('game-network-preview')).toBeInTheDocument();
  });

  it('hides settings by default and reveals timer / mode toggle / studies on toggle', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    expect(screen.queryByText(/Time limit per study/)).not.toBeInTheDocument();
    fireEvent.click(screen.getByTestId('game-settings-toggle'));
    expect(screen.getByText(/Time limit per study/)).toBeInTheDocument();
    expect(screen.getByTestId('game-mode-picker-toggle')).toBeInTheDocument();
    expect(screen.getByText(/Studies \(/)).toBeInTheDocument();
  });

  it('keeps other modes and the network-difficulty picker hidden until the mode toggle is used', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    fireEvent.click(screen.getByTestId('game-settings-toggle'));
    expect(screen.queryByTestId('game-mode-tht')).not.toBeInTheDocument();
    expect(screen.queryByTestId('game-mode-matpower')).not.toBeInTheDocument();
    expect(screen.queryByText(/Difficulty \(network\)/)).not.toBeInTheDocument();

    fireEvent.click(screen.getByTestId('game-mode-picker-toggle'));
    expect(screen.getByTestId('game-mode-demo')).toBeInTheDocument();
    expect(screen.getByTestId('game-mode-tht')).toBeInTheDocument();
    expect(screen.getByTestId('game-mode-matpower')).toBeInTheDocument();
    // Demo is still the active mode, so its network-difficulty select shows too.
    expect(screen.getByText(/Difficulty \(network\)/)).toBeInTheDocument();
  });

  it('starts a session with the entered config', async () => {
    const onStart = vi.fn();
    render(<GameConfigScreen onStart={onStart} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
    fireEvent.click(screen.getByTestId('game-start'));

    expect(onStart).toHaveBeenCalledTimes(1);
    const cfg = onStart.mock.calls[0][0];
    expect(cfg.player).toBe('amarot');
    expect(cfg.sessionName).toBe('amarot — session 3');
    expect(cfg.assistance).toBe(true);
    expect(cfg.timerSeconds).toBe(300);
    expect(cfg.studies.length).toBeGreaterThan(0);
  });
});

describe('GameConfigScreen — France THT mode', () => {
  it('reveals the difficulty + case-count pickers and hides the demo studies/preview', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    // Demo is the default: its studies summary + network preview are shown.
    expect(screen.getByTestId('game-studies-summary')).toBeInTheDocument();
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-tht'));
    expect(screen.getByTestId('game-tht-difficulty')).toBeInTheDocument();
    expect(screen.getByTestId('game-tht-count')).toBeInTheDocument();
    expect(screen.getByTestId('game-tht-summary')).toBeInTheDocument();
    // France THT shows its own network map (the shared RTE7000 backbone).
    expect(screen.getByTestId('game-tht-preview')).toBeInTheDocument();
    // The demo studies list + per-network preview belong to demo mode only.
    expect(screen.queryByTestId('game-studies-summary')).not.toBeInTheDocument();
    expect(screen.queryByTestId('game-network-preview')).not.toBeInTheDocument();
  });

  it('starts a France THT session by sampling the chosen number of cases', async () => {
    const onStart = vi.fn();
    render(<GameConfigScreen onStart={onStart} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-tht'));
    fireEvent.change(screen.getByTestId('game-tht-count'), { target: { value: '3' } });
    fireEvent.click(screen.getByTestId('game-start'));

    expect(onStart).toHaveBeenCalledTimes(1);
    const cfg = onStart.mock.calls[0][0];
    expect(cfg.studies).toHaveLength(3);
    for (const s of cfg.studies) {
      expect(s.networkPath).toContain('data/rte7000_tht/grids/');
      expect(s.contingencyElementId).toBeTruthy();
    }
  });

  it('caps the number of cases at the pool size', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-tht'));
    const count = screen.getByTestId('game-tht-count') as HTMLInputElement;
    fireEvent.change(count, { target: { value: '999999' } });
    // Clamped to the difficulty's available cases (a positive, finite pool).
    expect(Number(count.value)).toBeGreaterThan(0);
    expect(Number(count.value)).toBeLessThan(999999);
  });

  it('still requires a player name to start in THT mode', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-tht'));
    expect(screen.getByTestId('game-start')).toBeDisabled();
  });
});

describe('GameConfigScreen — France EHV (Matpower) mode', () => {
  it('is a third top-level mode, exclusive with demo and France THT', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-matpower'));
    expect(screen.getByTestId('game-matpower-difficulty')).toBeInTheDocument();
    expect(screen.getByTestId('game-matpower-count')).toBeInTheDocument();
    expect(screen.getByTestId('game-matpower-summary')).toBeInTheDocument();
    expect(screen.getByTestId('game-matpower-preview')).toBeInTheDocument();
    // The other two modes' panels are gone.
    expect(screen.queryByTestId('game-tht-summary')).not.toBeInTheDocument();
    expect(screen.queryByTestId('game-studies-summary')).not.toBeInTheDocument();
    // ...and switching back to France THT restores its own panel.
    fireEvent.click(screen.getByTestId('game-mode-tht'));
    expect(screen.getByTestId('game-tht-summary')).toBeInTheDocument();
    expect(screen.queryByTestId('game-matpower-summary')).not.toBeInTheDocument();
  });

  it('samples its studies from the Matpower grids, not the THT ones', async () => {
    const onStart = vi.fn();
    render(<GameConfigScreen onStart={onStart} />);
    fireEvent.change(screen.getByTestId('game-player'), { target: { value: 'amarot' } });
    await waitFor(() => expect(sessionInput().value).toBe('amarot — session 3'));
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-matpower'));
    fireEvent.change(screen.getByTestId('game-matpower-count'), { target: { value: '1' } });
    fireEvent.click(screen.getByTestId('game-start'));

    expect(onStart).toHaveBeenCalledTimes(1);
    const cfg = onStart.mock.calls[0][0];
    expect(cfg.studies).toHaveLength(1);
    for (const s of cfg.studies) {
      expect(s.networkPath).toContain('data/rte_matpower/grids/');
      expect(s.contingencyElementId).toBeTruthy();
    }
  });

  it('still requires a player name to start', () => {
    render(<GameConfigScreen onStart={vi.fn()} />);
    openModePicker();
    fireEvent.click(screen.getByTestId('game-mode-matpower'));
    expect(screen.getByTestId('game-start')).toBeDisabled();
  });
});
