import datetime
import random
from unittest.mock import patch

import pytest

from idlemaxx.game import (
    AUTO_EAT_THRESHOLD,
    DEATH_RECOVERY_HOURS,
    CombatResult,
    Game,
    KillRecord,
)
from idlemaxx.models import (
    Character,
    CharacterItem,
    CombatArea,
    EquipmentSlot,
    Item,
    Monster,
    Skill,
    now_utc,
    xp_for_level,
)


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


def _give_item(game: Game, character: Character, item: Item, quantity: int) -> None:
    inv = game._get_character_item(character, item)
    inv.quantity += quantity
    game._session.commit()


def _set_skill(game: Game, character: Character, skill: Skill, level: int) -> None:
    s = game._get_character_skill(character, skill)
    s.experience = xp_for_level(level)
    game._session.commit()


def _strong_character(game: Game) -> Character:
    """Level 50 in all combat skills — easily handles goblin cave."""
    character = game.create_character("Fighter")
    for skill in (Skill.ATTACK, Skill.STRENGTH, Skill.DEFENSE, Skill.HITPOINTS):
        _set_skill(game, character, skill, 50)
    return character


# ---------------------------------------------------------------------------
# Equipment
# ---------------------------------------------------------------------------


def test_equip_item(game: Game) -> None:
    character = game.create_character("Foo")
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)

    eq = game.equip_item(character, Item.BRONZE_SCIMITAR)

    assert eq.slot == EquipmentSlot.WEAPON
    assert eq.item == Item.BRONZE_SCIMITAR
    inv = game._get_character_item(character, Item.BRONZE_SCIMITAR)
    assert inv.quantity == 0  # moved from inventory


def test_equip_armor(game: Game) -> None:
    character = game.create_character("Foo")
    _give_item(game, character, Item.BRONZE_HELMET, 1)

    eq = game.equip_item(character, Item.BRONZE_HELMET)

    assert eq.slot == EquipmentSlot.HELMET
    inv = game._get_character_item(character, Item.BRONZE_HELMET)
    assert inv.quantity == 0


def test_equip_food(game: Game) -> None:
    character = game.create_character("Foo")
    _give_item(game, character, Item.COOKED_LOBSTER, 10)

    eq = game.equip_food(character, Item.COOKED_LOBSTER, 10)

    assert eq.slot == EquipmentSlot.FOOD
    assert eq.item == Item.COOKED_LOBSTER
    assert eq.quantity == 10
    inv = game._get_character_item(character, Item.COOKED_LOBSTER)
    assert inv.quantity == 0


def test_unequip_slot(game: Game) -> None:
    character = game.create_character("Foo")
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    game.unequip_slot(character, EquipmentSlot.WEAPON)

    equipment = game.get_equipment(character)
    assert EquipmentSlot.WEAPON not in equipment
    inv = game._get_character_item(character, Item.BRONZE_SCIMITAR)
    assert inv.quantity == 1  # returned to inventory


def test_unequip_food_returns_quantity(game: Game) -> None:
    character = game.create_character("Foo")
    _give_item(game, character, Item.COOKED_SHARK, 5)
    game.equip_food(character, Item.COOKED_SHARK, 5)

    game.unequip_slot(character, EquipmentSlot.FOOD)

    inv = game._get_character_item(character, Item.COOKED_SHARK)
    assert inv.quantity == 5


def test_equip_item_requires_attack_level(game: Game) -> None:
    character = game.create_character("Foo")  # attack level 1
    _give_item(game, character, Item.RUNITE_SCIMITAR, 1)  # requires level 50

    with pytest.raises(Exception, match="Attack level 50"):
        game.equip_item(character, Item.RUNITE_SCIMITAR)


def test_equip_armor_requires_defense_level(game: Game) -> None:
    character = game.create_character("Foo")  # defense level 1
    _give_item(game, character, Item.RUNITE_HELMET, 1)  # requires level 50

    with pytest.raises(Exception, match="Defense level 50"):
        game.equip_item(character, Item.RUNITE_HELMET)


def test_equip_item_not_in_inventory_raises(game: Game) -> None:
    character = game.create_character("Foo")
    with pytest.raises(Exception, match="don't have"):
        game.equip_item(character, Item.BRONZE_SCIMITAR)


def test_equip_item_replaces_existing(game: Game) -> None:
    character = game.create_character("Foo")
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    _give_item(game, character, Item.IRON_SCIMITAR, 1)
    _set_skill(game, character, Skill.ATTACK, 10)

    game.equip_item(character, Item.BRONZE_SCIMITAR)
    game.equip_item(character, Item.IRON_SCIMITAR)

    equipment = game.get_equipment(character)
    assert equipment[EquipmentSlot.WEAPON].item == Item.IRON_SCIMITAR
    # Bronze scimitar was returned to inventory
    inv = game._get_character_item(character, Item.BRONZE_SCIMITAR)
    assert inv.quantity == 1


# ---------------------------------------------------------------------------
# Combat lifecycle
# ---------------------------------------------------------------------------


def test_enter_combat_creates_session(game: Game) -> None:
    character = game.create_character("Foo")
    session = game.enter_combat(character, CombatArea.GOBLIN_CAVE)

    assert session.area == CombatArea.GOBLIN_CAVE
    assert session.started_at is not None
    assert session.ended_at is None
    assert game.get_active_combat(character) is session


def test_enter_combat_raises_if_already_in_combat(game: Game) -> None:
    character = game.create_character("Foo")
    game.enter_combat(character, CombatArea.GOBLIN_CAVE)

    with pytest.raises(Exception, match="already in combat"):
        game.enter_combat(character, CombatArea.GOBLIN_CAVE)


def test_enter_combat_raises_if_doing_activity(game: Game) -> None:
    character = game.create_character("Foo")
    game.start_activity(character, game.activities_for_category(game.activity_categories()[0])[0])

    with pytest.raises(Exception, match="busy with an activity"):
        game.enter_combat(character, CombatArea.GOBLIN_CAVE)


def test_end_combat_no_active_session_returns_none(game: Game) -> None:
    character = game.create_character("Foo")
    assert game.end_combat(character) is None


# ---------------------------------------------------------------------------
# Combat simulation — player survives
# ---------------------------------------------------------------------------


def test_end_combat_player_survives(game: Game) -> None:
    character = _strong_character(game)
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.GOBLIN_CAVE)
    session.started_at = now_utc() - datetime.timedelta(minutes=5)

    result = game.end_combat(character)

    assert result is not None
    assert result.survived is True
    assert result.died_at is None
    assert len(result.kills) > 0
    assert game.get_active_combat(character) is None


def test_end_combat_xp_gained(game: Game) -> None:
    character = _strong_character(game)
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.GOBLIN_CAVE)
    session.started_at = now_utc() - datetime.timedelta(minutes=5)

    result = game.end_combat(character)

    assert result is not None
    assert result.xp_gained.get(Skill.ATTACK, 0) > 0
    assert result.xp_gained.get(Skill.STRENGTH, 0) > 0
    assert result.xp_gained.get(Skill.HITPOINTS, 0) > 0

    # XP applied to character
    attack_skill = game._get_character_skill(character, Skill.ATTACK)
    assert attack_skill.experience > xp_for_level(50)


def test_end_combat_loot_added_to_inventory(game: Game) -> None:
    """Bones always drop — character should have bones after killing goblins."""
    character = _strong_character(game)
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.GOBLIN_CAVE)
    session.started_at = now_utc() - datetime.timedelta(minutes=5)

    game.end_combat(character)

    bones = game._get_character_item(character, Item.BONES)
    assert bones.quantity > 0


# ---------------------------------------------------------------------------
# Combat simulation — player dies
# ---------------------------------------------------------------------------


def test_end_combat_player_dies(game: Game) -> None:
    """Level 1 character unarmed vs barbarians — should die quickly."""
    character = game.create_character("Weakling")

    session = game.enter_combat(character, CombatArea.BARBARIAN_CAMP)
    session.started_at = now_utc() - datetime.timedelta(hours=1)

    result = game.end_combat(character)

    assert result is not None
    assert result.survived is False
    assert result.died_at is not None
    assert result.recovery_deadline is not None
    # died_at is between session start and end
    assert session.started_at <= result.died_at <= session.ended_at  # type: ignore[operator]
    # recovery deadline is 24h after death
    expected_deadline = result.died_at + datetime.timedelta(hours=DEATH_RECOVERY_HOURS)
    assert abs((result.recovery_deadline - expected_deadline).total_seconds()) < 1


def test_player_death_recorded_on_session(game: Game) -> None:
    character = game.create_character("Weakling")
    session = game.enter_combat(character, CombatArea.BARBARIAN_CAMP)
    session.started_at = now_utc() - datetime.timedelta(hours=1)

    game.end_combat(character)

    assert session.died_at is not None
    assert session.items_recovered is False


# ---------------------------------------------------------------------------
# Auto-eat
# ---------------------------------------------------------------------------


def test_auto_eat_food_consumed(game: Game) -> None:
    """Character with food should consume some of it during a long fight."""
    character = game.create_character("Eater")
    # Modest stats so monster can damage them
    _set_skill(game, character, Skill.ATTACK, 5)
    _set_skill(game, character, Skill.STRENGTH, 5)
    _set_skill(game, character, Skill.HITPOINTS, 5)

    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)
    _give_item(game, character, Item.COOKED_SHARK, 20)
    game.equip_food(character, Item.COOKED_SHARK, 20)

    session = game.enter_combat(character, CombatArea.GOBLIN_CAVE)
    session.started_at = now_utc() - datetime.timedelta(minutes=10)

    result = game.end_combat(character)

    # Some food should have been consumed (food slot quantity reduced)
    food_eq = game.get_equipment(character).get(EquipmentSlot.FOOD)
    assert food_eq is not None
    assert food_eq.quantity < 20


def test_auto_eat_extends_survival(game: Game) -> None:
    """Character with food should survive longer than without, or at least not die sooner."""
    # Without food
    char_no_food = game.create_character("NoFood")
    _set_skill(game, char_no_food, Skill.ATTACK, 5)
    _set_skill(game, char_no_food, Skill.STRENGTH, 5)
    _set_skill(game, char_no_food, Skill.HITPOINTS, 5)
    _give_item(game, char_no_food, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(char_no_food, Item.BRONZE_SCIMITAR)

    _RealRandom = random.Random
    _fixed_instance = _RealRandom("fixed-seed")

    def _fixed_rng(_seed: object) -> random.Random:
        _fixed_instance.seed("fixed-seed")
        return _fixed_instance

    session_no_food = game.enter_combat(char_no_food, CombatArea.BARBARIAN_CAMP)
    session_no_food.started_at = now_utc() - datetime.timedelta(hours=2)
    with patch("idlemaxx.game.random.Random", side_effect=_fixed_rng):
        result_no_food = game.end_combat(char_no_food)

    # With food — same fixed RNG seed so food is the only variable
    char_food = game.create_character("WithFood")
    _set_skill(game, char_food, Skill.ATTACK, 5)
    _set_skill(game, char_food, Skill.STRENGTH, 5)
    _set_skill(game, char_food, Skill.HITPOINTS, 5)
    _give_item(game, char_food, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(char_food, Item.BRONZE_SCIMITAR)
    _give_item(game, char_food, Item.COOKED_SHARK, 50)
    game.equip_food(char_food, Item.COOKED_SHARK, 50)

    session_food = game.enter_combat(char_food, CombatArea.BARBARIAN_CAMP)
    session_food.started_at = now_utc() - datetime.timedelta(hours=2)
    with patch("idlemaxx.game.random.Random", side_effect=_fixed_rng):
        result_food = game.end_combat(char_food)

    # Character without food must die, character with food either survives or dies later
    assert result_no_food is not None
    assert result_no_food.survived is False

    if result_food is not None and not result_food.survived:
        # If both die, food character should die no sooner
        assert result_food.died_at >= result_no_food.died_at  # type: ignore[operator]


# ---------------------------------------------------------------------------
# Reproducibility
# ---------------------------------------------------------------------------


def test_combat_simulation_reproducible(game: Game) -> None:
    """Calling _simulate_combat twice on the same session gives the same result."""
    character = _strong_character(game)
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.GOBLIN_CAVE)
    ended = now_utc()
    session.ended_at = ended
    session.started_at = ended - datetime.timedelta(minutes=3)

    result1 = game._simulate_combat(session, character)
    result2 = game._simulate_combat(session, character)

    assert len(result1.kills) == len(result2.kills)
    assert result1.survived == result2.survived
    assert result1.xp_gained == result2.xp_gained
    for k1, k2 in zip(result1.kills, result2.kills):
        assert k1.monster == k2.monster
        assert k1.loot == k2.loot


# ---------------------------------------------------------------------------
# Death recovery
# ---------------------------------------------------------------------------


def test_death_recovery_within_deadline(game: Game) -> None:
    character = game.create_character("Weakling")
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.BARBARIAN_CAMP)
    session.started_at = now_utc() - datetime.timedelta(hours=1)
    result = game.end_combat(character)
    assert result is not None and not result.survived

    # Should be recoverable
    pending = game.get_pending_death_recovery(character)
    assert pending is not None

    recovered = game.recover_items(character, pending)

    # Scimitar should be recovered
    assert Item.BRONZE_SCIMITAR in recovered
    inv = game._get_character_item(character, Item.BRONZE_SCIMITAR)
    assert inv.quantity == 1
    assert pending.items_recovered is True

    # No longer pending
    assert game.get_pending_death_recovery(character) is None


def test_death_recovery_expired(game: Game) -> None:
    character = game.create_character("Weakling")
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.BARBARIAN_CAMP)
    session.started_at = now_utc() - datetime.timedelta(hours=1)
    result = game.end_combat(character)
    assert result is not None and not result.survived

    # Backdate death to >24h ago
    session.died_at = now_utc() - datetime.timedelta(hours=25)
    game._session.commit()

    # Should not be recoverable
    pending = game.get_pending_death_recovery(character)
    assert pending is None


def test_recovery_expired_clears_equipment_without_returning(game: Game) -> None:
    """Expired recovery clears equipment but does NOT return items to inventory."""
    character = game.create_character("Weakling")
    _give_item(game, character, Item.BRONZE_SCIMITAR, 1)
    game.equip_item(character, Item.BRONZE_SCIMITAR)

    session = game.enter_combat(character, CombatArea.BARBARIAN_CAMP)
    session.started_at = now_utc() - datetime.timedelta(hours=1)
    game.end_combat(character)

    # Manually expire the death
    session.died_at = now_utc() - datetime.timedelta(hours=25)
    game._session.commit()

    recovered = game.recover_items(character, session)

    assert recovered == {}  # nothing returned
    inv = game._get_character_item(character, Item.BRONZE_SCIMITAR)
    assert inv.quantity == 0  # items lost
    assert len(character.equipment) == 0  # equipment cleared
