"""Tests for combat commands."""

import time
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

import pytest

from mudlib.combat import commands as combat_commands
from mudlib.combat.engine import active_encounters, get_encounter
from mudlib.combat.moves import load_moves
from mudlib.player import Player, players
from mudlib.zone import Zone


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


@pytest.fixture
def test_zone():
    """Create a test zone for players."""
    terrain = [["." for _ in range(256)] for _ in range(256)]
    zone = Zone(
        name="testzone",
        width=256,
        height=256,
        toroidal=True,
        terrain=terrain,
        impassable=set(),
    )
    return zone


@pytest.fixture
def mock_writer():
    writer = MagicMock()
    writer.write = MagicMock()
    writer.drain = AsyncMock()
    return writer


@pytest.fixture
def mock_reader():
    return MagicMock()


@pytest.fixture
def player(mock_reader, mock_writer, test_zone):
    p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
    p.location = test_zone
    test_zone._contents.append(p)
    players[p.name] = p
    return p


@pytest.fixture
def target(mock_reader, mock_writer, test_zone):
    t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
    t.location = test_zone
    test_zone._contents.append(t)
    players[t.name] = t
    return t


@pytest.fixture
def moves():
    """Load combat moves from content directory."""
    content_dir = Path(__file__).parent.parent / "content" / "combat"
    return load_moves(content_dir)


@pytest.fixture(autouse=True)
def inject_moves(moves):
    """Inject loaded moves into combat commands module."""
    combat_commands.combat_moves = moves
    yield
    combat_commands.combat_moves = {}


@pytest.fixture
def punch_right(moves):
    """Get the punch right move."""
    return moves["punch right"]


@pytest.fixture
def punch_left(moves):
    """Get the punch left move."""
    return moves["punch left"]


@pytest.fixture
def dodge_left(moves):
    """Get the dodge left move."""
    return moves["dodge left"]


# --- do_attack tests ---


@pytest.mark.asyncio
async def test_attack_starts_combat_with_target(player, target, punch_right):
    """Test do_attack with target starts combat encounter."""
    await combat_commands.do_attack(player, "Vegeta", punch_right)

    encounter = get_encounter(player)
    assert encounter is not None
    assert encounter.attacker is player
    assert encounter.defender is target
    assert player.mode == "combat"


@pytest.mark.asyncio
async def test_attack_without_target_when_not_in_combat(player, punch_right):
    """Test do_attack without target when not in combat gives error."""
    await combat_commands.do_attack(player, "", punch_right)

    player.writer.write.assert_called()
    message = player.writer.write.call_args[0][0]
    assert "need a target" in message.lower()


@pytest.mark.asyncio
async def test_attack_without_target_when_in_combat(
    player, target, punch_right, punch_left
):
    """Test do_attack without target when in combat uses implicit target."""
    # Start combat first
    await combat_commands.do_attack(player, "Vegeta", punch_right)

    # Reset mock to track new calls
    player.writer.write.reset_mock()

    # Attack without target should work
    await combat_commands.do_attack(player, "", punch_left)

    encounter = get_encounter(player)
    assert encounter is not None
    assert encounter.current_move is not None
    assert encounter.current_move.name == "punch left"


@pytest.mark.asyncio
async def test_attack_insufficient_stamina(player, target, punch_right):
    """Test do_attack with insufficient stamina gives error."""
    player.stamina = 1.0  # Not enough for punch (costs 5)

    await combat_commands.do_attack(player, "Vegeta", punch_right)

    player.writer.write.assert_called()
    message = player.writer.write.call_args[0][0]
    assert "stamina" in message.lower()


@pytest.mark.asyncio
async def test_attack_sends_telegraph_to_defender(player, target, punch_right):
    """Test do_attack sends telegraph message to defender."""
    await combat_commands.do_attack(player, "Vegeta", punch_right)

    # Check that encounter has the move
    encounter = get_encounter(player)
    assert encounter is not None
    assert encounter.current_move is not None
    assert encounter.current_move.name == "punch right"


# --- do_defend tests ---


@pytest.mark.asyncio
async def test_defense_works_outside_combat(player, dodge_left):
    """Test do_defend works outside combat (costs stamina, 'the air' message)."""
    initial_stamina = player.stamina
    await combat_commands.do_defend(player, "", dodge_left)

    assert player.stamina == initial_stamina - dodge_left.stamina_cost
    messages = [call[0][0] for call in player.writer.write.call_args_list]
    assert any("dodge the air" in msg.lower() for msg in messages)


@pytest.mark.asyncio
async def test_defense_records_pending_defense(player, target, punch_right, dodge_left):
    """Test do_defend queues defense on encounter and costs stamina."""
    # Start combat
    await combat_commands.do_attack(player, "Vegeta", punch_right)
    player.writer.write.reset_mock()

    initial_stamina = target.stamina

    # Defend
    await combat_commands.do_defend(target, "", dodge_left)

    encounter = get_encounter(target)
    assert encounter is not None
    assert encounter.pending_defense is not None
    assert encounter.pending_defense.name == "dodge left"
    assert target.stamina == initial_stamina - dodge_left.stamina_cost


@pytest.mark.asyncio
async def test_defense_insufficient_stamina(player, dodge_left):
    """Test do_defend with insufficient stamina gives error."""
    player.stamina = 1.0  # Not enough for dodge (costs 3)

    await combat_commands.do_defend(player, "", dodge_left)

    player.writer.write.assert_called()
    messages = [call[0][0] for call in player.writer.write.call_args_list]
    assert any("stamina" in msg.lower() for msg in messages)


# --- variant handler tests ---


@pytest.mark.asyncio
async def test_variant_handler_parses_direction(player, target, moves):
    """Test the variant handler parses direction from args."""
    variant_moves = {
        "left": moves["punch left"],
        "right": moves["punch right"],
    }
    handler = combat_commands.make_variant_handler(
        "punch", variant_moves, combat_commands.do_attack
    )

    await handler(player, "right Vegeta")

    encounter = get_encounter(player)
    assert encounter is not None
    assert encounter.current_move is not None
    assert encounter.current_move.name == "punch right"


@pytest.mark.asyncio
async def test_variant_handler_no_direction(player, moves):
    """Test the variant handler prompts when no direction given."""
    variant_moves = {
        "left": moves["punch left"],
        "right": moves["punch right"],
    }
    handler = combat_commands.make_variant_handler(
        "punch", variant_moves, combat_commands.do_attack
    )

    await handler(player, "")

    player.writer.write.assert_called()
    message = player.writer.write.call_args[0][0]
    assert "which way" in message.lower()


@pytest.mark.asyncio
async def test_variant_handler_bad_direction(player, moves):
    """Test the variant handler rejects invalid direction."""
    variant_moves = {
        "left": moves["punch left"],
        "right": moves["punch right"],
    }
    handler = combat_commands.make_variant_handler(
        "punch", variant_moves, combat_commands.do_attack
    )

    await handler(player, "up Vegeta")

    player.writer.write.assert_called()
    message = player.writer.write.call_args[0][0]
    assert "unknown" in message.lower()


# --- direct handler tests ---


@pytest.mark.asyncio
async def test_direct_handler_passes_move(player, target, punch_right):
    """Test the direct handler passes the bound move through."""
    handler = combat_commands.make_direct_handler(
        punch_right, combat_commands.do_attack
    )

    await handler(player, "Vegeta")

    encounter = get_encounter(player)
    assert encounter is not None
    assert encounter.current_move is not None
    assert encounter.current_move.name == "punch right"


@pytest.mark.asyncio
async def test_direct_handler_alias_for_variant(player, target, punch_right):
    """Test direct handler works for variant moves."""
    handler = combat_commands.make_direct_handler(
        punch_right, combat_commands.do_attack
    )

    await handler(player, "Vegeta")

    encounter = get_encounter(player)
    assert encounter is not None
    assert encounter.current_move is not None
    assert encounter.current_move.name == "punch right"
    assert encounter.attacker is player
    assert encounter.defender is target


# --- attack switching tests ---


@pytest.mark.asyncio
async def test_switch_attack_shows_switch_message(
    player, target, punch_right, punch_left
):
    """Test switching attack says 'switch to' instead of 'use'."""
    await combat_commands.do_attack(player, "Vegeta", punch_right)
    player.writer.write.reset_mock()

    await combat_commands.do_attack(player, "", punch_left)

    messages = [call[0][0] for call in player.writer.write.call_args_list]
    assert any("switch to" in msg.lower() for msg in messages)


@pytest.mark.asyncio
async def test_switch_attack_sends_new_telegraph(
    player, target, punch_right, punch_left
):
    """Test switching attack sends new telegraph to defender."""
    await combat_commands.do_attack(player, "Vegeta", punch_right)
    target.writer.write.reset_mock()

    await combat_commands.do_attack(player, "", punch_left)

    target_msgs = [call[0][0] for call in target.writer.write.call_args_list]
    # Defender should get a new telegraph with the new move's text
    assert any("left arm" in msg.lower() for msg in target_msgs)


# --- defense commitment tests ---


@pytest.mark.asyncio
async def test_defense_blocks_for_timing_window(player, dodge_left):
    """Test defense sleeps for timing_window_ms (commitment via blocking)."""
    before = time.monotonic()
    await combat_commands.do_defend(player, "", dodge_left)
    elapsed = time.monotonic() - before

    expected = dodge_left.timing_window_ms / 1000.0
    assert elapsed >= expected - 0.05


@pytest.mark.asyncio
async def test_defense_in_combat_queues_on_encounter(
    player, target, punch_right, dodge_left
):
    """Test defense in combat queues on encounter and shows move name."""
    await combat_commands.do_attack(player, "Vegeta", punch_right)

    await combat_commands.do_defend(target, "", dodge_left)

    encounter = get_encounter(target)
    assert encounter is not None
    assert encounter.pending_defense is dodge_left
    # In combat: full move name (not "the air")
    messages = [call[0][0] for call in target.writer.write.call_args_list]
    assert any("dodge left" in msg.lower() for msg in messages)


@pytest.mark.asyncio
async def test_defense_broadcast_to_nearby(player, target, dodge_left):
    """Test defense broadcasts to nearby players."""
    target.writer.write.reset_mock()

    await combat_commands.do_defend(player, "", dodge_left)

    # Target is at same coords, should get broadcast
    target_msgs = [call[0][0] for call in target.writer.write.call_args_list]
    assert any(player.name in msg for msg in target_msgs)
