"""Tests for combat engine and encounter management."""

import time
from unittest.mock import AsyncMock, MagicMock

import pytest

from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import (
    active_encounters,
    end_encounter,
    get_encounter,
    process_combat,
    start_encounter,
)
from mudlib.combat.moves import CombatMove
from mudlib.entity import Entity
from mudlib.player import Player


def _mock_writer():
    writer = MagicMock()
    writer.write = MagicMock()
    writer.drain = AsyncMock()
    return writer


@pytest.fixture(autouse=True)
def clear_encounters():
    """Clear encounters before and after each test."""
    active_encounters.clear()
    yield
    active_encounters.clear()


@pytest.fixture
def attacker():
    return Entity(name="Goku", x=0, y=0, pl=100.0, stamina=50.0)


@pytest.fixture
def defender():
    return Entity(name="Vegeta", x=0, y=0, pl=100.0, stamina=50.0)


@pytest.fixture
def punch():
    return CombatMove(
        name="punch right",
        move_type="attack",
        stamina_cost=5.0,
        timing_window_ms=800,
        damage_pct=0.15,
        countered_by=["dodge left"],
    )


def test_start_encounter_creates_encounter(attacker, defender):
    """Test starting an encounter creates and registers it."""
    encounter = start_encounter(attacker, defender)

    assert encounter is not None
    assert encounter.attacker is attacker
    assert encounter.defender is defender
    assert encounter in active_encounters


def test_get_encounter_finds_by_attacker(attacker, defender):
    """Test get_encounter finds encounter by attacker."""
    encounter = start_encounter(attacker, defender)
    found = get_encounter(attacker)

    assert found is encounter


def test_get_encounter_finds_by_defender(attacker, defender):
    """Test get_encounter finds encounter by defender."""
    encounter = start_encounter(attacker, defender)
    found = get_encounter(defender)

    assert found is encounter


def test_get_encounter_returns_none_if_not_in_combat(attacker):
    """Test get_encounter returns None for entity not in combat."""
    found = get_encounter(attacker)
    assert found is None


def test_end_encounter_removes_from_active_list(attacker, defender):
    """Test ending encounter removes it from active list."""
    encounter = start_encounter(attacker, defender)
    end_encounter(encounter)

    assert encounter not in active_encounters
    assert get_encounter(attacker) is None
    assert get_encounter(defender) is None


@pytest.mark.asyncio
async def test_process_combat_advances_encounters(attacker, defender, punch):
    """Test process_combat advances all active encounters."""
    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)

    # Process combat should advance state from TELEGRAPH to WINDOW
    time.sleep(0.31)
    await process_combat()

    assert encounter.state == CombatState.WINDOW


@pytest.mark.asyncio
async def test_process_combat_handles_multiple_encounters(punch):
    """Test process_combat handles multiple simultaneous encounters."""
    e1_attacker = Entity(name="A1", x=0, y=0)
    e1_defender = Entity(name="D1", x=0, y=0)
    e2_attacker = Entity(name="A2", x=10, y=10)
    e2_defender = Entity(name="D2", x=10, y=10)

    enc1 = start_encounter(e1_attacker, e1_defender)
    enc2 = start_encounter(e2_attacker, e2_defender)

    enc1.attack(punch)
    enc2.attack(punch)

    time.sleep(0.31)
    await process_combat()

    assert enc1.state == CombatState.WINDOW
    assert enc2.state == CombatState.WINDOW


@pytest.mark.asyncio
async def test_process_combat_auto_resolves_expired_windows(attacker, defender, punch):
    """Test process_combat auto-resolves when window expires."""
    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)
    initial_pl = defender.pl

    # Skip past telegraph and window
    time.sleep(0.31)  # Telegraph
    await process_combat()
    assert encounter.state == CombatState.WINDOW

    time.sleep(0.85)  # Window
    await process_combat()
    # Should auto-resolve and return to IDLE
    assert encounter.state == CombatState.IDLE
    # Damage should have been applied (no defense = 1.5x damage)
    assert defender.pl < initial_pl


def test_start_encounter_when_already_in_combat(attacker, defender):
    """Test starting encounter when attacker already in combat raises error."""
    start_encounter(attacker, defender)

    other = Entity(name="Other", x=5, y=5)
    with pytest.raises(ValueError, match="already in combat"):
        start_encounter(attacker, other)


def test_start_encounter_when_defender_in_combat(attacker, defender):
    """Test starting encounter when defender already in combat raises error."""
    start_encounter(attacker, defender)

    other = Entity(name="Other", x=5, y=5)
    with pytest.raises(ValueError, match="already in combat"):
        start_encounter(other, defender)


def test_active_encounters_list():
    """Test active_encounters is a list."""
    assert isinstance(active_encounters, list)


@pytest.mark.asyncio
async def test_process_combat_with_no_encounters():
    """Test process_combat handles empty encounter list."""
    await process_combat()  # Should not raise


@pytest.mark.asyncio
async def test_encounter_cleanup_after_resolution(attacker, defender, punch):
    """Test encounter can be ended after resolution."""
    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)

    # Advance to resolution
    time.sleep(0.31)
    await process_combat()
    time.sleep(0.85)
    await process_combat()

    # Resolve
    encounter.resolve()

    # Now end it
    end_encounter(encounter)
    assert get_encounter(attacker) is None


@pytest.mark.asyncio
async def test_process_combat_ends_encounter_on_knockout(punch):
    """Test process_combat ends encounter when defender is knocked out."""
    w = _mock_writer
    attacker = Player(name="Goku", x=0, y=0, pl=100.0, stamina=50.0, writer=w())
    defender = Player(name="Vegeta", x=0, y=0, pl=10.0, stamina=50.0, writer=w())

    # Push combat mode onto both stacks
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)

    # Advance to resolution
    time.sleep(0.31)
    await process_combat()
    time.sleep(0.85)
    await process_combat()

    # Combat should have ended and been cleaned up
    assert get_encounter(attacker) is None
    assert get_encounter(defender) is None
    # Mode stacks should have combat popped
    assert attacker.mode_stack == ["normal"]
    assert defender.mode_stack == ["normal"]


@pytest.mark.asyncio
async def test_process_combat_ends_encounter_on_exhaustion(punch):
    """Test process_combat ends encounter when attacker is exhausted."""
    w = _mock_writer
    attacker = Player(
        name="Goku",
        x=0,
        y=0,
        pl=100.0,
        stamina=punch.stamina_cost,
        writer=w(),
    )
    defender = Player(
        name="Vegeta",
        x=0,
        y=0,
        pl=100.0,
        stamina=50.0,
        writer=w(),
    )

    # Push combat mode onto both stacks
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)

    # Advance to resolution
    time.sleep(0.31)
    await process_combat()
    time.sleep(0.85)
    await process_combat()

    # Combat should have ended
    assert get_encounter(attacker) is None
    assert get_encounter(defender) is None
    assert attacker.mode_stack == ["normal"]
    assert defender.mode_stack == ["normal"]


@pytest.mark.asyncio
async def test_process_combat_continues_with_resources(punch):
    """Test process_combat continues encounter when both have resources."""
    w = _mock_writer
    attacker = Player(name="Goku", x=0, y=0, pl=100.0, stamina=50.0, writer=w())
    defender = Player(name="Vegeta", x=0, y=0, pl=100.0, stamina=50.0, writer=w())

    # Push combat mode onto both stacks
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)

    # Advance to resolution
    time.sleep(0.31)
    await process_combat()
    time.sleep(0.85)
    await process_combat()

    # Combat should still be active (but in IDLE state)
    assert get_encounter(attacker) is encounter
    assert get_encounter(defender) is encounter
    assert attacker.mode_stack == ["normal", "combat"]
    assert defender.mode_stack == ["normal", "combat"]


@pytest.mark.asyncio
async def test_process_combat_sends_messages_on_resolve(punch):
    """Test process_combat sends resolution messages to both players."""
    mock_writer = _mock_writer()
    attacker = Player(name="Goku", x=0, y=0, pl=100.0, stamina=50.0, writer=mock_writer)
    defender_writer = _mock_writer()
    defender = Player(
        name="Vegeta", x=0, y=0, pl=100.0, stamina=50.0, writer=defender_writer
    )

    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    encounter.attack(punch)

    # Advance past telegraph and window
    time.sleep(0.31)
    await process_combat()
    time.sleep(0.85)

    mock_writer.write.reset_mock()
    defender_writer.write.reset_mock()

    await process_combat()

    # Both players should have received messages
    attacker_msgs = [call[0][0] for call in mock_writer.write.call_args_list]
    defender_msgs = [call[0][0] for call in defender_writer.write.call_args_list]

    assert len(attacker_msgs) > 0
    assert len(defender_msgs) > 0


# --- Idle timeout tests ---


@pytest.mark.asyncio
async def test_idle_timeout_ends_encounter():
    """Test encounter times out after 30s of no actions."""
    w = _mock_writer
    attacker = Player(name="Goku", x=0, y=0, writer=w())
    defender = Player(name="Vegeta", x=0, y=0, writer=w())
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    # Force last_action_at to 31 seconds ago
    encounter.last_action_at = time.monotonic() - 31

    await process_combat()

    assert get_encounter(attacker) is None
    assert get_encounter(defender) is None


@pytest.mark.asyncio
async def test_idle_timeout_sends_message():
    """Test timeout sends fizzle message to both players."""
    atk_writer = _mock_writer()
    def_writer = _mock_writer()
    attacker = Player(name="Goku", x=0, y=0, writer=atk_writer)
    defender = Player(name="Vegeta", x=0, y=0, writer=def_writer)
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    encounter.last_action_at = time.monotonic() - 31

    await process_combat()

    atk_msgs = [c[0][0] for c in atk_writer.write.call_args_list]
    def_msgs = [c[0][0] for c in def_writer.write.call_args_list]
    assert any("fizzled" in msg.lower() for msg in atk_msgs)
    assert any("fizzled" in msg.lower() for msg in def_msgs)


@pytest.mark.asyncio
async def test_idle_timeout_pops_combat_mode():
    """Test timeout pops combat mode from both players."""
    w = _mock_writer
    attacker = Player(name="Goku", x=0, y=0, writer=w())
    defender = Player(name="Vegeta", x=0, y=0, writer=w())
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    encounter.last_action_at = time.monotonic() - 31

    await process_combat()

    assert attacker.mode_stack == ["normal"]
    assert defender.mode_stack == ["normal"]


@pytest.mark.asyncio
async def test_recent_action_prevents_timeout():
    """Test recent action prevents idle timeout."""
    w = _mock_writer
    attacker = Player(name="Goku", x=0, y=0, writer=w())
    defender = Player(name="Vegeta", x=0, y=0, writer=w())
    attacker.mode_stack.append("combat")
    defender.mode_stack.append("combat")

    encounter = start_encounter(attacker, defender)
    # last_action_at was set to now by start_encounter

    await process_combat()

    # Should still be active
    assert get_encounter(attacker) is encounter


@pytest.mark.asyncio
async def test_start_encounter_sets_last_action_at():
    """Test start_encounter initializes last_action_at."""
    attacker = Entity(name="Goku", x=0, y=0)
    defender = Entity(name="Vegeta", x=0, y=0)

    before = time.monotonic()
    encounter = start_encounter(attacker, defender)

    assert encounter.last_action_at >= before
