from __future__ import annotations

import pytest
from coolrl_lost_cities.games.classic.game import GameState, classic_config

pytest.importorskip("pygame")

from coolrl_lost_cities.games.classic import pygame_table  # noqa: E402


def test_argparser_defaults_to_final_candidate() -> None:
    args = pygame_table.build_argparser().parse_args([])

    assert args.checkpoint == str(pygame_table.DEFAULT_CHECKPOINT)
    assert args.seed is None
    assert args.width == 1600
    assert args.height == 1000
    assert args.offline is False
    assert args.export_dir == "exports"


def test_argparser_accepts_overrides() -> None:
    args = pygame_table.build_argparser().parse_args(
        ["--checkpoint", "/tmp/ckpt", "--seed", "3", "--offline"]
    )

    assert args.checkpoint == "/tmp/ckpt"
    assert args.seed == 3
    assert args.offline is True


def test_one_suit_per_classic_color() -> None:
    config = classic_config()
    assert len(pygame_table.SUITS) == config.n_colors
    assert len(pygame_table.SUIT_PAINTERS) == config.n_colors


def test_card_key_distinguishes_wagers_from_numbers() -> None:
    config = classic_config()
    state = GameState.new_game(config, seed=0)
    keys = [pygame_table.card_key(card, config.min_rank) for card in state.deck]
    wagers = [key for key in keys if key[1] == 0]
    numbers = [key for key in keys if key[1] != 0]
    assert all(config.min_rank <= value <= config.max_rank for _, value in numbers)
    assert all(0 <= color < config.n_colors for color, _ in keys)
    assert wagers  # a shuffled deck slice still contains wager cards


def test_layout_scales_between_window_sizes() -> None:
    small = pygame_table.Layout(1024, 720, 5)
    large = pygame_table.Layout(1920, 1200, 5)

    for layout in (small, large):
        assert layout.board_top < layout.discard_cy < layout.board_bottom
        assert len(layout.col_x) == 5
        slots = layout.hand_slots(8)
        assert len(slots) == 8
        assert slots[0][0] >= 0
        assert slots[-1][0] + layout.hand_w <= layout.w
        lane_center = (layout.col_x[0] + layout.col_x[-1] + layout.board_w) / 2
        assert abs(lane_center - layout.w / 2) <= 1
    assert large.hand_w >= small.hand_w


def _settle(app: pygame_table.TableApp) -> None:
    now = pygame_table.pygame.time.get_ticks() + 10_000
    app._assign_targets(now)
    for sprite in app.sprites:
        sprite.pos.update(sprite.target)
        sprite.release_at = 0
        sprite.pending_flip_at = None


def test_discard_draw_stays_public_and_survives_undo_redo() -> None:
    app = pygame_table.TableApp(seed=5, offline=True, headless=True)
    try:
        app.input_locked_until = 0
        # Human discards, draws from deck, then the rival takes that public card.
        human_discard = next(i for i in app.state.unified_legal_actions() if i % 2 == 1)
        discarded = app.state.hands[app.human_seat][human_discard // 2]
        app.apply_unified(human_discard)
        app.apply_unified(app.state.card_action_size)

        rival_discard = next(
            i
            for i in app.state.unified_legal_actions()
            if i % 2 == 1 and app.state.hands[app.ai_seat][i // 2].color != discarded.color
        )
        app.apply_unified(rival_discard)
        draw_discard = app.state.card_action_size + 1 + discarded.color
        assert app._legal(draw_discard)
        app.apply_unified(draw_discard)

        face = pygame_table.card_key(discarded, app.min_rank)
        assert app.public_hand_counts[app.ai_seat][face] == 1
        assert any(
            sprite.public and (sprite.color, sprite.value) == face
            for sprite in app.hand_zones[app.ai_seat]
        )

        before = app._copy_public_counts()
        app.undo()
        app.redo()
        assert app.public_hand_counts == before
        assert any(
            sprite.public and sprite.face_up and (sprite.color, sprite.value) == face
            for sprite in app.hand_zones[app.ai_seat]
        )
    finally:
        app.opponent.shutdown()
        pygame_table.pygame.quit()


def test_missing_checkpoint_is_visible_failure_not_heuristic_fallback(tmp_path) -> None:
    opponent = pygame_table.Opponent(tmp_path / "missing", None, seed=1, offline=False)
    try:
        opponent.load_future.result(timeout=5)
        assert opponent.policy is None
        assert opponent.load_error is not None
        assert opponent.label == "MODEL LOAD FAILED"
    finally:
        opponent.shutdown()


def test_offline_mode_explicitly_uses_heuristic() -> None:
    opponent = pygame_table.Opponent(None, None, seed=1, offline=True)
    try:
        opponent.load_future.result(timeout=5)
        assert opponent.policy is not None
        assert opponent.load_error is None
        assert "offline" in opponent.label
    finally:
        opponent.shutdown()


def test_finished_match_can_be_replayed_and_exported(tmp_path) -> None:
    app = pygame_table.TableApp(
        seed=29,
        offline=True,
        headless=True,
        export_dir=tmp_path,
    )
    try:
        while not app.state.terminal:
            app.apply_unified(app.state.unified_legal_actions()[0])

        final_index = len(app.match_steps) - 1
        assert app.review_index == final_index
        assert all(sprite.face_up for sprite in app.hand_zones[app.ai_seat])

        app.review_step(-1)
        assert app.review_index == final_index - 1
        app.enter_review(0)
        assert app.review_index == 0
        assert app.state.turn_count == 0
        assert all(sprite.face_up for sprite in app.hand_zones[app.ai_seat])

        path = app.export_match()
        restored = pygame_table.MatchRecord.read_jsonl(path)
        assert restored.metadata["complete"] is True
        assert len(restored.steps) == len(app.match_steps)
        assert restored.steps[-1]["state"]["terminal"] is True
    finally:
        app.opponent.shutdown()
        pygame_table.pygame.quit()


def test_undo_then_new_action_replaces_export_timeline() -> None:
    app = pygame_table.TableApp(seed=37, offline=True, headless=True)
    try:
        first = app.state.unified_legal_actions()[0]
        app.apply_unified(first)
        app.apply_unified(app.state.unified_legal_actions()[0])
        assert app.timeline_cursor == 2

        app.undo()
        assert app.timeline_cursor < len(app.match_steps) - 1
        replacement = app.state.unified_legal_actions()[-1]
        app.apply_unified(replacement)

        record = app.current_match_record()
        record.validate()
        assert len(record.steps) == app.timeline_cursor + 1
        assert record.steps[-1]["action_id"] == replacement
    finally:
        app.opponent.shutdown()
        pygame_table.pygame.quit()


def test_menu_exports_current_match(tmp_path) -> None:
    app = pygame_table.TableApp(
        seed=43,
        offline=True,
        headless=True,
        export_dir=tmp_path,
    )
    try:
        app.draw()
        app.on_click(app.menu_button_rect.center)
        assert app.menu_open is True
        app.draw()
        export_rect = next(rect for action, rect in app.menu_items if action == "export")
        app.on_click(export_rect.center)

        exported = list(tmp_path.glob("lost-cities-match-*.jsonl"))
        assert len(exported) == 1
        restored = pygame_table.MatchRecord.read_jsonl(exported[0])
        assert restored.metadata["complete"] is False
        assert len(restored.steps) == 1
    finally:
        app.opponent.shutdown()
        pygame_table.pygame.quit()
