"""Tests for Corpse class and create_corpse factory."""

import time
from unittest.mock import MagicMock

import pytest

from mudlib.container import Container
from mudlib.corpse import Corpse, create_corpse
from mudlib.entity import Mob
from mudlib.mobs import mobs
from mudlib.thing import Thing
from mudlib.zone import Zone


@pytest.fixture(autouse=True)
def clear_mobs():
    """Clear mobs registry before and after each test."""
    mobs.clear()
    yield
    mobs.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_mob(test_zone):
    """Create a goblin mob at (5, 10) in the test zone."""
    mob = Mob(
        name="goblin",
        x=5,
        y=10,
        location=test_zone,
        pl=50.0,
        stamina=40.0,
    )
    mobs.append(mob)
    return mob


@pytest.fixture
def sword():
    """Create a sword Thing."""
    return Thing(name="sword", description="a rusty sword", portable=True)


@pytest.fixture
def potion():
    """Create a potion Thing."""
    return Thing(name="potion", description="a health potion", portable=True)


class TestCorpseClass:
    def test_corpse_is_container_subclass(self):
        """Corpse is a subclass of Container."""
        assert issubclass(Corpse, Container)

    def test_corpse_not_portable(self, test_zone):
        """Corpse is not portable (can't pick up a corpse)."""
        corpse = Corpse(name="test corpse", location=test_zone, x=0, y=0)
        assert corpse.portable is False

    def test_corpse_always_open(self, test_zone):
        """Corpse is always open (closed=False)."""
        corpse = Corpse(name="test corpse", location=test_zone, x=0, y=0)
        assert corpse.closed is False

    def test_corpse_has_decompose_at_field(self, test_zone):
        """Corpse has decompose_at field (float, monotonic time)."""
        decompose_time = time.monotonic() + 300
        corpse = Corpse(
            name="test corpse",
            location=test_zone,
            x=0,
            y=0,
            decompose_at=decompose_time,
        )
        assert hasattr(corpse, "decompose_at")
        assert isinstance(corpse.decompose_at, float)
        assert corpse.decompose_at == decompose_time


class TestCreateCorpseFactory:
    def test_creates_corpse_at_mob_position(self, goblin_mob, test_zone):
        """create_corpse creates a corpse at mob's x, y in the zone."""
        corpse = create_corpse(goblin_mob, test_zone)

        assert isinstance(corpse, Corpse)
        assert corpse.x == 5
        assert corpse.y == 10
        assert corpse.location is test_zone

    def test_corpse_name_from_mob(self, goblin_mob, test_zone):
        """Corpse name is '{mob.name}'s corpse'."""
        corpse = create_corpse(goblin_mob, test_zone)

        assert corpse.name == "goblin's corpse"

    def test_transfers_mob_inventory(self, goblin_mob, test_zone, sword, potion):
        """Transfers mob's inventory items into the corpse."""
        # Add items to mob's inventory
        sword.move_to(goblin_mob)
        potion.move_to(goblin_mob)

        corpse = create_corpse(goblin_mob, test_zone)

        # Items should now be in the corpse
        assert sword in corpse._contents
        assert potion in corpse._contents
        assert sword.location is corpse
        assert potion.location is corpse

    def test_sets_decompose_at(self, goblin_mob, test_zone):
        """Sets decompose_at = time.monotonic() + ttl."""
        before = time.monotonic()
        corpse = create_corpse(goblin_mob, test_zone, ttl=300)
        after = time.monotonic()

        # decompose_at should be within reasonable range
        assert corpse.decompose_at >= before + 300
        assert corpse.decompose_at <= after + 300

    def test_custom_ttl(self, goblin_mob, test_zone):
        """create_corpse respects custom ttl parameter."""
        before = time.monotonic()
        corpse = create_corpse(goblin_mob, test_zone, ttl=600)
        after = time.monotonic()

        assert corpse.decompose_at >= before + 600
        assert corpse.decompose_at <= after + 600

    def test_calls_despawn_mob(self, goblin_mob, test_zone):
        """create_corpse calls despawn_mob to remove mob from registry."""
        assert goblin_mob in mobs
        assert goblin_mob.alive is True

        create_corpse(goblin_mob, test_zone)

        assert goblin_mob not in mobs
        assert goblin_mob.alive is False

    def test_returns_corpse(self, goblin_mob, test_zone):
        """create_corpse returns the created corpse."""
        corpse = create_corpse(goblin_mob, test_zone)

        assert isinstance(corpse, Corpse)
        assert corpse.name == "goblin's corpse"

    def test_empty_inventory(self, goblin_mob, test_zone):
        """create_corpse with a mob that has no inventory creates empty corpse."""
        corpse = create_corpse(goblin_mob, test_zone)

        assert len(corpse._contents) == 0
        assert corpse.name == "goblin's corpse"


class TestCorpseAsContainer:
    def test_can_put_things_in_corpse(self, goblin_mob, test_zone, sword):
        """Container commands work: Things can be put into corpses."""
        corpse = create_corpse(goblin_mob, test_zone)

        # Manually move item into corpse (simulating "put" command)
        sword.move_to(corpse)

        assert sword in corpse._contents
        assert sword.location is corpse

    def test_can_take_things_from_corpse(self, goblin_mob, test_zone, sword, potion):
        """Container commands work: Things can be taken out of corpses."""
        sword.move_to(goblin_mob)
        potion.move_to(goblin_mob)

        corpse = create_corpse(goblin_mob, test_zone)

        # Items start in corpse
        assert sword in corpse._contents
        assert potion in corpse._contents

        # Take sword out (move to zone)
        sword.move_to(test_zone, x=5, y=10)

        assert sword not in corpse._contents
        assert sword.location is test_zone
        assert potion in corpse._contents

    def test_corpse_can_accept_things(self, goblin_mob, test_zone, sword):
        """Corpse.can_accept returns True for Things."""
        corpse = create_corpse(goblin_mob, test_zone)

        assert corpse.can_accept(sword) is True

    def test_corpse_not_portable(self, goblin_mob, test_zone):
        """Corpse cannot be picked up (portable=False)."""
        corpse = create_corpse(goblin_mob, test_zone)

        # Try to move corpse to a mock entity (simulating "get corpse")
        mock_entity = MagicMock()
        mock_entity._contents = []

        # Corpse is not portable, so entity.can_accept would return False
        # (Entity.can_accept checks obj.portable)
        from mudlib.entity import Entity

        dummy_entity = Entity(name="dummy", x=0, y=0, location=test_zone)
        assert dummy_entity.can_accept(corpse) is False


class TestCombatDeathCorpse:
    """Tests for corpse spawning when a mob dies in combat."""

    @pytest.fixture(autouse=True)
    def clear_corpses(self):
        from mudlib.corpse import active_corpses

        active_corpses.clear()
        yield
        active_corpses.clear()

    @pytest.mark.asyncio
    async def test_mob_death_in_combat_spawns_corpse(self, test_zone):
        """Mob death in combat spawns a corpse at mob's position."""
        from mudlib.combat.encounter import CombatState
        from mudlib.combat.engine import (
            active_encounters,
            process_combat,
            start_encounter,
        )
        from mudlib.combat.moves import CombatMove
        from mudlib.entity import Entity

        # Clear active encounters
        active_encounters.clear()

        # Create a weak mob
        mob = Mob(
            name="goblin",
            x=5,
            y=10,
            location=test_zone,
            pl=1.0,
            stamina=40.0,
        )
        mobs.append(mob)

        # Create attacker
        attacker = Entity(
            name="hero",
            x=5,
            y=10,
            location=test_zone,
            pl=100.0,
            stamina=50.0,
        )

        # Start encounter
        encounter = start_encounter(attacker, mob)

        # Set up a lethal move
        punch = CombatMove(
            name="punch right",
            move_type="attack",
            stamina_cost=5.0,
            timing_window_ms=800,
            damage_pct=0.15,
            countered_by=["dodge left"],
        )
        encounter.attack(punch)
        encounter.state = CombatState.RESOLVE

        # Process combat to trigger resolve
        await process_combat()

        # Check for corpse at mob's position
        corpses = [
            obj for obj in test_zone.contents_at(5, 10) if isinstance(obj, Corpse)
        ]
        assert len(corpses) == 1
        assert corpses[0].name == "goblin's corpse"

    @pytest.mark.asyncio
    async def test_mob_death_transfers_inventory_to_corpse(self, test_zone, sword):
        """Mob death transfers inventory to corpse."""
        from mudlib.combat.encounter import CombatState
        from mudlib.combat.engine import (
            active_encounters,
            process_combat,
            start_encounter,
        )
        from mudlib.combat.moves import CombatMove
        from mudlib.entity import Entity

        # Clear active encounters
        active_encounters.clear()

        # Create a weak mob with inventory
        mob = Mob(
            name="goblin",
            x=5,
            y=10,
            location=test_zone,
            pl=1.0,
            stamina=40.0,
        )
        mobs.append(mob)
        sword.move_to(mob)

        # Create attacker
        attacker = Entity(
            name="hero",
            x=5,
            y=10,
            location=test_zone,
            pl=100.0,
            stamina=50.0,
        )

        # Start encounter and kill mob
        encounter = start_encounter(attacker, mob)
        punch = CombatMove(
            name="punch right",
            move_type="attack",
            stamina_cost=5.0,
            timing_window_ms=800,
            damage_pct=0.15,
            countered_by=["dodge left"],
        )
        encounter.attack(punch)
        encounter.state = CombatState.RESOLVE

        # Process combat
        await process_combat()

        # Find corpse
        corpses = [
            obj for obj in test_zone.contents_at(5, 10) if isinstance(obj, Corpse)
        ]
        assert len(corpses) == 1
        corpse = corpses[0]

        # Verify sword is in corpse
        assert sword in corpse._contents
        assert sword.location is corpse

    @pytest.mark.asyncio
    async def test_corpse_appears_in_zone_contents(self, test_zone):
        """Corpse appears in zone.contents_at after mob death."""
        from mudlib.combat.encounter import CombatState
        from mudlib.combat.engine import (
            active_encounters,
            process_combat,
            start_encounter,
        )
        from mudlib.combat.moves import CombatMove
        from mudlib.entity import Entity

        # Clear active encounters
        active_encounters.clear()

        # Create a weak mob
        mob = Mob(
            name="goblin",
            x=5,
            y=10,
            location=test_zone,
            pl=1.0,
            stamina=40.0,
        )
        mobs.append(mob)

        # Create attacker
        attacker = Entity(
            name="hero",
            x=5,
            y=10,
            location=test_zone,
            pl=100.0,
            stamina=50.0,
        )

        # Start encounter and kill mob
        encounter = start_encounter(attacker, mob)
        punch = CombatMove(
            name="punch right",
            move_type="attack",
            stamina_cost=5.0,
            timing_window_ms=800,
            damage_pct=0.15,
            countered_by=["dodge left"],
        )
        encounter.attack(punch)
        encounter.state = CombatState.RESOLVE

        # Process combat
        await process_combat()

        # Verify corpse is in zone contents
        contents = list(test_zone.contents_at(5, 10))
        corpse_count = sum(1 for obj in contents if isinstance(obj, Corpse))
        assert corpse_count == 1

        # Verify it's the goblin's corpse
        corpse = next(obj for obj in contents if isinstance(obj, Corpse))
        assert corpse.name == "goblin's corpse"


class TestCorpseDisplay:
    """Tests for corpse display in look command."""

    @pytest.fixture
    def player(self, test_zone):
        """Create a test player."""
        from unittest.mock import AsyncMock, MagicMock

        from mudlib.player import Player, players

        writer = MagicMock()
        writer.write = MagicMock()
        writer.drain = AsyncMock()
        reader = MagicMock()
        p = Player(
            name="TestPlayer",
            x=5,
            y=10,
            reader=reader,
            writer=writer,
        )
        p.location = test_zone
        test_zone._contents.append(p)
        players[p.name] = p
        yield p
        players.clear()

    @pytest.mark.asyncio
    async def test_corpse_shown_in_look(self, player, test_zone):
        """Corpse appears as 'X is here.' in look output."""
        from mudlib.commands.look import cmd_look

        # Create a corpse on player's tile
        Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() + 300,
        )

        await cmd_look(player, "")

        # Check output for corpse line
        output = "".join(
            call.args[0] for call in player.writer.write.call_args_list if call.args
        )
        assert "goblin's corpse is here." in output

    @pytest.mark.asyncio
    async def test_corpse_not_in_ground_items(self, player, test_zone):
        """Corpse is NOT in 'On the ground:' list, but regular items are."""
        from mudlib.commands.look import cmd_look

        # Create a corpse and a regular item on player's tile
        Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() + 300,
        )
        Thing(name="sword", location=test_zone, x=5, y=10)

        await cmd_look(player, "")

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

        # Corpse should be shown as "is here", not in ground items
        assert "goblin's corpse is here." in output
        # Sword should be in ground items
        assert "On the ground:" in output
        assert "sword" in output
        # Corpse name should NOT appear in the ground items line
        lines = output.split("\r\n")
        ground_line = next((line for line in lines if "On the ground:" in line), None)
        assert ground_line is not None
        assert "goblin's corpse" not in ground_line

    @pytest.mark.asyncio
    async def test_multiple_corpses(self, player, test_zone):
        """Multiple corpses each show as separate 'X is here.' lines."""
        from mudlib.commands.look import cmd_look

        # Create two corpses on player's tile
        Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() + 300,
        )
        Corpse(
            name="orc's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() + 300,
        )

        await cmd_look(player, "")

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

        # Both corpses should appear
        assert "goblin's corpse is here." in output
        assert "orc's corpse is here." in output


class TestDecomposition:
    @pytest.fixture(autouse=True)
    def clear_corpses(self):
        from mudlib.corpse import active_corpses

        active_corpses.clear()
        yield
        active_corpses.clear()

    @pytest.mark.asyncio
    async def test_expired_corpse_removed(self, test_zone):
        """Corpse past its decompose_at is removed from the world."""
        from mudlib.corpse import active_corpses, process_decomposing

        corpse = Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() - 1,  # already expired
        )
        active_corpses.append(corpse)

        await process_decomposing()

        assert corpse not in active_corpses
        assert corpse.location is None
        assert corpse not in test_zone._contents

    @pytest.mark.asyncio
    async def test_unexpired_corpse_stays(self, test_zone):
        """Corpse before its decompose_at stays in the world."""
        from mudlib.corpse import active_corpses, process_decomposing

        corpse = Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() + 300,  # far future
        )
        active_corpses.append(corpse)

        await process_decomposing()

        assert corpse in active_corpses
        assert corpse.location is test_zone

    @pytest.mark.asyncio
    async def test_decomposition_broadcasts_message(self, test_zone):
        """Decomposition broadcasts to entities at the same tile."""
        from unittest.mock import AsyncMock, MagicMock

        from mudlib.corpse import active_corpses, process_decomposing
        from mudlib.player import Player

        # Create player at same position
        writer = MagicMock()
        writer.write = MagicMock()
        writer.drain = AsyncMock()
        reader = MagicMock()
        _player = Player(
            name="hero", x=5, y=10, reader=reader, writer=writer, location=test_zone
        )

        corpse = Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() - 1,
        )
        active_corpses.append(corpse)

        await process_decomposing()

        # Check that decomposition message was written
        messages = [call[0][0] for call in writer.write.call_args_list]
        assert any("goblin's corpse decomposes" in msg for msg in messages)

    @pytest.mark.asyncio
    async def test_decomposition_only_broadcasts_to_same_tile(self, test_zone):
        """Decomposition does NOT broadcast to entities at different tiles."""
        from unittest.mock import AsyncMock, MagicMock

        from mudlib.corpse import active_corpses, process_decomposing
        from mudlib.player import Player

        # Player at different position
        writer = MagicMock()
        writer.write = MagicMock()
        writer.drain = AsyncMock()
        reader = MagicMock()
        _player = Player(
            name="faraway", x=50, y=50, reader=reader, writer=writer, location=test_zone
        )

        corpse = Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() - 1,
        )
        active_corpses.append(corpse)

        await process_decomposing()

        # Check that no decomposition message was written
        messages = [call[0][0] for call in writer.write.call_args_list]
        assert not any("goblin's corpse decomposes" in msg for msg in messages)

    def test_create_corpse_registers_in_active_corpses(self, goblin_mob, test_zone):
        """create_corpse adds corpse to active_corpses list."""
        from mudlib.corpse import active_corpses

        corpse = create_corpse(goblin_mob, test_zone)

        assert corpse in active_corpses

    @pytest.mark.asyncio
    async def test_items_in_decomposed_corpse_are_lost(self, test_zone):
        """Items inside a corpse when it decomposes are removed with it."""
        from mudlib.corpse import active_corpses, process_decomposing

        corpse = Corpse(
            name="goblin's corpse",
            location=test_zone,
            x=5,
            y=10,
            decompose_at=time.monotonic() - 1,
        )
        sword = Thing(name="sword", location=corpse)
        active_corpses.append(corpse)

        await process_decomposing()

        # Corpse is gone
        assert corpse.location is None
        # Sword should also be removed (items rot with the corpse)
        assert sword.location is None


class TestCorpseLoot:
    """Tests for loot drops in corpses."""

    def test_create_corpse_with_loot(self, goblin_mob, test_zone):
        """create_corpse with loot_table rolls loot and adds to corpse."""
        from mudlib.loot import LootEntry

        loot = [LootEntry(name="gold coin", chance=1.0)]
        corpse = create_corpse(goblin_mob, test_zone, loot_table=loot)
        items = [obj for obj in corpse._contents if isinstance(obj, Thing)]
        assert len(items) == 1
        assert items[0].name == "gold coin"

    def test_create_corpse_without_loot(self, goblin_mob, test_zone):
        """create_corpse without loot_table creates empty corpse."""
        corpse = create_corpse(goblin_mob, test_zone)
        assert len(corpse._contents) == 0
