"""Tests for the verb system on Object."""

from unittest.mock import AsyncMock, MagicMock

import pytest

from mudlib.object import Object
from mudlib.player import Player
from mudlib.thing import Thing
from mudlib.verbs import find_object, verb
from mudlib.zone import 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 test_zone():
    terrain = [["." for _ in range(10)] for _ in range(10)]
    return Zone(name="testzone", width=10, height=10, terrain=terrain)


@pytest.fixture
def player(mock_reader, mock_writer, test_zone):
    return Player(
        name="TestPlayer",
        x=5,
        y=5,
        reader=mock_reader,
        writer=mock_writer,
        location=test_zone,
    )


def test_object_starts_with_empty_verbs():
    """Object starts with empty _verbs dict."""
    obj = Object(name="rock")
    assert obj._verbs == {}


def test_register_verb():
    """register_verb adds a verb to the _verbs dict."""

    async def test_handler(obj, player, args):
        pass

    obj = Object(name="fountain")
    obj.register_verb("drink", test_handler)
    assert "drink" in obj._verbs
    assert obj._verbs["drink"] is test_handler


def test_get_verb_returns_handler():
    """get_verb returns the registered handler."""

    async def test_handler(obj, player, args):
        pass

    obj = Object(name="fountain")
    obj.register_verb("drink", test_handler)
    assert obj.get_verb("drink") is test_handler


def test_get_verb_returns_none_for_unknown():
    """get_verb returns None for unknown verb."""
    obj = Object(name="rock")
    assert obj.get_verb("nonexistent") is None


def test_has_verb_returns_true_when_present():
    """has_verb returns True when verb is registered."""

    async def test_handler(obj, player, args):
        pass

    obj = Object(name="fountain")
    obj.register_verb("drink", test_handler)
    assert obj.has_verb("drink") is True


def test_has_verb_returns_false_when_absent():
    """has_verb returns False when verb is not registered."""
    obj = Object(name="rock")
    assert obj.has_verb("drink") is False


def test_verb_decorator_marks_method():
    """@verb decorator marks method with _verb_name attribute."""

    class TestObject(Object):
        @verb("test")
        async def test_verb(self, player, args):
            pass

    assert hasattr(TestObject.test_verb, "_verb_name")
    assert TestObject.test_verb._verb_name == "test"


def test_verb_decorator_auto_registers_on_instantiation():
    """Class with @verb methods auto-registers verbs on instantiation."""

    class Fountain(Thing):
        @verb("drink")
        async def drink(self, player, args):
            pass

    fountain = Fountain(name="fountain")
    assert fountain.has_verb("drink")
    assert fountain.get_verb("drink") is not None


def test_multiple_verbs_on_same_class():
    """Multiple verbs on same class all register."""

    class Fountain(Thing):
        @verb("drink")
        async def drink(self, player, args):
            pass

        @verb("splash")
        async def splash(self, player, args):
            pass

    fountain = Fountain(name="fountain")
    assert fountain.has_verb("drink")
    assert fountain.has_verb("splash")


@pytest.mark.asyncio
async def test_verb_handler_can_be_called(player):
    """Verb handler can be called as an async function."""
    called = False
    received_args = None

    class Fountain(Thing):
        @verb("drink")
        async def drink(self, player, args):
            nonlocal called, received_args
            called = True
            received_args = args

    fountain = Fountain(name="fountain")
    handler = fountain.get_verb("drink")
    assert handler is not None
    # handler is a bound method, so we don't pass self/fountain again
    await handler(player, "from the left side")

    assert called
    assert received_args == "from the left side"


def test_verb_registered_on_parent_class_propagates_to_subclass():
    """Verb registered on parent class propagates to subclass instances."""

    class BaseFountain(Thing):
        @verb("drink")
        async def drink(self, player, args):
            pass

    class FancyFountain(BaseFountain):
        @verb("splash")
        async def splash(self, player, args):
            pass

    fancy = FancyFountain(name="fancy fountain")
    assert fancy.has_verb("drink")
    assert fancy.has_verb("splash")


def test_find_object_by_name_in_inventory(player):
    """find_object finds object by name in inventory."""
    sword = Thing(name="sword", location=player)
    found = find_object("sword", player)
    assert found is sword


def test_find_object_by_alias_in_inventory(player):
    """find_object finds object by alias in inventory."""
    can = Thing(name="pepsi can", aliases=["can", "pepsi"], location=player)
    found = find_object("can", player)
    assert found is can


def test_find_object_by_name_on_ground(player, test_zone):
    """find_object finds object by name on ground."""
    rock = Thing(name="rock", location=test_zone, x=5, y=5)
    found = find_object("rock", player)
    assert found is rock


def test_find_object_by_alias_on_ground(player, test_zone):
    """find_object finds object by alias on ground."""
    can = Thing(
        name="pepsi can", aliases=["can", "pepsi"], location=test_zone, x=5, y=5
    )
    found = find_object("pepsi", player)
    assert found is can


def test_find_object_prefers_inventory_over_ground(player, test_zone):
    """find_object prefers inventory over ground when matching object in both."""
    Thing(name="sword", location=test_zone, x=5, y=5)
    inv_sword = Thing(name="sword", location=player)
    found = find_object("sword", player)
    assert found is inv_sword


def test_find_object_returns_none_when_nothing_matches(player):
    """find_object returns None when nothing matches."""
    found = find_object("nonexistent", player)
    assert found is None


def test_find_object_searches_all_objects_not_just_things(player, test_zone):
    """find_object searches all Objects, not just Things."""

    class CustomObject(Object):
        def __init__(self, name, aliases=None, **kwargs):
            super().__init__(name=name, **kwargs)
            self.aliases = aliases or []

    custom = CustomObject(name="widget", aliases=["gadget"], location=player)
    found = find_object("widget", player)
    assert found is custom
    found_by_alias = find_object("gadget", player)
    assert found_by_alias is custom


def test_find_object_skips_self(player, test_zone):
    """find_object should not return the querying player."""
    player.move_to(test_zone, x=0, y=0)
    result = find_object(player.name, player)
    assert result is None
