"""End-to-end integration tests for NPC system (behavior + dialogue + schedule)."""

from unittest.mock import AsyncMock, MagicMock

import pytest

from mudlib.commands.talk import cmd_reply, cmd_talk, dialogue_trees
from mudlib.conversation import active_conversations, get_conversation
from mudlib.dialogue import load_dialogue
from mudlib.mob_ai import process_mob_movement
from mudlib.mobs import load_mob_template, mobs, spawn_mob
from mudlib.npc_behavior import transition_state
from mudlib.npc_schedule import process_schedules
from mudlib.player import Player, players
from mudlib.zone import Zone


@pytest.fixture(autouse=True)
def clear_state():
    """Clear global state before and after each test."""
    mobs.clear()
    players.clear()
    active_conversations.clear()
    dialogue_trees.clear()
    yield
    mobs.clear()
    players.clear()
    active_conversations.clear()
    dialogue_trees.clear()


@pytest.fixture
def test_zone():
    """Create a test zone for testing."""
    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="Hero", x=10, y=10, reader=mock_reader, writer=mock_writer)
    p.location = test_zone
    test_zone._contents.append(p)
    players[p.name] = p
    return p


@pytest.fixture
def librarian_template(tmp_path):
    """Create librarian mob template TOML."""
    toml_path = tmp_path / "librarian.toml"
    toml_path.write_text(
        'name = "the librarian"\n'
        'description = "a studious figure in a worn cardigan"\n'
        "pl = 50.0\n"
        "stamina = 50.0\n"
        "max_stamina = 50.0\n"
        "moves = []\n"
        'npc_name = "librarian"\n'
        "\n"
        "[[schedule]]\n"
        "hour = 7\n"
        'state = "working"\n'
        "\n"
        "[[schedule]]\n"
        "hour = 21\n"
        'state = "idle"\n'
    )
    return load_mob_template(toml_path)


@pytest.fixture
def librarian_dialogue(tmp_path):
    """Create librarian dialogue tree TOML."""
    toml_path = tmp_path / "librarian.toml"
    toml_path.write_text(
        'npc_name = "librarian"\n'
        'root_node = "greeting"\n'
        "\n"
        "[nodes.greeting]\n"
        'text = "Welcome to the library. How can I help you today?"\n'
        "\n"
        "[[nodes.greeting.choices]]\n"
        'text = "I\'m looking for a book."\n'
        'next_node = "book_search"\n'
        "\n"
        "[[nodes.greeting.choices]]\n"
        'text = "Tell me about this place."\n'
        'next_node = "about_library"\n'
        "\n"
        "[[nodes.greeting.choices]]\n"
        'text = "Goodbye."\n'
        'next_node = "farewell"\n'
        "\n"
        "[nodes.book_search]\n"
        'text = "We have quite the collection. Browse the shelves."\n'
        "\n"
        "[[nodes.book_search.choices]]\n"
        'text = "Thanks, I\'ll look around."\n'
        'next_node = "farewell"\n'
        "\n"
        "[nodes.about_library]\n"
        'text = "This library was built to preserve the old stories."\n'
        "\n"
        "[[nodes.about_library.choices]]\n"
        'text = "Thanks for the information."\n'
        'next_node = "farewell"\n'
        "\n"
        "[nodes.farewell]\n"
        'text = "Happy reading. Come back anytime."\n'
    )
    return load_dialogue(toml_path)


class TestLibrarianSpawn:
    def test_spawn_from_template_has_npc_name(self, librarian_template, test_zone):
        """Librarian spawned from template has npc_name field set."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)

        assert mob.npc_name == "librarian"

    def test_spawn_from_template_has_schedule(self, librarian_template, test_zone):
        """Librarian spawned from template has schedule loaded."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)

        assert mob.schedule is not None
        assert len(mob.schedule.entries) == 2
        assert mob.schedule.entries[0].hour == 7
        assert mob.schedule.entries[0].state == "working"
        assert mob.schedule.entries[1].hour == 21
        assert mob.schedule.entries[1].state == "idle"

    def test_spawn_from_template_no_combat_moves(self, librarian_template, test_zone):
        """Librarian has empty moves list (non-combatant)."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)

        assert mob.moves == []


class TestLibrarianConversation:
    @pytest.mark.asyncio
    async def test_talk_to_librarian_starts_dialogue(
        self, player, librarian_template, librarian_dialogue, test_zone
    ):
        """Player can talk to librarian and start conversation."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)
        dialogue_trees["librarian"] = librarian_dialogue

        await cmd_talk(player, "the librarian")

        # Conversation started
        conv = get_conversation(player)
        assert conv is not None
        assert conv.npc == mob
        assert conv.current_node == "greeting"

        # Mob transitioned to converse state
        assert mob.behavior_state == "converse"

        # Player received greeting
        output = "".join([call[0][0] for call in player.writer.write.call_args_list])
        assert "Welcome to the library" in output

    @pytest.mark.asyncio
    async def test_conversation_full_flow(
        self, player, librarian_template, librarian_dialogue, test_zone
    ):
        """Player can walk through dialogue tree and end conversation."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)
        dialogue_trees["librarian"] = librarian_dialogue

        # Store mob's initial state
        transition_state(mob, "working")

        # Start conversation
        await cmd_talk(player, "the librarian")
        assert mob.behavior_state == "converse"

        # Reset mock to only capture reply
        player.writer.write.reset_mock()

        # Choose option 1: "I'm looking for a book."
        await cmd_reply(player, "1")

        output = "".join([call[0][0] for call in player.writer.write.call_args_list])
        assert "Browse the shelves" in output

        # Reset mock
        player.writer.write.reset_mock()

        # Choose option 1: "Thanks, I'll look around."
        await cmd_reply(player, "1")

        output = "".join([call[0][0] for call in player.writer.write.call_args_list])
        assert "Happy reading" in output

        # Conversation ended
        assert get_conversation(player) is None

        # Mob returned to previous state
        assert mob.behavior_state == "working"


class TestConverseBehaviorBlocksMovement:
    @pytest.mark.asyncio
    async def test_librarian_in_conversation_doesnt_move(
        self, player, librarian_template, librarian_dialogue, test_zone
    ):
        """Librarian in conversation stays put (doesn't wander)."""
        mob = spawn_mob(
            librarian_template,
            10,
            10,
            test_zone,
            home_region={"x": [0, 5], "y": [0, 5]},
        )
        dialogue_trees["librarian"] = librarian_dialogue
        mob.next_action_at = 0.0

        # Start conversation (transitions to converse state)
        await cmd_talk(player, "the librarian")

        original_x, original_y = mob.x, mob.y

        # Process movement
        await process_mob_movement()

        # Mob didn't move
        assert mob.x == original_x
        assert mob.y == original_y


class TestScheduleTransitions:
    @pytest.mark.asyncio
    async def test_schedule_transitions_behavior_state(
        self, librarian_template, test_zone
    ):
        """Librarian schedule transitions state at specified hours."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)

        # Process schedules at 7:00 (working state)
        process_schedules([mob], 7)

        assert mob.behavior_state == "working"

        # Advance to 21:00 (idle state)
        process_schedules([mob], 21)

        assert mob.behavior_state == "idle"

    @pytest.mark.asyncio
    async def test_working_state_prevents_movement(self, librarian_template, test_zone):
        """Librarian in working state doesn't wander."""
        mob = spawn_mob(
            librarian_template,
            50,
            50,
            test_zone,
            home_region={"x": [0, 5], "y": [0, 5]},
        )
        mob.next_action_at = 0.0

        # Set to working state (simulating schedule transition)
        transition_state(mob, "working")

        original_x, original_y = mob.x, mob.y

        # Process movement
        await process_mob_movement()

        # Mob didn't move (working state blocks movement)
        assert mob.x == original_x
        assert mob.y == original_y


class TestPatrolBehavior:
    @pytest.mark.asyncio
    async def test_librarian_patrol_follows_waypoints(
        self, librarian_template, test_zone
    ):
        """Librarian can patrol between waypoints."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)
        mob.next_action_at = 0.0

        # Set patrol with waypoints
        waypoints = [{"x": 20, "y": 10}, {"x": 20, "y": 20}]
        transition_state(mob, "patrol", {"waypoints": waypoints, "waypoint_index": 0})

        # Process movement
        await process_mob_movement()

        # Mob moved toward first waypoint (east)
        assert mob.x == 11
        assert mob.y == 10


class TestNonCombatantNPC:
    def test_librarian_has_no_combat_moves(self, librarian_template, test_zone):
        """Librarian has empty moves list (cannot fight)."""
        mob = spawn_mob(librarian_template, 10, 10, test_zone)

        assert mob.moves == []
        assert len(mob.moves) == 0
