import json
import random
from pathlib import Path

import pytest
from coolrl_lost_cities.games.classic.game import Card, GameState, LostCitiesConfig

import coolrl_lost_cities.games.classic as classic
from tests.games.classic.helpers import make_state

FIXTURE_DIR = Path(classic.__file__).resolve().parent / "fixtures"


def _small_config() -> LostCitiesConfig:
    return LostCitiesConfig(
        n_colors=2,
        n_ranks=2,
        min_rank=1,
        n_handshakes=0,
        hand_size=1,
        expedition_penalty=0,
        bonus_threshold=99,
        bonus_amount=0,
    )


def test_new_game_from_deck_uses_explicit_internal_deck_order() -> None:
    config = _small_config()
    state = GameState.new_game_from_deck(
        [
            Card(0, 1),
            Card(0, 2),
            Card(1, 1),
            Card(1, 2),
        ],
        config,
    )

    assert state.hands == [[Card(1, 2)], [Card(1, 1)]]
    assert state.deck == [Card(0, 1), Card(0, 2)]
    state.validate_invariants()


def test_new_game_preserves_dealt_hand_order() -> None:
    config = LostCitiesConfig(
        n_colors=2,
        n_ranks=3,
        min_rank=1,
        n_handshakes=0,
        hand_size=2,
        expedition_penalty=0,
        bonus_threshold=99,
        bonus_amount=0,
    )
    state = GameState.new_game_from_deck(
        [
            Card(0, 1),
            Card(0, 2),
            Card(0, 3),
            Card(1, 1),
            Card(1, 2),
            Card(1, 3),
        ],
        config,
    )

    assert state.hands[0] == [Card(1, 3), Card(1, 1)]
    assert state.hands[1] == [Card(1, 2), Card(0, 3)]
    assert state.deck == [Card(0, 1), Card(0, 2)]
    state.validate_invariants()


def test_snapshot_roundtrip_preserves_json_state() -> None:
    state = GameState.new_game(LostCitiesConfig(seed=5))
    first_action = next(index for index, legal in enumerate(state.unified_legal_mask()) if legal)
    state.apply_unified_action(first_action)
    second_action = next(index for index, legal in enumerate(state.unified_legal_mask()) if legal)
    state.apply_unified_action(second_action)

    payload = json.loads(json.dumps(state.to_snapshot()))
    restored = GameState.from_snapshot(payload)

    assert restored.to_snapshot() == state.to_snapshot()
    restored.validate_invariants()


def test_validate_invariants_detects_card_loss() -> None:
    state = GameState.new_game(LostCitiesConfig(seed=7))
    snapshot = state.to_snapshot()
    snapshot["deck"].pop()
    broken = GameState.from_snapshot(snapshot, validate=False)

    with pytest.raises(ValueError, match="card conservation"):
        broken.validate_invariants()


def test_validate_invariants_detects_bad_expedition_order() -> None:
    config = LostCitiesConfig(seed=8)
    state = make_state(
        config,
        deck=GameState.new_game(config).deck,
        expeditions=[[[Card(0, 2), Card(0, 1)], [], [], [], []], [[], [], [], [], []]],
    )

    with pytest.raises(ValueError, match="strictly increasing"):
        state.validate_invariants()


def test_canonical_small_fixture_matches_expected_trace() -> None:
    fixture = json.loads((FIXTURE_DIR / "canonical_small.json").read_text())
    config = LostCitiesConfig(**fixture["config"])
    state = GameState.new_game_from_deck(fixture["initial_deck"], config)

    for step in fixture["steps"]:
        if step["action"] is not None:
            state.apply_unified_action(step["action"])
        assert state.phase == step["phase"]
        assert state.current_player == step["current_player"]
        assert state.turn_count == step["turn_count"]
        assert state.terminal is step["terminal"]
        assert state.score_diff(0) == step["score_diff_player0"]
        assert state.unified_legal_mask() == step["legal_mask"]
        state.validate_invariants()


def test_random_games_preserve_python_core_invariants() -> None:
    config = LostCitiesConfig(
        n_colors=3,
        n_ranks=5,
        min_rank=2,
        n_handshakes=1,
        hand_size=5,
    )
    for seed in range(128):
        state = GameState.new_game(config, seed=seed)
        rng = random.Random(seed ^ 0x5EED)
        steps = 0
        while not state.terminal:
            state.validate_invariants()
            legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
            state.apply_unified_action(rng.choice(legal))
            steps += 1
            assert steps < 1000
        state.validate_invariants()


def test_apply_action_with_undo_restores_every_legal_action() -> None:
    config = LostCitiesConfig(
        n_colors=3,
        n_ranks=5,
        min_rank=2,
        n_handshakes=1,
        hand_size=5,
    )
    for seed in range(32):
        state = GameState.new_game(config, seed=seed)
        rng = random.Random(seed ^ 0xA11CE)
        steps = 0
        while not state.terminal:
            legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
            for unified_action in legal:
                candidate = state.clone()
                before = candidate.to_snapshot()
                undo = candidate.apply_unified_action_with_undo(unified_action)
                candidate.undo_action(undo)
                assert candidate.to_snapshot() == before
                candidate.validate_invariants()

            state.apply_unified_action(rng.choice(legal))
            steps += 1
            assert steps < 1000


def test_apply_action_with_undo_matches_apply_action_result() -> None:
    config = LostCitiesConfig(
        n_colors=3,
        n_ranks=5,
        min_rank=2,
        n_handshakes=1,
        hand_size=5,
    )
    for seed in range(32):
        state = GameState.new_game(config, seed=seed)
        rng = random.Random(seed ^ 0xC0FFEE)
        steps = 0
        while not state.terminal:
            legal = [index for index, is_legal in enumerate(state.unified_legal_mask()) if is_legal]
            action = rng.choice(legal)
            left = state.clone()
            right = state.clone()

            left.apply_unified_action(action)
            right.apply_unified_action_with_undo(action)

            assert right.to_snapshot() == left.to_snapshot()
            state = left
            steps += 1
            assert steps < 1000


def test_same_seed_and_action_sequence_are_deterministic() -> None:
    config = LostCitiesConfig(seed=1234)
    left = GameState.new_game(config)
    right = GameState.new_game(config)

    while True:
        assert left.to_snapshot() == right.to_snapshot()
        if left.terminal:
            break

        action = next(index for index, is_legal in enumerate(left.unified_legal_mask()) if is_legal)
        left.apply_unified_action(action)
        right.apply_unified_action(action)
