"""Tests for content loading from TOML files."""

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

import pytest

from mudlib.content import load_command, load_commands
from mudlib.player import Player


@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):
    return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer)


def test_load_command_with_handler_reference():
    """Test loading a command with a Python handler reference."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "shout"
aliases = ["yell"]
help = "shout a message to nearby players"
mode = "normal"
handler = "mudlib.commands.look:cmd_look"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        defn = load_command(tmp_path)
        assert defn.name == "shout"
        assert defn.aliases == ["yell"]
        assert defn.help == "shout a message to nearby players"
        assert defn.mode == "normal"
        assert callable(defn.handler)
    finally:
        tmp_path.unlink()


def test_load_command_with_message():
    """Test loading a command that just sends a message."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "rules"
aliases = ["info"]
help = "display the server rules"
mode = "*"
message = \"\"\"
Welcome to the MUD!
1. Be respectful
2. No cheating
\"\"\"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        defn = load_command(tmp_path)
        assert defn.name == "rules"
        assert defn.aliases == ["info"]
        assert defn.help == "display the server rules"
        assert defn.mode == "*"
        assert callable(defn.handler)
    finally:
        tmp_path.unlink()


@pytest.mark.asyncio
async def test_message_command_sends_text(player, mock_writer):
    """Test that a message command actually sends the text to the player."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "motd"
message = "Welcome to the server!\\nEnjoy your stay."
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        defn = load_command(tmp_path)
        await defn.handler(player, "")

        assert mock_writer.write.called
        written_text = mock_writer.write.call_args[0][0]
        assert "Welcome to the server!" in written_text
        assert "Enjoy your stay." in written_text
    finally:
        tmp_path.unlink()


def test_load_commands_from_directory():
    """Test loading multiple command definitions from a directory."""
    with tempfile.TemporaryDirectory() as tmpdir:
        tmp_path = Path(tmpdir)

        # Create two command files
        (tmp_path / "cmd1.toml").write_text("""
name = "cmd1"
message = "Command 1"
""")
        (tmp_path / "cmd2.toml").write_text("""
name = "cmd2"
message = "Command 2"
""")

        # Create a non-toml file that should be ignored
        (tmp_path / "readme.txt").write_text("This is not a command")

        commands = load_commands(tmp_path)

        assert len(commands) == 2
        names = {cmd.name for cmd in commands}
        assert names == {"cmd1", "cmd2"}


def test_load_command_missing_name():
    """Test that missing name field raises an error."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
message = "Hello"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        with pytest.raises(KeyError):
            load_command(tmp_path)
    finally:
        tmp_path.unlink()


def test_load_command_missing_handler_and_message():
    """Test that a command without handler or message raises an error."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "broken"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        match_msg = "must specify either 'handler' or 'message'"
        with pytest.raises(ValueError, match=match_msg):
            load_command(tmp_path)
    finally:
        tmp_path.unlink()


def test_load_command_bad_handler_reference():
    """Test that an invalid handler reference raises an error."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "broken"
handler = "nonexistent.module:function"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        with pytest.raises((ImportError, ModuleNotFoundError)):
            load_command(tmp_path)
    finally:
        tmp_path.unlink()


def test_load_command_defaults():
    """Test that optional fields have sensible defaults."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "simple"
message = "Hello"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        defn = load_command(tmp_path)
        assert defn.name == "simple"
        assert defn.aliases == []
        assert defn.help == ""
        assert defn.mode == "normal"
    finally:
        tmp_path.unlink()


def test_load_command_both_handler_and_message():
    """Test that specifying both handler and message raises an error."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "broken"
handler = "mudlib.commands.look:cmd_look"
message = "Hello"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        match_msg = "cannot specify both 'handler' and 'message'"
        with pytest.raises(ValueError, match=match_msg):
            load_command(tmp_path)
    finally:
        tmp_path.unlink()


def test_load_command_handler_wrong_type():
    """Test that non-string handler field raises TypeError."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "broken"
handler = 123
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        match_msg = "'handler' must be a string"
        with pytest.raises(TypeError, match=match_msg):
            load_command(tmp_path)
    finally:
        tmp_path.unlink()


def test_load_command_message_wrong_type():
    """Test that non-string message field raises TypeError."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "broken"
message = 456
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        match_msg = "'message' must be a string"
        with pytest.raises(TypeError, match=match_msg):
            load_command(tmp_path)
    finally:
        tmp_path.unlink()


@pytest.mark.asyncio
async def test_message_whitespace_handling(player, mock_writer):
    """Test message stripping removes outer whitespace, keeps internal formatting."""
    with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write("""
name = "formatted"
message = \"\"\"

First line
  Indented line
Last line

\"\"\"
""")
        f.flush()
        tmp_path = Path(f.name)

    try:
        defn = load_command(tmp_path)
        await defn.handler(player, "")

        assert mock_writer.write.called
        written_text = mock_writer.write.call_args[0][0]
        # Outer whitespace should be stripped
        assert not written_text.startswith("\n")
        # But internal formatting should be preserved
        assert "First line\n  Indented line\nLast line" in written_text
    finally:
        tmp_path.unlink()


def test_load_commands_logs_broken_files(caplog):
    """Test that load_commands logs warnings for broken TOML files."""
    with tempfile.TemporaryDirectory() as tmpdir:
        tmp_path = Path(tmpdir)

        # Create a valid command file
        (tmp_path / "valid.toml").write_text("""
name = "valid"
message = "OK"
""")

        # Create a broken command file (missing required field)
        (tmp_path / "broken.toml").write_text("""
name = "broken"
""")

        with caplog.at_level(logging.WARNING):
            commands = load_commands(tmp_path)

        # Should load the valid one only
        assert len(commands) == 1
        assert commands[0].name == "valid"

        # Should log warning about the broken file
        warnings = [r.message for r in caplog.records]
        assert any("failed to load command from" in msg for msg in warnings)
        assert any("broken.toml" in msg for msg in warnings)
