"""Tests for talk command and conversation system."""

import tempfile
from pathlib import Path
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,
    advance_conversation,
    end_conversation,
    get_conversation,
    start_conversation,
)
from mudlib.dialogue import load_dialogue
from mudlib.entity import Mob
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."""
    players.clear()
    active_conversations.clear()
    dialogue_trees.clear()
    yield
    players.clear()
    active_conversations.clear()
    dialogue_trees.clear()


@pytest.fixture
def test_zone():
    """Create a test zone for players and mobs."""
    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_tree():
    """Create a simple dialogue tree for testing."""
    toml_content = """
npc_name = "librarian"
root_node = "greeting"

[nodes.greeting]
text = "Welcome to the library. How can I help you today?"

[[nodes.greeting.choices]]
text = "I'm looking for a book."
next_node = "books"

[[nodes.greeting.choices]]
text = "Tell me about this place."
next_node = "about"

[[nodes.greeting.choices]]
text = "Goodbye."
next_node = "farewell"

[nodes.books]
text = "We have many books. What genre interests you?"

[[nodes.books.choices]]
text = "History."
next_node = "history"

[[nodes.books.choices]]
text = "Never mind."
next_node = "farewell"

[nodes.history]
text = "The history section is on the second floor."

[nodes.about]
text = "This library has been here for 200 years."

[nodes.farewell]
text = "Happy reading. Come back anytime."
"""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write(toml_content)
        f.flush()
        path = Path(f.name)

    try:
        tree = load_dialogue(path)
        return tree
    finally:
        path.unlink()


@pytest.fixture
def librarian_mob(test_zone, librarian_tree):
    """Create a librarian NPC with dialogue."""
    mob = Mob(name="librarian", x=10, y=10, npc_name="librarian")
    mob.location = test_zone
    test_zone._contents.append(mob)
    return mob


# Conversation state management tests


@pytest.mark.asyncio
async def test_start_conversation_creates_state(player, librarian_mob, librarian_tree):
    """start_conversation creates ConversationState and transitions mob."""
    from mudlib.conversation import active_conversations

    node = start_conversation(player, librarian_mob, librarian_tree)

    assert node.id == "greeting"
    assert player.name in active_conversations
    conv = active_conversations[player.name]
    assert conv.tree == librarian_tree
    assert conv.current_node == "greeting"
    assert conv.npc == librarian_mob
    assert librarian_mob.behavior_state == "converse"


@pytest.mark.asyncio
async def test_advance_conversation_follows_choice(
    player, librarian_mob, librarian_tree
):
    """advance_conversation moves to next node based on choice."""

    start_conversation(player, librarian_mob, librarian_tree)

    # Choose option 1: "I'm looking for a book."
    node = advance_conversation(player, 1)

    assert node is not None
    assert node.id == "books"
    assert node.text == "We have many books. What genre interests you?"


@pytest.mark.asyncio
async def test_advance_conversation_terminal_node_ends(
    player, librarian_mob, librarian_tree
):
    """advance_conversation returns node but doesn't auto-end conversation."""
    from mudlib.conversation import active_conversations

    start_conversation(player, librarian_mob, librarian_tree)

    # Choose "Goodbye." (option 3)
    node = advance_conversation(player, 3)

    # Should return the terminal node
    assert node is not None
    assert len(node.choices) == 0

    # Conversation state is updated but still active
    # (command layer is responsible for ending it)
    assert player.name in active_conversations


@pytest.mark.asyncio
async def test_end_conversation_cleans_up(player, librarian_mob, librarian_tree):
    """end_conversation removes state and transitions mob back."""
    from mudlib.conversation import active_conversations
    from mudlib.npc_behavior import transition_state

    # Set mob to idle first
    transition_state(librarian_mob, "idle")

    start_conversation(player, librarian_mob, librarian_tree)

    end_conversation(player)

    assert player.name not in active_conversations
    assert librarian_mob.behavior_state == "idle"


@pytest.mark.asyncio
async def test_get_conversation_returns_state(player, librarian_mob, librarian_tree):
    """get_conversation returns active conversation or None."""

    assert get_conversation(player) is None

    start_conversation(player, librarian_mob, librarian_tree)

    conv = get_conversation(player)
    assert conv is not None
    assert conv.tree == librarian_tree
    assert conv.npc == librarian_mob


# Talk command tests


@pytest.mark.asyncio
async def test_talk_starts_conversation(player, librarian_mob, librarian_tree):
    """talk <npc> starts conversation and shows root node."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees["librarian"] = librarian_tree

    await cmd_talk(player, "librarian")

    messages = [call[0][0] for call in player.writer.write.call_args_list]
    full_output = "".join(messages)

    assert "Welcome to the library" in full_output
    assert "1." in full_output
    assert "I'm looking for a book" in full_output
    assert "2." in full_output
    assert "Tell me about this place" in full_output
    assert "3." in full_output
    assert "Goodbye" in full_output


@pytest.mark.asyncio
async def test_talk_no_npc_nearby(player):
    """talk with no matching NPC shows error."""

    await cmd_talk(player, "librarian")

    player.writer.write.assert_called_once()
    output = player.writer.write.call_args[0][0]
    assert "don't see" in output.lower() or "not here" in output.lower()


@pytest.mark.asyncio
async def test_talk_non_npc_mob(player, test_zone, mock_reader, mock_writer):
    """talk with non-NPC mob (no npc_name) shows error."""

    # Create a mob without npc_name
    mob = Mob(name="rat", x=10, y=10)
    mob.location = test_zone
    test_zone._contents.append(mob)

    await cmd_talk(player, "rat")

    output = "".join([call[0][0] for call in player.writer.write.call_args_list])
    assert "can't talk" in output.lower() or "no dialogue" in output.lower()


@pytest.mark.asyncio
async def test_talk_no_dialogue_tree(player, test_zone):
    """talk with NPC that has no dialogue tree shows error."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees.clear()

    mob = Mob(name="guard", x=10, y=10, npc_name="guard")
    mob.location = test_zone
    test_zone._contents.append(mob)

    await cmd_talk(player, "guard")

    output = "".join([call[0][0] for call in player.writer.write.call_args_list])
    assert "nothing to say" in output.lower() or "don't have anything" in output.lower()


@pytest.mark.asyncio
async def test_talk_already_in_conversation_shows_current(
    player, librarian_mob, librarian_tree
):
    """talk when already in conversation shows current node."""
    from mudlib.commands.talk import dialogue_trees
    from mudlib.conversation import advance_conversation, start_conversation

    dialogue_trees["librarian"] = librarian_tree

    # Start conversation
    start_conversation(player, librarian_mob, librarian_tree)

    # Advance to a different node
    advance_conversation(player, 1)  # books node

    # Clear writer calls
    player.writer.write.reset_mock()

    # Talk again
    await cmd_talk(player, "librarian")

    output = "".join([call[0][0] for call in player.writer.write.call_args_list])
    assert "We have many books" in output


# Reply command tests


@pytest.mark.asyncio
async def test_reply_advances_conversation(player, librarian_mob, librarian_tree):
    """reply <number> advances to next node."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees["librarian"] = librarian_tree

    # Start conversation
    await cmd_talk(player, "librarian")

    player.writer.write.reset_mock()

    # Reply with choice 1
    await cmd_reply(player, "1")

    output = "".join([call[0][0] for call in player.writer.write.call_args_list])
    assert "We have many books" in output
    assert "1." in output
    assert "History" in output


@pytest.mark.asyncio
async def test_reply_terminal_node_ends_conversation(
    player, librarian_mob, librarian_tree
):
    """reply that reaches terminal node ends conversation."""
    from mudlib.commands.talk import dialogue_trees
    from mudlib.conversation import active_conversations

    dialogue_trees["librarian"] = librarian_tree

    # Start conversation
    await cmd_talk(player, "librarian")

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

    # Choose "Goodbye." (option 3)
    await cmd_reply(player, "3")

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

    # No numbered choices should be shown for terminal node
    lines = output.split("\n")
    choice_lines = [line for line in lines if line.strip().startswith("1.")]
    assert len(choice_lines) == 0

    # Conversation should be ended
    assert player.name not in active_conversations


@pytest.mark.asyncio
async def test_reply_no_conversation(player):
    """reply when not in conversation shows error."""

    await cmd_reply(player, "1")

    output = player.writer.write.call_args[0][0]
    assert (
        "not in a conversation" in output.lower() or "no conversation" in output.lower()
    )


@pytest.mark.asyncio
async def test_reply_invalid_choice_number(player, librarian_mob, librarian_tree):
    """reply with invalid choice number shows error."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees["librarian"] = librarian_tree

    await cmd_talk(player, "librarian")

    player.writer.write.reset_mock()

    # Reply with out-of-range choice
    await cmd_reply(player, "99")

    output = player.writer.write.call_args[0][0]
    assert "invalid" in output.lower() or "choose" in output.lower()


@pytest.mark.asyncio
async def test_reply_non_numeric(player, librarian_mob, librarian_tree):
    """reply with non-numeric argument shows error."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees["librarian"] = librarian_tree

    await cmd_talk(player, "librarian")

    player.writer.write.reset_mock()

    # Reply with non-number
    await cmd_reply(player, "abc")

    output = player.writer.write.call_args[0][0]
    assert "number" in output.lower() or "invalid" in output.lower()


# Display format tests


@pytest.mark.asyncio
async def test_dialogue_display_format(player, librarian_mob, librarian_tree):
    """Dialogue is displayed with correct format."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees["librarian"] = librarian_tree

    await cmd_talk(player, "librarian")

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

    # Check for NPC name and "says"
    assert "librarian says" in output.lower()

    # Check for numbered list
    assert "1." in output
    assert "2." in output
    assert "3." in output


@pytest.mark.asyncio
async def test_terminal_node_no_choices(player, librarian_mob, librarian_tree):
    """Terminal nodes show text without choices."""
    from mudlib.commands.talk import dialogue_trees

    dialogue_trees["librarian"] = librarian_tree

    await cmd_talk(player, "librarian")

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

    # Advance to farewell (terminal node)
    await cmd_reply(player, "3")

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

    assert "Happy reading" in output
    # Should not have numbered choices
    lines = output.split("\n")
    numbered = [
        line
        for line in lines
        if line.strip() and line.strip()[0].isdigit() and "." in line.split()[0]
    ]
    assert len(numbered) == 0
