"""Testing utilities for creating reproducible game scenarios."""

from __future__ import annotations

import itertools
import random
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from unittest.mock import MagicMock

from magsim.core.registry import RACER_ABILITIES
from magsim.core.state import (
    GameRules,
    GameState,
    LogContext,
    RacerState,
)
from magsim.engine import ENGINE_ID_COUNTER
from magsim.engine.board import BOARD_DEFINITIONS, Board
from magsim.engine.game_engine import GameEngine

if TYPE_CHECKING:
    from magsim.core.agent import Agent
    from magsim.core.types import AbilityName, RacerName


@dataclass
class RacerConfig:
    """Configuration for a single racer in a scenario."""

    idx: int
    name: RacerName
    abilities: set[AbilityName] | None = None
    start_pos: int = 0
    agent: Agent | None = None

    def __post_init__(self):
        if self.abilities is None:
            if self.name not in RACER_ABILITIES:
                msg = f"Racer '{self.name}' not found in RACER_ABILITIES."
                raise ValueError(msg)

            defaults = RACER_ABILITIES[self.name]

            if not defaults:
                msg = f"Racer '{self.name}' has no default abilities defined."
                raise ValueError(msg)

            self.abilities = defaults.copy()


@dataclass
class GameScenario:
    """
    A reusable harness for creating controlled game scenarios.
    """

    racers_config: list[RacerConfig]
    dice_rolls: list[int] | None = None
    board: Board | None = None
    rules: GameRules | None = None
    seed: int | None = None

    # These are set in __post_init__
    state: GameState = field(init=False)
    engine: GameEngine = field(init=False)
    mock_rng: MagicMock | None = field(init=False, default=None)

    def __post_init__(self):
        racers: list[RacerState] = []
        agents: dict[int, Agent] = {}

        # Setup racers from config
        for cfg in self.racers_config:
            r = RacerState(cfg.idx, cfg.name, position=cfg.start_pos)
            racers.append(r)
            if cfg.agent:
                agents[cfg.idx] = cfg.agent

        # Choose RNG strategy
        # 1. Create a real RNG base (deterministic if seed is provided)
        base_rng = (
            random.Random(self.seed) if self.seed is not None else random.Random()
        )

        if self.dice_rolls is not None:
            # 2. Wrap the real RNG so sample(), choices(), etc., still work natively
            self.mock_rng = MagicMock(wraps=base_rng)
            # 3. Intercept ONLY randint for the fixed dice rolls
            self.mock_rng.randint.side_effect = itertools.cycle(self.dice_rolls)
            rng = self.mock_rng
        else:
            self.mock_rng = None
            rng = base_rng

        # Initialize engine
        board = (
            self.board if self.board is not None else BOARD_DEFINITIONS["Standard"]()
        )
        rules = self.rules if self.rules is not None else GameRules()

        self.state = GameState(racers, board=board, rules=rules)

        engine_id = next(ENGINE_ID_COUNTER)
        self.engine = GameEngine(
            self.state,
            rng,
            log_context=LogContext(
                engine_id=engine_id,
                engine_level=0,
                parent_engine_id=None,
            ),
            agents=agents,
        )

    def set_dice_rolls(self, rolls: list[int]):
        if self.mock_rng is None:
            msg = "Cannot set dice rolls when using a real Random instance."
            raise ValueError(msg)
        self.mock_rng.randint.side_effect = itertools.cycle(rolls)

    def run_turn(self):
        self.engine.run_turn()
        self.engine.advance_turn()

    def run_turns(self, n: int):
        for _ in range(n):
            if not self.engine.state.race_active:
                break
            self.run_turn()

    def get_racer(self, idx: int) -> RacerState:
        return self.engine.get_racer(idx)
