"""Tests for stamina cue system wiring and reset behavior."""

import asyncio
import contextlib
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from mudlib.player import Player
from mudlib.zone import Zone


@pytest.fixture
def test_zone():
    """Override conftest test_zone with smaller zone for stamina tests."""
    return Zone(name="test", width=10, height=10)


@pytest.fixture
def player(mock_writer, test_zone):
    """Override conftest player with custom position and stats for stamina tests."""
    p = Player(
        name="Goku",
        x=5,
        y=5,
        pl=100.0,
        stamina=100.0,
        max_stamina=100.0,
        writer=mock_writer,
    )
    p.move_to(test_zone, x=5, y=5)
    return p


@pytest.fixture
def defender(test_zone):
    """Defender fixture with custom position for stamina tests."""
    w = MagicMock()
    w.write = MagicMock()
    w.drain = AsyncMock()
    p = Player(
        name="Vegeta",
        x=5,
        y=5,
        pl=100.0,
        stamina=100.0,
        max_stamina=100.0,
        writer=w,
    )
    p.move_to(test_zone, x=5, y=5)
    return p


@pytest.mark.asyncio
async def test_stamina_cue_after_combat_damage(player, defender):
    """check_stamina_cues is called after damage in combat resolution."""
    import time

    from mudlib.combat.encounter import CombatState
    from mudlib.combat.engine import active_encounters, start_encounter
    from mudlib.combat.moves import CombatMove

    # Clear encounters
    active_encounters.clear()

    # Create a move that deals damage
    move = CombatMove(
        name="weak punch",
        command="weak",
        move_type="attack",
        damage_pct=0.5,
        stamina_cost=5.0,
        timing_window_ms=100,
    )

    # Start encounter
    encounter = start_encounter(player, defender)
    encounter.attack(move)

    # Fast-forward to RESOLVE state
    encounter.state = CombatState.RESOLVE
    encounter.move_started_at = time.monotonic()

    # Patch check_stamina_cues to verify it's called
    with patch("mudlib.combat.engine.check_stamina_cues") as mock_check:
        from mudlib.combat.engine import process_combat

        await process_combat()

        # Should be called for both entities after damage
        assert mock_check.called
        calls = [call[0][0] for call in mock_check.call_args_list]
        assert player in calls
        assert defender in calls


@pytest.mark.asyncio
async def test_stamina_cue_after_power_up_deduction(player):
    """check_stamina_cues is called during power-up stamina deduction."""
    from mudlib.commands.power import power_up_loop

    player.stamina = 100.0
    player.max_stamina = 100.0
    player.pl = 10.0
    player.max_pl = 50.0

    with patch("mudlib.commands.power.check_stamina_cues") as mock_check:
        # Run power-up for a short time
        task = asyncio.create_task(power_up_loop(player, target_pl=20.0))
        await asyncio.sleep(0.15)  # Let a few ticks happen
        task.cancel()
        with contextlib.suppress(asyncio.CancelledError):
            await task

        # check_stamina_cues should have been called at least once
        assert mock_check.called
        # Verify player was the argument
        calls = [call[0][0] for call in mock_check.call_args_list]
        assert player in calls


@pytest.mark.asyncio
async def test_stamina_cue_after_attack_cost(player, defender):
    """check_stamina_cues is called after attack stamina cost deduction."""
    from mudlib.combat.commands import do_attack
    from mudlib.combat.engine import active_encounters
    from mudlib.combat.moves import CombatMove
    from mudlib.player import players

    # Clear encounters
    active_encounters.clear()

    # Register defender in global players dict so do_attack can find it
    players["Vegeta"] = defender

    try:
        # Create a move with high stamina cost
        move = CombatMove(
            name="power punch",
            command="power",
            move_type="attack",
            damage_pct=0.3,
            stamina_cost=60.0,
            timing_window_ms=1000,
        )

        player.stamina = 100.0
        player.max_stamina = 100.0

        with patch("mudlib.combat.commands.check_stamina_cues") as mock_check:
            await do_attack(player, "Vegeta", move)

            # check_stamina_cues should have been called
            assert mock_check.called
            # Verify player was the argument
            calls = [call[0][0] for call in mock_check.call_args_list]
            assert player in calls
    finally:
        players.clear()


@pytest.mark.asyncio
async def test_stamina_cue_after_defense_cost(player):
    """check_stamina_cues is called after defense stamina cost deduction."""
    from mudlib.combat.commands import do_defend
    from mudlib.combat.moves import CombatMove

    # Create a defense move with stamina cost
    move = CombatMove(
        name="duck",
        command="duck",
        move_type="defense",
        stamina_cost=30.0,
        timing_window_ms=100,
    )

    player.stamina = 100.0
    player.max_stamina = 100.0

    with patch("mudlib.combat.commands.check_stamina_cues") as mock_check:
        # Use a short timing window to avoid blocking too long
        move.timing_window_ms = 10
        await do_defend(player, "", move)

        # check_stamina_cues should have been called
        assert mock_check.called
        # Verify player was the argument
        calls = [call[0][0] for call in mock_check.call_args_list]
        assert player in calls


@pytest.mark.asyncio
async def test_stamina_cue_reset_on_resting_recovery(player):
    """_last_stamina_cue resets to 1.0 when stamina fully recovers via resting."""
    from mudlib.combat.stamina import check_stamina_cues
    from mudlib.player import players
    from mudlib.resting import process_resting

    # Register player in global dict so process_resting can find them
    players[player.name] = player

    try:
        # Drop stamina to trigger a cue
        player.stamina = 20.0
        player.max_stamina = 100.0
        await check_stamina_cues(player)
        assert player._last_stamina_cue == 0.25  # Below 25% threshold

        # Start resting and set stamina very close to max
        player.resting = True
        player.stamina = 99.85  # Will be clamped to 100.0 after one tick

        # Process one tick to reach max
        await process_resting()

        # Stamina should be at max and _last_stamina_cue should reset
        assert player.stamina == 100.0
        assert player._last_stamina_cue == 1.0
    finally:
        players.clear()


@pytest.mark.asyncio
async def test_stamina_cue_reset_on_unconscious_recovery(player):
    """_last_stamina_cue resets to 1.0 when stamina recovers from unconsciousness."""
    from mudlib.combat.stamina import check_stamina_cues
    from mudlib.player import players
    from mudlib.unconscious import process_unconscious

    # Register player in global dict so process_unconscious can find them
    players[player.name] = player

    try:
        # Drop stamina to trigger a cue
        player.stamina = 5.0
        player.max_stamina = 100.0
        await check_stamina_cues(player)
        assert player._last_stamina_cue == 0.10  # Below 10% threshold

        # Drop to unconscious
        player.stamina = 0.0
        player.pl = 0.0

        # Verify player is unconscious
        assert player.posture == "unconscious"

        # Process recovery tick to regain consciousness
        await process_unconscious()

        # Should have regained consciousness and reset cue
        assert player.stamina > 0
        assert player.pl > 0
        assert player._last_stamina_cue == 1.0
    finally:
        players.clear()


@pytest.mark.asyncio
async def test_cue_fires_again_after_reset(player):
    """After reset, cues fire again on next descent."""
    from mudlib.combat.stamina import check_stamina_cues

    player.max_stamina = 100.0

    # First descent: trigger 75% threshold
    player.stamina = 70.0
    player.writer.write.reset_mock()
    await check_stamina_cues(player)
    first_call_count = player.writer.write.call_count
    assert first_call_count > 0
    assert player._last_stamina_cue == 0.75

    # Recover to max and reset
    player.stamina = 100.0
    player._last_stamina_cue = 1.0

    # Second descent: should fire again at 75%
    player.stamina = 70.0
    player.writer.write.reset_mock()
    await check_stamina_cues(player)
    second_call_count = player.writer.write.call_count
    assert second_call_count > 0  # Should fire again
