"""Tests for mob templates, registry, spawn/despawn, and combat integration."""

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

import pytest

from mudlib.combat import commands as combat_commands
from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import active_encounters, get_encounter
from mudlib.combat.moves import load_moves
from mudlib.entity import Mob
from mudlib.mobs import (
    despawn_mob,
    get_nearby_mob,
    load_mob_template,
    load_mob_templates,
    mobs,
    spawn_mob,
)
from mudlib.player import Player, players
from mudlib.zone import Zone


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


@pytest.fixture
def test_zone():
    """Create a test zone for entities."""
    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 goblin_toml(tmp_path):
    """Create a goblin TOML file."""
    path = tmp_path / "goblin.toml"
    path.write_text(
        'name = "goblin"\n'
        'description = "a snarling goblin with a crude club"\n'
        "pl = 50.0\n"
        "stamina = 40.0\n"
        "max_stamina = 40.0\n"
        'moves = ["punch left", "punch right", "sweep"]\n'
    )
    return path


@pytest.fixture
def dummy_toml(tmp_path):
    """Create a training dummy TOML file."""
    path = tmp_path / "training_dummy.toml"
    path.write_text(
        'name = "training dummy"\n'
        'description = "a battered wooden training dummy"\n'
        "pl = 200.0\n"
        "stamina = 100.0\n"
        "max_stamina = 100.0\n"
        "moves = []\n"
    )
    return path


class TestLoadTemplate:
    def test_load_single_template(self, goblin_toml):
        template = load_mob_template(goblin_toml)
        assert template.name == "goblin"
        assert template.description == "a snarling goblin with a crude club"
        assert template.pl == 50.0
        assert template.stamina == 40.0
        assert template.max_stamina == 40.0
        assert template.moves == ["punch left", "punch right", "sweep"]

    def test_load_template_no_moves(self, dummy_toml):
        template = load_mob_template(dummy_toml)
        assert template.name == "training dummy"
        assert template.moves == []

    def test_load_all_templates(self, goblin_toml, dummy_toml):
        templates = load_mob_templates(goblin_toml.parent)
        assert "goblin" in templates
        assert "training dummy" in templates
        assert len(templates) == 2


class TestSpawnDespawn:
    def test_spawn_creates_mob(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 10, 20, test_zone)
        assert isinstance(mob, Mob)
        assert mob.name == "goblin"
        assert mob.x == 10
        assert mob.y == 20
        assert mob.pl == 50.0
        assert mob.stamina == 40.0
        assert mob.max_stamina == 40.0
        assert mob.moves == ["punch left", "punch right", "sweep"]
        assert mob.alive is True
        assert mob in mobs
        assert mob.location is test_zone
        assert mob in test_zone._contents

    def test_spawn_adds_to_registry(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        spawn_mob(template, 0, 0, test_zone)
        spawn_mob(template, 5, 5, test_zone)
        assert len(mobs) == 2

    def test_despawn_removes_from_list(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 0, 0, test_zone)
        despawn_mob(mob)
        assert mob not in mobs
        assert mob.alive is False

    def test_despawn_sets_alive_false(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 0, 0, test_zone)
        despawn_mob(mob)
        assert mob.alive is False


class TestGetNearbyMob:
    def test_finds_by_name_within_range(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 5, 5, test_zone)
        found = get_nearby_mob("goblin", 3, 3, test_zone)
        assert found is mob

    def test_returns_none_when_out_of_range(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        spawn_mob(template, 100, 100, test_zone)
        found = get_nearby_mob("goblin", 0, 0, test_zone)
        assert found is None

    def test_returns_none_for_wrong_name(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        spawn_mob(template, 5, 5, test_zone)
        found = get_nearby_mob("dragon", 3, 3, test_zone)
        assert found is None

    def test_picks_closest_when_multiple(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        spawn_mob(template, 8, 8, test_zone)
        close_mob = spawn_mob(template, 1, 1, test_zone)
        found = get_nearby_mob("goblin", 0, 0, test_zone)
        assert found is close_mob

    def test_skips_dead_mobs(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 5, 5, test_zone)
        mob.alive = False
        found = get_nearby_mob("goblin", 3, 3, test_zone)
        assert found is None

    def test_wrapping_distance(self, goblin_toml, test_zone):
        """Mob near world edge is close to player at opposite edge."""
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 254, 254, test_zone)
        found = get_nearby_mob("goblin", 2, 2, test_zone, range_=10)
        assert found is mob


# --- Phase 2: target resolution tests ---


@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 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):
    return moves["punch right"]


class TestTargetResolution:
    @pytest.mark.asyncio
    async def test_attack_mob_by_name(
        self, player, punch_right, goblin_toml, test_zone
    ):
        """do_attack with mob name finds and engages the mob."""
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 0, 0, test_zone)

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

        encounter = get_encounter(player)
        assert encounter is not None
        assert encounter.attacker is player
        assert encounter.defender is mob

    @pytest.mark.asyncio
    async def test_attack_prefers_player_over_mob(
        self, player, punch_right, goblin_toml, mock_reader, mock_writer, test_zone
    ):
        """When a player and mob share a name, player takes priority."""
        template = load_mob_template(goblin_toml)
        spawn_mob(template, 0, 0, test_zone)

        # Create a player named "goblin"
        goblin_player = Player(
            name="goblin",
            x=0,
            y=0,
            reader=mock_reader,
            writer=mock_writer,
        )
        goblin_player.location = test_zone
        test_zone._contents.append(goblin_player)
        players["goblin"] = goblin_player

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

        encounter = get_encounter(player)
        assert encounter is not None
        assert encounter.defender is goblin_player

    @pytest.mark.asyncio
    async def test_attack_mob_out_of_range(
        self, player, punch_right, goblin_toml, test_zone
    ):
        """Mob outside viewport range is not found as target."""
        template = load_mob_template(goblin_toml)
        spawn_mob(template, 100, 100, test_zone)

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

        encounter = get_encounter(player)
        assert encounter is None
        messages = [call[0][0] for call in player.writer.write.call_args_list]
        assert any("need a target" in msg.lower() for msg in messages)

    @pytest.mark.asyncio
    async def test_encounter_mob_no_mode_push(
        self, player, punch_right, goblin_toml, test_zone
    ):
        """Mob doesn't get mode_stack push (it has no mode_stack)."""
        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 0, 0, test_zone)

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

        # Player should be in combat mode
        assert player.mode == "combat"
        # Mob has no mode_stack attribute
        assert not hasattr(mob, "mode_stack")


# --- Phase 3: viewport rendering tests ---


class TestViewportRendering:
    @pytest.fixture
    def look_world(self):
        """A mock world that returns a flat viewport of '.' tiles."""
        from mudlib.commands.look import VIEWPORT_HEIGHT, VIEWPORT_WIDTH

        w = MagicMock()
        w.width = 256
        w.height = 256
        w.get_viewport = MagicMock(
            return_value=[
                ["." for _ in range(VIEWPORT_WIDTH)] for _ in range(VIEWPORT_HEIGHT)
            ]
        )
        w.wrap = lambda x, y: (x % 256, y % 256)
        w.is_passable = MagicMock(return_value=True)
        return w

    @pytest.mark.asyncio
    async def test_mob_renders_as_star(
        self, player, goblin_toml, look_world, test_zone
    ):
        """Mob within viewport renders as * in look output."""
        import mudlib.commands.look as look_mod

        template = load_mob_template(goblin_toml)
        # Place mob 2 tiles to the right of the player
        spawn_mob(template, 2, 0, test_zone)

        await look_mod.cmd_look(player, "")

        output = "".join(call[0][0] for call in player.writer.write.call_args_list)
        # The center is at (10, 5), mob at relative (12, 5)
        # Output should contain a * character
        assert "*" in output

    @pytest.mark.asyncio
    async def test_mob_outside_viewport_not_rendered(
        self, player, goblin_toml, look_world, test_zone
    ):
        """Mob outside viewport bounds is not rendered."""
        import mudlib.commands.look as look_mod

        template = load_mob_template(goblin_toml)
        # Place mob far away
        spawn_mob(template, 100, 100, test_zone)

        await look_mod.cmd_look(player, "")

        output = "".join(call[0][0] for call in player.writer.write.call_args_list)
        # Should only have @ (player) and . (terrain), no *
        stripped = output.replace("\033[0m", "").replace("\r\n", "")
        # Remove ANSI codes for terrain colors
        import re

        stripped = re.sub(r"\033\[[0-9;]*m", "", stripped)
        assert "*" not in stripped

    @pytest.mark.asyncio
    async def test_dead_mob_not_rendered(
        self, player, goblin_toml, look_world, test_zone
    ):
        """Dead mob (alive=False) not rendered in viewport."""
        import mudlib.commands.look as look_mod

        template = load_mob_template(goblin_toml)
        mob = spawn_mob(template, 2, 0, test_zone)
        mob.alive = False

        await look_mod.cmd_look(player, "")

        output = "".join(call[0][0] for call in player.writer.write.call_args_list)
        import re

        stripped = re.sub(r"\033\[[0-9;]*m", "", output).replace("\r\n", "")
        assert "*" not in stripped


# --- Phase 4: mob defeat tests ---


class TestMobDefeat:
    @pytest.fixture
    def goblin_mob(self, goblin_toml, test_zone):
        template = load_mob_template(goblin_toml)
        return spawn_mob(template, 0, 0, test_zone)

    @pytest.mark.asyncio
    async def test_mob_despawned_on_pl_zero(self, player, goblin_mob, punch_right):
        """Mob with PL <= 0 gets despawned after combat resolves."""
        from mudlib.combat.engine import process_combat, start_encounter

        encounter = start_encounter(player, goblin_mob)
        player.mode_stack.append("combat")

        # Set mob PL very low so attack kills it
        goblin_mob.pl = 1.0

        # Attack and force resolution
        encounter.attack(punch_right)
        encounter.state = CombatState.RESOLVE

        await process_combat()

        assert goblin_mob not in mobs
        assert goblin_mob.alive is False

    @pytest.mark.asyncio
    async def test_player_gets_victory_message(self, player, goblin_mob, punch_right):
        """Player receives a victory message when mob is defeated."""
        from mudlib.combat.engine import process_combat, start_encounter

        encounter = start_encounter(player, goblin_mob)
        player.mode_stack.append("combat")

        goblin_mob.pl = 1.0
        encounter.attack(punch_right)
        encounter.state = CombatState.RESOLVE

        await process_combat()

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

    @pytest.mark.asyncio
    async def test_mob_stamina_depleted_despawns(self, player, goblin_mob, punch_right):
        """Mob is despawned when attacker stamina depleted (combat end)."""
        from mudlib.combat.engine import process_combat, start_encounter

        encounter = start_encounter(player, goblin_mob)
        player.mode_stack.append("combat")

        # Drain player stamina so combat ends on exhaustion
        player.stamina = 0.0
        encounter.attack(punch_right)
        encounter.state = CombatState.RESOLVE

        await process_combat()

        # Encounter should have ended
        assert get_encounter(player) is None

    @pytest.mark.asyncio
    async def test_player_defeat_not_despawned(self, player, goblin_mob, punch_right):
        """When player loses, player is not despawned."""
        from mudlib.combat.engine import process_combat, start_encounter

        # Mob attacks player — mob is attacker, player is defender
        encounter = start_encounter(goblin_mob, player)
        player.mode_stack.append("combat")

        player.pl = 1.0
        encounter.attack(punch_right)
        encounter.state = CombatState.RESOLVE

        await process_combat()

        # Player should get defeat message, not be despawned
        messages = [call[0][0] for call in player.writer.write.call_args_list]
        assert any(
            "defeated" in msg.lower() or "damage" in msg.lower() for msg in messages
        )
        # Player is still in players dict (not removed)
        assert player.name in players
