import datetime
import uuid

import pytest
from sqlalchemy.exc import IntegrityError, NoResultFound

from idlemaxx.game import ActivityReward, Game
from idlemaxx.models import (
    CharacterItem,
    Skill,
    now_utc,
    Activity,
    Item,
    Character,
    TradeStatus,
)


@pytest.fixture(scope="function")
def game() -> Game:
    return Game("sqlite:///:memory:")


def test_create_character(game: Game) -> None:
    character = game.create_character("Foo")
    assert character.name == "Foo"
    assert len(character.skills) == len(Skill)


def test_get_character_by_name(game: Game) -> None:
    character = game.create_character("Foo")
    assert character == game.get_character_by_name("Foo")

    with pytest.raises(NoResultFound):
        game.get_character_by_name("Bar")


def test_start_activity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    assert character_activity.started_at is not None
    assert character_activity.ended_at is None


def test_end_activity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    rewards = game.end_activity(character)

    assert isinstance(rewards, list)
    assert len(rewards) == 1
    assert isinstance(rewards[0], ActivityReward)
    assert character_activity.ended_at is not None

    # No active activity
    assert game.end_activity(character) == []


def test_only_one_activity(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    with pytest.raises(Exception):
        game.start_activity(character, Activity.MINE_TIN)


def test_switch_activity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    game.end_activity(character)
    character_activity_2 = game.start_activity(character, Activity.MINE_TIN)

    assert character_activity.ended_at is not None
    assert character_activity_2.started_at >= character_activity.ended_at
    assert character_activity_2.ended_at is None


def test_character_activity_skill_requirements(game: Game) -> None:
    character = game.create_character("Foo")

    # level 1 woodcutting is sufficient
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    # level 90 woodcutting required — should raise
    game.end_activity(character)
    with pytest.raises(Exception):
        game.start_activity(character, Activity.CHOP_REDWOOD)


def test_character_activity_skill_rewards(game: Game) -> None:
    character = game.create_character("Foo")
    woodcutting = next(s for s in character.skills if s.skill == Skill.WOODCUTTING)
    assert woodcutting.experience == 0

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    # 10s / 0.5s action_time = 20 actions, 25 xp each
    assert woodcutting.experience == 500

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=15)
    game.end_activity(character)

    # 30 more actions × 25 xp → 1250 total
    assert woodcutting.experience == 1250


def test_character_activity_item_rewards(game: Game) -> None:
    character = game.create_character("Foo")

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    logs = next(i for i in character.items if i.item == Item.REGULAR_LOG)
    assert logs.quantity == 20

    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=15)
    game.end_activity(character)

    assert logs.quantity == 50


def test_character_skill_non_negative_xp(game: Game) -> None:
    character = game.create_character("Foo")
    woodcutting = next(s for s in character.skills if s.skill == Skill.WOODCUTTING)
    assert woodcutting.experience == 0

    woodcutting.experience = -50
    with pytest.raises(IntegrityError):
        game._session.commit()


def test_character_item_non_negative_quantity(game: Game) -> None:
    character = game.create_character("Foo")
    character_activity = game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    logs = next(i for i in character.items if i.item == Item.REGULAR_LOG)
    assert logs.quantity == 20

    logs.quantity = -50
    with pytest.raises(IntegrityError):
        game._session.commit()


def test_activity_materials_consumed(game: Game) -> None:
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.COPPER_ORE, quantity=1))
    character.items.append(CharacterItem(character_id=character.id, item=Item.TIN_ORE, quantity=1))

    character_activity = game.start_activity(character, Activity.SMITH_BRONZE_BAR)
    character_activity.started_at = now_utc() - datetime.timedelta(seconds=Activity.SMITH_BRONZE_BAR.action_time * 2)

    rewards = game.end_activity(character)

    assert len(rewards) == 1
    reward = rewards[0]
    assert reward.actions_completed == 1
    assert reward.items_gained == {Item.BRONZE_BAR: 1}
    assert reward.materials_consumed == {Item.COPPER_ORE: 1, Item.TIN_ORE: 1}

    bronze_bars = next(i for i in character.items if i.item == Item.BRONZE_BAR)
    copper_ore = next(i for i in character.items if i.item == Item.COPPER_ORE)
    tin_ore = next(i for i in character.items if i.item == Item.TIN_ORE)

    assert bronze_bars.quantity == 1
    assert copper_ore.quantity == 0
    assert tin_ore.quantity == 0


def test_action_limit_caps_actions(game: Game) -> None:
    character = game.create_character("Foo")
    ca = game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=5)
    ca.started_at = now_utc() - datetime.timedelta(seconds=100)  # way more time than needed
    rewards = game.end_activity(character)
    assert rewards[0].actions_completed == 5


def test_action_limit_material_auto_cap(game: Game) -> None:
    """Material limit caps actions even when action_limit is higher."""
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.COPPER_ORE, quantity=2))
    character.items.append(CharacterItem(character_id=character.id, item=Item.TIN_ORE, quantity=2))
    ca = game.start_activity(character, Activity.SMITH_BRONZE_BAR, action_limit=100)
    ca.started_at = now_utc() - datetime.timedelta(seconds=Activity.SMITH_BRONZE_BAR.action_time * 50)
    rewards = game.end_activity(character)
    assert rewards[0].actions_completed == 2  # limited by materials


def test_queue_activity_starts_immediately_if_idle(game: Game) -> None:
    character = game.create_character("Foo")
    game.queue_activity(character, Activity.CHOP_REGULAR_TREE)
    assert character.current_activity is not None
    assert character.current_activity.activity == Activity.CHOP_REGULAR_TREE


def test_queue_activity_adds_to_queue_if_busy(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    queue = game.get_activity_queue(character)
    assert len(queue) == 1
    assert queue[0].activity == Activity.MINE_TIN


def test_queue_limit_enforced(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)
    game.queue_activity(character, Activity.FISH_SHRIMP)
    with pytest.raises(Exception):
        game.queue_activity(character, Activity.MINE_TIN)


def test_end_activity_chains_queue(game: Game) -> None:
    """When activity A hits its action limit before now, queued activity B starts retroactively."""
    character = game.create_character("Foo")
    # MINE_TIN: 3 actions at 1s each → done after 3s from start
    ca = game.start_activity(character, Activity.MINE_TIN, action_limit=3)
    ca.started_at = now_utc() - datetime.timedelta(seconds=10)
    # Queue MINE_COPPER: 2 actions at 1s each
    game.queue_activity(character, Activity.MINE_COPPER, action_limit=2)

    rewards = game.end_activity(character)
    assert len(rewards) == 2
    assert rewards[0].actions_completed == 3  # MINE_TIN completed its limit
    assert rewards[1].actions_completed == 2  # MINE_IRON completed its limit


def test_end_activity_retroactive_multiple(game: Game) -> None:
    """Three activities chain: A (limit 2) → B (limit 3) → C (no limit, still running)."""
    character = game.create_character("Foo")
    # CHOP_REGULAR_TREE: 0.5s per action, limit 2 → done after 1s
    ca = game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=2)
    ca.started_at = now_utc() - datetime.timedelta(seconds=20)

    game.queue_activity(character, Activity.MINE_TIN, action_limit=3)  # 1s each → done after 3s from chain
    game.queue_activity(character, Activity.MINE_COPPER)  # no limit → still running

    rewards = game.end_activity(character)
    # CHOP completes at +1s, MINE_TIN at +4s, MINE_COPPER runs from +4s to now (~16s)
    assert len(rewards) == 3
    assert rewards[0].actions_completed == 2  # CHOP_REGULAR_TREE
    assert rewards[1].actions_completed == 3  # MINE_TIN
    assert rewards[2].actions_completed >= 1  # MINE_COPPER (16s / 1s = 16 actions)


def test_get_activity_queue_returns_pending_only(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=1)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)

    queue = game.get_activity_queue(character)
    assert len(queue) == 2

    # Simulate enough time for CHOP to complete and chain into MINE_TIN
    ca = character.current_activity
    assert ca is not None
    ca.started_at = now_utc() - datetime.timedelta(seconds=10)
    game.end_activity(character)

    # MINE_TIN was dequeued (started_at set); MINE_COPPER may still be pending or also dequeued
    queue_after = game.get_activity_queue(character)
    assert len(queue_after) <= 1


def test_remove_from_queue(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)

    queue = game.get_activity_queue(character)
    assert len(queue) == 2

    # Remove the first item
    game.remove_from_queue(character, queue[0])

    queue_after = game.get_activity_queue(character)
    assert len(queue_after) == 1
    assert queue_after[0].activity == Activity.MINE_COPPER


def test_end_unlimited_activity_starts_next_queued(game: Game) -> None:
    """Ending an unlimited activity should automatically start the next queued activity."""
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)

    game.end_activity(character)

    assert character.current_activity is not None
    assert character.current_activity.activity == Activity.MINE_TIN
    assert len(game.get_activity_queue(character)) == 1
    assert game.get_activity_queue(character)[0].activity == Activity.MINE_COPPER


def test_queue_activity_checks_skill_requirements(game: Game) -> None:
    """Queuing an activity the character doesn't meet requirements for should raise."""
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE)

    with pytest.raises(Exception, match="level"):
        game.queue_activity(character, Activity.CHOP_REDWOOD)


def test_remove_from_queue_middle(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, Activity.CHOP_REGULAR_TREE, action_limit=10)
    game.queue_activity(character, Activity.MINE_TIN)
    game.queue_activity(character, Activity.MINE_COPPER)
    game.queue_activity(character, Activity.FISH_SHRIMP)

    queue = game.get_activity_queue(character)
    assert len(queue) == 3

    # Remove the middle item
    game.remove_from_queue(character, queue[1])

    queue_after = game.get_activity_queue(character)
    assert len(queue_after) == 2
    assert queue_after[0].activity == Activity.MINE_TIN
    assert queue_after[1].activity == Activity.FISH_SHRIMP


def test_cooking_materials_consumed(game: Game) -> None:
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.RAW_SHRIMP, quantity=1))

    ca = game.start_activity(character, Activity.COOK_SHRIMP)
    ca.started_at = now_utc() - datetime.timedelta(seconds=Activity.COOK_SHRIMP.action_time * 2)

    rewards = game.end_activity(character)

    assert len(rewards) == 1
    reward = rewards[0]
    assert reward.actions_completed == 1
    assert reward.items_gained == {Item.COOKED_SHRIMP: 1}
    assert reward.materials_consumed == {Item.RAW_SHRIMP: 1}

    cooking = next(s for s in character.skills if s.skill == Skill.COOKING)
    assert cooking.experience == 30

    raw_shrimp = next(i for i in character.items if i.item == Item.RAW_SHRIMP)
    cooked_shrimp = next(i for i in character.items if i.item == Item.COOKED_SHRIMP)
    assert raw_shrimp.quantity == 0
    assert cooked_shrimp.quantity == 1


def test_fletching_arrow_shaft_output(game: Game) -> None:
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.REGULAR_LOG, quantity=1))

    ca = game.start_activity(character, Activity.FLETCH_ARROW_SHAFT)
    ca.started_at = now_utc() - datetime.timedelta(seconds=Activity.FLETCH_ARROW_SHAFT.action_time * 2)

    rewards = game.end_activity(character)

    assert len(rewards) == 1
    reward = rewards[0]
    assert reward.actions_completed == 1
    assert reward.items_gained == {Item.ARROW_SHAFT: 15}
    assert reward.materials_consumed == {Item.REGULAR_LOG: 1}

    fletching = next(s for s in character.skills if s.skill == Skill.FLETCHING)
    assert fletching.experience == 5

    log = next(i for i in character.items if i.item == Item.REGULAR_LOG)
    shaft = next(i for i in character.items if i.item == Item.ARROW_SHAFT)
    assert log.quantity == 0
    assert shaft.quantity == 15


def test_fletching_arrow_assembly(game: Game) -> None:
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.ARROW_SHAFT, quantity=1))
    character.items.append(CharacterItem(character_id=character.id, item=Item.FEATHER, quantity=1))
    character.items.append(CharacterItem(character_id=character.id, item=Item.BRONZE_ARROWHEAD, quantity=1))

    ca = game.start_activity(character, Activity.FLETCH_BRONZE_ARROW)
    ca.started_at = now_utc() - datetime.timedelta(seconds=Activity.FLETCH_BRONZE_ARROW.action_time * 2)

    rewards = game.end_activity(character)

    assert len(rewards) == 1
    reward = rewards[0]
    assert reward.actions_completed == 1
    assert reward.items_gained == {Item.BRONZE_ARROW: 1}
    assert reward.materials_consumed == {Item.ARROW_SHAFT: 1, Item.FEATHER: 1, Item.BRONZE_ARROWHEAD: 1}

    fletching = next(s for s in character.skills if s.skill == Skill.FLETCHING)
    assert fletching.experience == 1

    shaft = next(i for i in character.items if i.item == Item.ARROW_SHAFT)
    feather = next(i for i in character.items if i.item == Item.FEATHER)
    arrowhead = next(i for i in character.items if i.item == Item.BRONZE_ARROWHEAD)
    arrow = next(i for i in character.items if i.item == Item.BRONZE_ARROW)
    assert shaft.quantity == 0
    assert feather.quantity == 0
    assert arrowhead.quantity == 0
    assert arrow.quantity == 1


def test_smithing_arrowheads(game: Game) -> None:
    character = game.create_character("Foo")
    character.items.append(CharacterItem(character_id=character.id, item=Item.BRONZE_BAR, quantity=1))
    smithing = next(s for s in character.skills if s.skill == Skill.SMITHING)
    smithing.experience = 500  # enough for level 5

    ca = game.start_activity(character, Activity.SMITH_BRONZE_ARROWHEADS)
    ca.started_at = now_utc() - datetime.timedelta(seconds=Activity.SMITH_BRONZE_ARROWHEADS.action_time * 2)

    rewards = game.end_activity(character)

    assert len(rewards) == 1
    reward = rewards[0]
    assert reward.actions_completed == 1
    assert reward.items_gained == {Item.BRONZE_ARROWHEAD: 15}
    assert reward.materials_consumed == {Item.BRONZE_BAR: 1}

    smithing = next(s for s in character.skills if s.skill == Skill.SMITHING)
    assert smithing.experience == 512  # 500 pre-existing + 12 from activity

    bar = next(i for i in character.items if i.item == Item.BRONZE_BAR)
    arrowhead = next(i for i in character.items if i.item == Item.BRONZE_ARROWHEAD)
    assert bar.quantity == 0
    assert arrowhead.quantity == 15


# --- Trading tests ---


@pytest.fixture
def two_chars(game: Game) -> tuple[Character, Character]:
    alice = game.create_character("Alice")
    bob = game.create_character("Bob")
    game._get_character_item(alice, Item.REGULAR_LOG).quantity = 50
    game._get_character_item(bob, Item.BRONZE_BAR).quantity = 10
    game._session.commit()
    return alice, bob


def test_create_trade_basic(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 5})

    assert trade.status == TradeStatus.PENDING
    assert trade.current_turn_id == bob.id
    assert len(game.get_pending_trades(bob)) == 1
    assert len(game.get_outgoing_trades(alice)) == 1
    assert len(game.get_pending_trades(alice)) == 0
    assert len(game.get_outgoing_trades(bob)) == 0


def test_full_lifecycle(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 5})

    # Bob counters: fewer bars
    game.counter_trade(trade, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 3})
    assert trade.current_turn_id == alice.id
    assert len(trade.offers) == 2

    # Alice accepts
    game.accept_trade(trade, alice)
    assert trade.status == TradeStatus.ACCEPTED

    alice_logs = game._get_character_item(alice, Item.REGULAR_LOG).quantity
    alice_bars = game._get_character_item(alice, Item.BRONZE_BAR).quantity
    bob_logs = game._get_character_item(bob, Item.REGULAR_LOG).quantity
    bob_bars = game._get_character_item(bob, Item.BRONZE_BAR).quantity

    assert alice_logs == 40  # 50 - 10
    assert alice_bars == 3  # 0 + 3
    assert bob_logs == 10  # 0 + 10
    assert bob_bars == 7  # 10 - 3


def test_reject_by_counterparty(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 10}, {})

    game.reject_trade(trade, bob)
    assert trade.status == TradeStatus.REJECTED

    # Inventories unchanged
    assert game._get_character_item(alice, Item.REGULAR_LOG).quantity == 50
    assert game._get_character_item(bob, Item.BRONZE_BAR).quantity == 10


def test_reject_by_initiator(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 5}, {})
    # It's Bob's turn but Alice can still reject
    game.reject_trade(trade, alice)
    assert trade.status == TradeStatus.REJECTED


def test_accept_validates_insufficient_items(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 5})

    # Drain Alice's logs after the trade was created
    game._get_character_item(alice, Item.REGULAR_LOG).quantity = 0
    game._session.commit()

    with pytest.raises(ValueError, match="lacks"):
        game.accept_trade(trade, bob)


def test_wrong_turn_cannot_accept(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 5}, {})
    # It's Bob's turn; Alice cannot accept
    with pytest.raises(ValueError, match="not your turn"):
        game.accept_trade(trade, alice)


def test_wrong_turn_cannot_counter(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 5}, {})
    # It's Bob's turn; Alice cannot counter
    with pytest.raises(ValueError, match="not your turn"):
        game.counter_trade(trade, alice, {Item.REGULAR_LOG: 3}, {})


def test_both_sides_empty_raises(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    with pytest.raises(ValueError, match="At least one side"):
        game.create_trade(alice, bob, {}, {})


def test_self_trade_raises(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, _ = two_chars
    with pytest.raises(ValueError, match="yourself"):
        game.create_trade(alice, alice, {Item.REGULAR_LOG: 1}, {})


def test_get_trade_unknown(game: Game) -> None:
    assert game.get_trade(uuid.uuid4()) is None


def test_current_offer_after_counter(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 5})
    game.counter_trade(trade, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 3})

    offer = game.get_current_offer(trade)
    assert offer.proposed_by_id == bob.id
    bar_items = [oi for oi in offer.items if oi.item == Item.BRONZE_BAR]
    assert bar_items[0].quantity == 3


def test_non_party_cannot_reject(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    charlie = game.create_character("Charlie")
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 5}, {})
    with pytest.raises(ValueError, match="not a party"):
        game.reject_trade(trade, charlie)


def test_quantity_zero_raises(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    with pytest.raises(ValueError, match="greater than 0"):
        game.create_trade(alice, bob, {Item.REGULAR_LOG: 0}, {Item.BRONZE_BAR: 1})


def test_giveaway_allowed(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    # Alice gives logs for free (counterparty side is empty)
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 5}, {})
    assert trade.status == TradeStatus.PENDING


def test_offer_exceeds_inventory_raises(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    # Alice only has 50 logs; offering 51 should fail
    with pytest.raises(ValueError, match="Insufficient"):
        game.create_trade(alice, bob, {Item.REGULAR_LOG: 51}, {})


def test_counter_exceeds_inventory_raises(game: Game, two_chars: tuple[Character, Character]) -> None:
    alice, bob = two_chars
    trade = game.create_trade(alice, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 5})
    # Bob only has 10 bars; offering 11 should fail
    with pytest.raises(ValueError, match="Insufficient"):
        game.counter_trade(trade, bob, {Item.REGULAR_LOG: 10}, {Item.BRONZE_BAR: 11})
