"""Tests for the skill unlock system."""

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

import pytest

from mudlib.combat.moves import CombatMove, UnlockCondition, load_move
from mudlib.combat.unlock import check_unlocks
from mudlib.player import Player


def _mock_writer():
    """Create a mock writer with all required attributes."""
    writer = MagicMock()
    writer.write = MagicMock()
    writer.drain = AsyncMock()
    writer.send_gmcp = MagicMock()
    # Add local_option and remote_option for gmcp_enabled property
    writer.local_option = MagicMock()
    writer.remote_option = MagicMock()
    writer.local_option.enabled = MagicMock(return_value=False)
    writer.remote_option.enabled = MagicMock(return_value=False)
    return writer


def test_unlock_condition_dataclass():
    """UnlockCondition stores type, threshold, and mob_name."""
    condition = UnlockCondition(type="kill_count", threshold=5)
    assert condition.type == "kill_count"
    assert condition.threshold == 5
    assert condition.mob_name == ""


def test_combat_move_with_unlock_condition():
    """CombatMove can have an unlock_condition field."""
    condition = UnlockCondition(type="kill_count", threshold=10)
    move = CombatMove(
        name="roundhouse",
        move_type="attack",
        stamina_cost=20.0,
        timing_window_ms=500,
        unlock_condition=condition,
    )
    assert move.unlock_condition is condition
    assert move.unlock_condition.threshold == 10


def test_combat_move_unlock_condition_defaults_to_none():
    """CombatMove.unlock_condition defaults to None when not provided."""
    move = CombatMove(
        name="punch",
        move_type="attack",
        stamina_cost=10.0,
        timing_window_ms=300,
    )
    assert move.unlock_condition is None


def test_check_unlocks_grants_move_on_kill_threshold():
    """check_unlocks adds move to unlocked_moves when kill threshold met."""
    player = Player(name="test", x=0, y=0)
    player.kills = 5
    player.unlocked_moves = set()

    move = CombatMove(
        name="roundhouse",
        move_type="attack",
        stamina_cost=20.0,
        timing_window_ms=500,
        command="roundhouse",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )
    moves = {"roundhouse": move}

    newly_unlocked = check_unlocks(player, moves)

    assert "roundhouse" in player.unlocked_moves
    assert newly_unlocked == ["roundhouse"]


def test_check_unlocks_grants_move_on_mob_kills_threshold():
    """check_unlocks unlocks move when specific mob kill count met."""
    player = Player(name="test", x=0, y=0)
    player.mob_kills = {"goblin": 3}
    player.unlocked_moves = set()

    move = CombatMove(
        name="goblin_slayer",
        move_type="attack",
        stamina_cost=15.0,
        timing_window_ms=400,
        command="goblin_slayer",
        unlock_condition=UnlockCondition(
            type="mob_kills", mob_name="goblin", threshold=3
        ),
    )
    moves = {"goblin_slayer": move}

    newly_unlocked = check_unlocks(player, moves)

    assert "goblin_slayer" in player.unlocked_moves
    assert newly_unlocked == ["goblin_slayer"]


def test_check_unlocks_does_not_unlock_when_threshold_not_met():
    """check_unlocks does not add move when kill threshold not reached."""
    player = Player(name="test", x=0, y=0)
    player.kills = 2
    player.unlocked_moves = set()

    move = CombatMove(
        name="roundhouse",
        move_type="attack",
        stamina_cost=20.0,
        timing_window_ms=500,
        command="roundhouse",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )
    moves = {"roundhouse": move}

    newly_unlocked = check_unlocks(player, moves)

    assert "roundhouse" not in player.unlocked_moves
    assert newly_unlocked == []


def test_check_unlocks_returns_empty_list_if_nothing_new():
    """check_unlocks returns empty list when no new unlocks."""
    player = Player(name="test", x=0, y=0)
    player.kills = 10
    player.unlocked_moves = {"roundhouse"}  # already unlocked

    move = CombatMove(
        name="roundhouse",
        move_type="attack",
        stamina_cost=20.0,
        timing_window_ms=500,
        command="roundhouse",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )
    moves = {"roundhouse": move}

    newly_unlocked = check_unlocks(player, moves)

    assert newly_unlocked == []


def test_load_toml_with_unlock_condition():
    """load_move parses [unlock] section from TOML."""
    toml_content = """
name = "roundhouse"
move_type = "attack"
stamina_cost = 20.0
timing_window_ms = 500

[unlock]
type = "kill_count"
threshold = 5
"""
    with NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write(toml_content)
        temp_path = Path(f.name)

    try:
        moves = load_move(temp_path)
        assert len(moves) == 1
        move = moves[0]
        assert move.unlock_condition is not None
        assert move.unlock_condition.type == "kill_count"
        assert move.unlock_condition.threshold == 5
        assert move.unlock_condition.mob_name == ""
    finally:
        temp_path.unlink()


def test_load_toml_without_unlock_section():
    """load_move sets unlock_condition to None when no [unlock] section."""
    toml_content = """
name = "punch"
move_type = "attack"
stamina_cost = 10.0
timing_window_ms = 300
"""
    with NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write(toml_content)
        temp_path = Path(f.name)

    try:
        moves = load_move(temp_path)
        assert len(moves) == 1
        move = moves[0]
        assert move.unlock_condition is None
    finally:
        temp_path.unlink()


def test_load_toml_with_mob_kills_unlock():
    """load_move parses mob_kills unlock condition with mob_name."""
    toml_content = """
name = "goblin_slayer"
move_type = "attack"
stamina_cost = 15.0
timing_window_ms = 400

[unlock]
type = "mob_kills"
mob_name = "goblin"
threshold = 3
"""
    with NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
        f.write(toml_content)
        temp_path = Path(f.name)

    try:
        moves = load_move(temp_path)
        assert len(moves) == 1
        move = moves[0]
        assert move.unlock_condition is not None
        assert move.unlock_condition.type == "mob_kills"
        assert move.unlock_condition.mob_name == "goblin"
        assert move.unlock_condition.threshold == 3
    finally:
        temp_path.unlink()


@pytest.mark.asyncio
async def test_gating_locked_move_rejected_in_do_attack():
    """Locked move with unlock_condition rejects player without unlock."""
    from mudlib.combat.commands import do_attack

    writer = _mock_writer()
    player = Player(name="test", x=0, y=0, writer=writer)
    player.unlocked_moves = set()  # no unlocks

    move = CombatMove(
        name="roundhouse",
        move_type="attack",
        stamina_cost=20.0,
        timing_window_ms=500,
        command="roundhouse",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )

    await do_attack(player, "", move)

    # Check that the rejection message was sent
    writer.write.assert_called()
    calls = [str(call) for call in writer.write.call_args_list]
    assert any("You haven't learned that yet." in str(call) for call in calls)


@pytest.mark.asyncio
async def test_gating_unlocked_move_works_normally():
    """Unlocked move executes when player has it in unlocked_moves."""
    from mudlib.combat.commands import do_attack

    writer = _mock_writer()
    player = Player(name="test", x=0, y=0, writer=writer)
    player.unlocked_moves = {"roundhouse"}  # unlocked
    player.stamina = 100.0

    move = CombatMove(
        name="roundhouse",
        move_type="attack",
        stamina_cost=20.0,
        timing_window_ms=500,
        command="roundhouse",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )

    # Try to attack without target (will fail to find target, but won't
    # reject for unlock)
    await do_attack(player, "", move)

    # Should NOT reject for unlock (gating check passes)
    calls = [str(call) for call in writer.write.call_args_list]
    assert not any("You haven't learned that yet." in str(call) for call in calls)
    # Will get "need a target" message instead
    assert any("You need a target" in str(call) for call in calls)


@pytest.mark.asyncio
async def test_gating_move_without_unlock_condition_always_works():
    """Moves without unlock_condition work without gating check."""
    from mudlib.combat.commands import do_attack

    writer = _mock_writer()
    player = Player(name="test", x=0, y=0, writer=writer)
    player.unlocked_moves = set()  # empty, but move has no unlock condition
    player.stamina = 100.0

    move = CombatMove(
        name="punch",
        move_type="attack",
        stamina_cost=10.0,
        timing_window_ms=300,
        command="punch",
        unlock_condition=None,  # no unlock needed
    )

    # Try to attack without target (will fail to find target, but won't
    # reject for unlock)
    await do_attack(player, "", move)

    # Should work normally (no unlock rejection)
    calls = [str(call) for call in writer.write.call_args_list]
    assert not any("You haven't learned that yet." in str(call) for call in calls)
    # Will get "need a target" message instead
    assert any("You need a target" in str(call) for call in calls)


def test_check_unlocks_deduplicates_variants():
    """check_unlocks only checks base command once for variant moves."""
    player = Player(name="test", x=0, y=0)
    player.kills = 5
    player.unlocked_moves = set()

    # Two variants of "punch" with same unlock condition
    punch_left = CombatMove(
        name="punch left",
        move_type="attack",
        stamina_cost=10.0,
        timing_window_ms=300,
        command="punch",
        variant="left",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )
    punch_right = CombatMove(
        name="punch right",
        move_type="attack",
        stamina_cost=10.0,
        timing_window_ms=300,
        command="punch",
        variant="right",
        unlock_condition=UnlockCondition(type="kill_count", threshold=5),
    )
    moves = {"punch left": punch_left, "punch right": punch_right}

    newly_unlocked = check_unlocks(player, moves)

    # Should only unlock "punch" once, not twice
    assert "punch" in player.unlocked_moves
    assert newly_unlocked == ["punch"]


def test_content_combat_moves_unlock_conditions():
    """Combat moves from content/ have expected unlock conditions."""
    from mudlib.combat.moves import load_moves

    content_dir = Path(__file__).parent.parent / "content" / "combat"
    moves = load_moves(content_dir)

    # Roundhouse has kill_count unlock (5 total kills)
    roundhouse = moves.get("roundhouse")
    assert roundhouse is not None
    assert roundhouse.unlock_condition is not None
    assert roundhouse.unlock_condition.type == "kill_count"
    assert roundhouse.unlock_condition.threshold == 5

    # Sweep has mob_kills unlock (3 goblins)
    sweep = moves.get("sweep")
    assert sweep is not None
    assert sweep.unlock_condition is not None
    assert sweep.unlock_condition.type == "mob_kills"
    assert sweep.unlock_condition.mob_name == "goblin"
    assert sweep.unlock_condition.threshold == 3

    # Punch (variants) are starter moves with no unlock
    punch_left = moves.get("punch left")
    punch_right = moves.get("punch right")
    assert punch_left is not None
    assert punch_left.unlock_condition is None
    assert punch_right is not None
    assert punch_right.unlock_condition is None
