from __future__ import annotations

import random
import re

from gungi.fen import ADVANCED_POSITION, BEGINNER_POSITION
from gungi.gungi import Gungi


def _collect_piece_ids(game: Gungi) -> tuple[list[str], list[str]]:
    board_ids: list[str] = []
    hand_ids: list[str] = []

    for rank in range(1, 10):
        for file in range(1, 10):
            tower = game.get(f"{rank}-{file}")
            if not tower:
                continue
            for piece in tower:
                if piece and piece.id:
                    board_ids.append(piece.id)

    for hand_piece in game.hand():
        if hand_piece.id:
            hand_ids.append(hand_piece.id)

    return board_ids, hand_ids


def _assert_unique_ids(board_ids: list[str], hand_ids: list[str]) -> None:
    all_ids = [*board_ids, *hand_ids]
    assert len(set(all_ids)) == len(all_ids)


def _assert_id_format(ids: list[str], mode: int) -> None:
    pattern = re.compile(rf"^{mode}-[wb]-[^\W\d_]-\d+$")
    assert all(pattern.match(identifier) for identifier in ids)


def test_piece_id_invariants_hold_through_random_advanced_game() -> None:
    rng = random.Random(20260306)
    game = Gungi(ADVANCED_POSITION)

    board_ids, hand_ids = _collect_piece_ids(game)
    _assert_unique_ids(board_ids, hand_ids)

    for _ in range(200):
        if game.is_game_over():
            break

        legal_moves = game.moves()
        if not legal_moves:
            break

        game.move(rng.choice(legal_moves))
        board_ids, hand_ids = _collect_piece_ids(game)
        _assert_id_format([*board_ids, *hand_ids], mode=3)
        _assert_unique_ids(board_ids, hand_ids)


def test_beginner_arata_board_piece_inherits_hand_id_without_conflicts() -> None:
    game = Gungi(BEGINNER_POSITION)
    original_hand_piece = next(hp for hp in game.hand("w") if hp.type == "\u5c0f")
    original_hand_id = original_hand_piece.id
    original_hand_count = original_hand_piece.count

    move = next(san for san in game.moves() if san.startswith("\u65b0\u5c0f"))
    game.move(move)

    new_hand_piece = next((hp for hp in game.hand("w") if hp.type == "\u5c0f"), None)
    if original_hand_count > 1:
        assert new_hand_piece is not None
        assert new_hand_piece.count == original_hand_count - 1
        assert new_hand_piece.id != original_hand_id
    else:
        assert new_hand_piece is None

    placed_piece = next(
        (
            game.get_top(f"{rank}-{file}")
            for rank in range(1, 10)
            for file in range(1, 10)
            if (piece := game.get_top(f"{rank}-{file}"))
            and piece.type == "\u5c0f"
            and piece.color == "w"
        ),
        None,
    )

    assert placed_piece is not None
    assert placed_piece.id == original_hand_id

    board_ids, hand_ids = _collect_piece_ids(game)
    _assert_id_format([*board_ids, *hand_ids], mode=1)
    _assert_unique_ids(board_ids, hand_ids)


def test_multiple_consecutive_arata_moves_keep_ids_unique() -> None:
    rng = random.Random(20260307)
    game = Gungi(ADVANCED_POSITION)

    for _ in range(10):
        if game.is_game_over():
            break

        legal_moves = game.moves()
        if not legal_moves:
            break

        game.move(rng.choice(legal_moves))
        board_ids, hand_ids = _collect_piece_ids(game)
        _assert_id_format([*board_ids, *hand_ids], mode=3)
        _assert_unique_ids(board_ids, hand_ids)
