from __future__ import annotations

from typing import List, Tuple

import pytest

from catan.data import Resource
from catan.game import Game, GameConfig, Phase


def _prepare_main_phase(game: Game) -> None:
    game.phase = Phase.MAIN
    game.has_rolled = True
    game.pending_discards.clear()
    game.robber_move_required = False


def _assign_settlement(game: Game, player, hex_id: int, corner_index: int = 0) -> None:
    corner_id = game.board.hexes[hex_id].corners[corner_index]
    corner = game.board.corners[corner_id]
    corner.owner = player.name
    corner.building = "settlement"
    player.settlements.add(corner_id)


def _find_resource_hex(game: Game) -> Tuple[int, Resource, int]:
    for tile in game.board.hexes.values():
        if tile.resource != Resource.DESERT and tile.number_token:
            return tile.id, tile.resource, tile.number_token
    raise RuntimeError("No producible hex found")


def _find_edge_path(game: Game, length: int) -> Tuple[List[Tuple[int, int, int]], List[Tuple[int, int, int]]]:
    board = game.board

    def dfs(corner_id, path_edges, path_corners, visited_edges):
        if len(path_edges) == length:
            return list(path_edges), list(path_corners)
        for edge_id in board.corners[corner_id].edges:
            if edge_id in visited_edges:
                continue
            visited_edges.add(edge_id)
            edge = board.edges[edge_id]
            next_corner = next(c for c in edge.corners if c != corner_id)
            path_edges.append(edge_id)
            path_corners.append(next_corner)
            result = dfs(next_corner, path_edges, path_corners, visited_edges)
            if result:
                return result
            path_edges.pop()
            path_corners.pop()
            visited_edges.remove(edge_id)
        return None

    for start in board.corners:
        result = dfs(start, [], [start], set())
        if result:
            return result
    raise RuntimeError("Unable to find connected edge path")


def test_resource_shortage_blocks_multiple_players() -> None:
    game = Game(GameConfig(player_names=["A", "B", "C"]))
    hex_id, resource, token = _find_resource_hex(game)
    _assign_settlement(game, game.players[0], hex_id, 0)
    _assign_settlement(game, game.players[1], hex_id, 1)
    game.bank[resource] = 1

    game._distribute_resources(token)  # noqa: SLF001 - internal rule helper

    assert game.players[0].resources[resource] == 0
    assert game.players[1].resources[resource] == 0
    assert game.bank[resource] == 1


def test_resource_shortage_single_player_gets_remaining() -> None:
    game = Game(GameConfig(player_names=["A", "B", "C"]))
    hex_id, resource, token = _find_resource_hex(game)
    _assign_settlement(game, game.players[0], hex_id, 0)
    game.bank[resource] = 1

    game._distribute_resources(token)  # noqa: SLF001

    assert game.players[0].resources[resource] == 1
    assert game.bank[resource] == 0


def test_robber_requires_valid_victim() -> None:
    game = Game(GameConfig(player_names=["A", "B", "C"]))
    _prepare_main_phase(game)
    other = game.players[1]
    target_hex = next(h for h in game.board.hexes if h != game.board.robber_hex)
    _assign_settlement(game, other, target_hex, 0)
    other.resources[Resource.BRICK] = 1
    game.robber_move_required = True

    with pytest.raises(ValueError, match="victim"):
        game.move_robber(target_hex)
    with pytest.raises(ValueError, match="adjacent"):
        game.move_robber(target_hex, victim=game.players[2].name)

    empty_hex = next(
        hid
        for hid in game.board.hexes
        if hid not in {target_hex, game.board.robber_hex}
        and all(game.board.corners[c].owner is None for c in game.board.hexes[hid].corners)
    )
    game.robber_move_required = True
    with pytest.raises(ValueError, match="No valid victim"):
        game.move_robber(empty_hex, victim=other.name)

    game.robber_move_required = True
    game.move_robber(target_hex, victim=other.name)
    assert not game.robber_move_required


def test_trade_restrictions_enforced() -> None:
    game = Game(GameConfig(player_names=["A", "B", "C"]))
    _prepare_main_phase(game)
    player = game.current_player
    opponent = game.players[1]
    player.resources[Resource.BRICK] = 2
    opponent.resources[Resource.WOOL] = 2

    with pytest.raises(ValueError, match="yourself"):
        game.trade_with_player(player.name, {Resource.BRICK: 1}, {Resource.WOOL: 1})
    with pytest.raises(ValueError, match="both ways"):
        game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {})
    with pytest.raises(ValueError, match="positive"):
        game.trade_with_player(opponent.name, {Resource.BRICK: 0}, {Resource.WOOL: 1})
    with pytest.raises(ValueError, match="matching resources"):
        game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {Resource.BRICK: 1})

    game.trade_with_player(opponent.name, {Resource.BRICK: 1}, {Resource.WOOL: 1})
    assert player.resources[Resource.WOOL] == 1
    assert opponent.resources[Resource.BRICK] == 1


def test_settlement_breaks_longest_road() -> None:
    game = Game(GameConfig(player_names=["A", "B", "C"]))
    player = game.players[0]
    rival = game.players[1]
    edges, corners = _find_edge_path(game, 5)
    for edge_id in edges:
        game.board.edges[edge_id].owner = player.name
        player.roads.add(edge_id)
    game._update_longest_road()  # noqa: SLF001
    assert player.longest_road

    block_corner = corners[2]
    game._build_settlement(rival, block_corner, require_connection=False)  # noqa: SLF001
    assert not player.longest_road
