"""Tests for builder commands."""

from unittest.mock import AsyncMock, MagicMock

import pytest

from mudlib.player import Player, players
from mudlib.zone import Zone
from mudlib.zones import get_zone, register_zone, zone_registry


@pytest.fixture(autouse=True)
def clear_state():
    players.clear()
    zone_registry.clear()
    yield
    players.clear()
    zone_registry.clear()


@pytest.fixture
def zone():
    terrain = [["." for _ in range(10)] for _ in range(10)]
    z = Zone(name="hub", width=10, height=10, terrain=terrain)
    register_zone("hub", z)
    return z


@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(zone, mock_writer, mock_reader):
    p = Player(
        name="builder",
        x=5,
        y=5,
        writer=mock_writer,
        reader=mock_reader,
        location=zone,
        is_admin=True,
    )
    zone._contents.append(p)
    players["builder"] = p
    return p


# --- @goto ---


@pytest.mark.asyncio
async def test_goto_existing_zone(player):
    """@goto teleports player to named zone's spawn point."""
    from mudlib.commands.build import cmd_goto

    target = Zone(
        name="forest",
        width=5,
        height=5,
        terrain=[["." for _ in range(5)] for _ in range(5)],
        spawn_x=2,
        spawn_y=3,
    )
    register_zone("forest", target)

    await cmd_goto(player, "forest")

    assert player.location is target
    assert player.x == 2
    assert player.y == 3


@pytest.mark.asyncio
async def test_goto_nonexistent_zone(player, mock_writer):
    """@goto with unknown zone name shows error."""
    from mudlib.commands.build import cmd_goto

    await cmd_goto(player, "nowhere")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "not found" in written.lower() or "no zone" in written.lower()


@pytest.mark.asyncio
async def test_goto_no_args(player, mock_writer):
    """@goto without arguments shows usage."""
    from mudlib.commands.build import cmd_goto

    await cmd_goto(player, "")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "usage" in written.lower() or "goto" in written.lower()


# --- @dig ---


@pytest.mark.asyncio
async def test_dig_creates_zone(player):
    """@dig creates a new zone and teleports player there."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "cave 8 6")

    new_zone = get_zone("cave")
    assert new_zone is not None
    assert new_zone.width == 8
    assert new_zone.height == 6
    assert player.location is new_zone
    assert player.x == 0
    assert player.y == 0


@pytest.mark.asyncio
async def test_dig_creates_terrain(player):
    """@dig creates terrain filled with '.' tiles."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "mine 5 4")

    new_zone = get_zone("mine")
    assert new_zone is not None
    assert len(new_zone.terrain) == 4
    assert len(new_zone.terrain[0]) == 5
    assert all(tile == "." for row in new_zone.terrain for tile in row)


@pytest.mark.asyncio
async def test_dig_zone_not_toroidal(player):
    """@dig creates bounded (non-toroidal) zones."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "room 3 3")

    new_zone = get_zone("room")
    assert new_zone is not None
    assert new_zone.toroidal is False


@pytest.mark.asyncio
async def test_dig_existing_name(player, mock_writer):
    """@dig with existing zone name shows error."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "hub 5 5")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "already exists" in written.lower()


@pytest.mark.asyncio
async def test_dig_bad_args(player, mock_writer):
    """@dig with wrong number of args shows usage."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "cave")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "usage" in written.lower() or "dig" in written.lower()


@pytest.mark.asyncio
async def test_dig_non_numeric_dimensions(player, mock_writer):
    """@dig with non-numeric dimensions shows error."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "cave abc def")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "numbers" in written.lower()


@pytest.mark.asyncio
async def test_dig_zero_or_negative_dimensions(player, mock_writer):
    """@dig with zero or negative dimensions shows error."""
    from mudlib.commands.build import cmd_dig

    await cmd_dig(player, "cave 0 5")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "at least 1x1" in written.lower()

    mock_writer.write.reset_mock()
    await cmd_dig(player, "cave 5 -2")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "at least 1x1" in written.lower()


# --- @save ---


@pytest.mark.asyncio
async def test_save_zone(player, zone, tmp_path, mock_writer):
    """@save writes zone to TOML file."""
    from mudlib.commands.build import cmd_save, set_content_dir

    set_content_dir(tmp_path)
    zones_dir = tmp_path / "zones"
    zones_dir.mkdir()

    await cmd_save(player, "")

    # Check file was created
    saved_file = zones_dir / "hub.toml"
    assert saved_file.exists()
    content = saved_file.read_text()
    assert 'name = "hub"' in content

    # Check confirmation message
    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "saved" in written.lower()


# --- @place ---


@pytest.mark.asyncio
async def test_place_thing(player, zone, mock_writer):
    """@place puts a thing at player's position."""
    from mudlib.commands.build import cmd_place
    from mudlib.things import ThingTemplate, thing_templates

    thing_templates["sign"] = ThingTemplate(
        name="sign",
        description="a wooden sign",
    )

    await cmd_place(player, "sign")

    # Check thing is in zone at player's position
    from mudlib.thing import Thing

    things = [
        obj
        for obj in zone.contents_at(5, 5)
        if isinstance(obj, Thing) and obj.name == "sign"
    ]
    assert len(things) == 1
    assert things[0].location is zone

    # Clean up
    del thing_templates["sign"]


@pytest.mark.asyncio
async def test_place_unknown_thing(player, mock_writer):
    """@place with unknown thing name shows error."""
    from mudlib.commands.build import cmd_place

    await cmd_place(player, "unicorn")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "not found" in written.lower() or "unknown" in written.lower()


@pytest.mark.asyncio
async def test_place_no_args(player, mock_writer):
    """@place without arguments shows usage."""
    from mudlib.commands.build import cmd_place

    await cmd_place(player, "")

    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[0][0][0]
    assert "usage" in written.lower() or "place" in written.lower()


# --- Permission checks ---


@pytest.mark.asyncio
async def test_builder_commands_require_admin(zone, mock_writer, mock_reader):
    """Non-admin players cannot use builder commands."""
    from mudlib.commands import dispatch

    non_admin = Player(
        name="player",
        x=5,
        y=5,
        writer=mock_writer,
        reader=mock_reader,
        location=zone,
        is_admin=False,
    )

    await dispatch(non_admin, "@goto hub")
    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "permission" in written.lower()

    mock_writer.write.reset_mock()
    await dispatch(non_admin, "@dig test 5 5")
    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "permission" in written.lower()

    mock_writer.write.reset_mock()
    await dispatch(non_admin, "@save")
    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "permission" in written.lower()

    mock_writer.write.reset_mock()
    await dispatch(non_admin, "@place thing")
    mock_writer.write.assert_called()
    written = mock_writer.write.call_args_list[-1][0][0]
    assert "permission" in written.lower()
