import asyncio
from unittest.mock import AsyncMock, patch

from services.rpg_plot import (
    prune_beats_for_done_quests,
    process_arc_beats,
    should_advance_arc_keywords,
    pop_matching_beats,
    dice_outcome_to_beat_trigger,
    active_quest_titles_to_close,
    can_fire_beat,
    record_beat_fired,
    complete_quest_for_fired_beat,
)


def test_prune_removes_beat_when_quest_done():
    arc = {
        "beats": [
            {"id": "b2", "title": "Highway Howl", "trigger": "event_driven:travel", "choices": []},
        ]
    }
    quests = [{"title": "Highway Howl", "status": "done"}]
    arc, removed = prune_beats_for_done_quests(arc, quests)
    assert removed[0]["title"] == "Highway Howl"
    assert arc["beats"] == []


def test_stuck_recovery_fires_when_no_active_quests_and_cooldown_ok():
    arc = {
        "beats": [
            {"id": "b3", "title": "New Beat", "trigger": "event_driven:rest", "choices": [{"id": "a", "label": "A"}]},
        ],
        "meta": {"last_beat_fired_at_user_turn": 1},
    }
    quests = [{"title": "Old", "status": "done"}]

    async def run():
        return await process_arc_beats(arc, quests, "hello", user_turn=5)

    arc2, fired, pruned, mode, extras = asyncio.run(run())
    assert mode == "stuck_recovery"
    assert fired[0]["title"] == "New Beat"
    assert arc2["beats"] == []
    assert extras.get("cooldown_skipped") is False


def test_stuck_recovery_skipped_when_cooldown_active():
    arc = {
        "beats": [
            {"id": "b3", "title": "New Beat", "trigger": "event_driven:rest", "choices": []},
        ],
        "meta": {"last_beat_fired_at_user_turn": 4},
    }
    quests = []

    async def run():
        return await process_arc_beats(arc, quests, "hello", user_turn=5)

    arc2, fired, _, mode, extras = asyncio.run(run())
    assert mode == ""
    assert fired == []
    assert extras.get("cooldown_skipped") is True
    assert len(arc2["beats"]) == 1


def test_stuck_recovery_skipped_after_reconcile_closed_quests():
    arc = {
        "beats": [
            {"id": "b3", "title": "New Beat", "trigger": "event_driven:rest", "choices": []},
        ],
    }
    quests = []

    async def run():
        return await process_arc_beats(
            arc, quests, "hello", user_turn=10, reconcile_closed_count=2
        )

    _, fired, _, mode, _ = asyncio.run(run())
    assert mode == ""
    assert fired == []


def test_dice_outcome_maps_to_after_fail():
    assert dice_outcome_to_beat_trigger("failure") == "event_driven:after_fail"
    assert dice_outcome_to_beat_trigger("critical failure") == "event_driven:after_fail"
    assert dice_outcome_to_beat_trigger("success") == "event_driven:after_success"


def test_after_fail_beat_fires_on_dice_failure_only_with_needs_check():
    arc = {
        "beats": [
            {
                "id": "b_fail",
                "title": "Dust Yourself Off",
                "trigger": "event_driven:after_fail",
                "injection": "The stumble leaves you both shaken.",
                "choices": [{"id": "a", "label": "Try again"}],
            },
        ]
    }

    async def run(needs_check: bool):
        return await process_arc_beats(
            arc, [], "продолжаем", last_dice_outcome="failure", needs_check=needs_check, user_turn=3
        )

    arc_ok, fired, _, mode, _ = asyncio.run(run(True))
    assert mode == "after_dice"
    assert fired[0]["id"] == "b_fail"
    assert arc_ok["beats"] == []

    arc2 = {
        "beats": [
            {
                "id": "b_fail",
                "title": "Dust Yourself Off",
                "trigger": "event_driven:after_fail",
                "choices": [],
            },
        ]
    }

    async def run_no_check():
        with patch(
            "services.rpg_plot.classify_plot_beat",
            new_callable=AsyncMock,
            return_value=None,
        ):
            return await process_arc_beats(
                arc2,
                [],
                "продолжаем",
                last_dice_outcome="failure",
                needs_check=False,
                user_turn=3,
                allow_stuck_recovery=False,
            )

    _, fired2, _, mode2, _ = asyncio.run(run_no_check())
    assert mode2 == ""
    assert fired2 == []


def test_orphan_active_quests_closed_when_not_in_arc():
    arc = {
        "beats": [
            {
                "id": "b_new_3",
                "title": "Defensive Magic",
                "trigger": "event_driven:after_fail",
                "choices": [],
            }
        ]
    }
    quests = [
        {"title": "Floral Whispers", "status": "active"},
        {"title": "Gift from the Glade", "status": "active"},
        {"title": "Defensive Magic", "status": "active"},
        {"title": "Old Done", "status": "done"},
    ]
    closed = active_quest_titles_to_close(arc, quests)
    assert set(closed) == {"Floral Whispers", "Gift from the Glade"}


def test_can_fire_beat_respects_min_gap():
    arc = {"meta": {"last_beat_fired_at_user_turn": 5}}
    assert can_fire_beat(arc, 6, min_gap=2) is False
    assert can_fire_beat(arc, 7, min_gap=2) is True


def test_record_beat_fired_updates_meta():
    arc = {}
    record_beat_fired(arc, {"id": "b1"}, 8)
    assert arc["meta"]["last_beat_fired_at_user_turn"] == 8
    assert arc["meta"]["last_beat_id"] == "b1"


def test_complete_quest_for_fired_beat():
    beat = {"id": "b1", "title": "Pancake Talk", "injection": "..."}

    async def run():
        with patch("services.memory.upsert_quest", new_callable=AsyncMock) as mock:
            await complete_quest_for_fired_beat("sess-1", beat)
            mock.assert_called_once_with("sess-1", "Pancake Talk", "done")

    asyncio.run(run())


def test_keyword_fallback_travel():
    arc = {
        "beats": [
            {"id": "b2", "title": "Highway Howl", "trigger": "event_driven:travel", "choices": []},
        ]
    }
    trig = should_advance_arc_keywords("едем на стадион")
    assert trig == "event_driven:travel"
    arc, fired = pop_matching_beats(arc, trig, max_beats=1)
    assert fired[0]["title"] == "Highway Howl"
