"""Unit tests for timmy.quest_system."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

import timmy.quest_system as qs
from timmy.quest_system import (
    QuestDefinition,
    QuestProgress,
    QuestStatus,
    QuestType,
    _get_progress_key,
    _get_target_value,
    _is_on_cooldown,
    check_daily_run_quest,
    check_issue_count_quest,
    check_issue_reduce_quest,
    claim_quest_reward,
    evaluate_quest_progress,
    get_active_quests,
    get_agent_quests_status,
    get_or_create_progress,
    get_quest_definition,
    get_quest_definitions,
    get_quest_leaderboard,
    get_quest_progress,
    load_quest_config,
    reset_quest_progress,
    update_quest_progress,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_quest(
    quest_id: str = "test_quest",
    quest_type: QuestType = QuestType.ISSUE_COUNT,
    reward_tokens: int = 10,
    enabled: bool = True,
    repeatable: bool = False,
    cooldown_hours: int = 0,
    criteria: dict[str, Any] | None = None,
) -> QuestDefinition:
    return QuestDefinition(
        id=quest_id,
        name=f"Quest {quest_id}",
        description="Test quest",
        reward_tokens=reward_tokens,
        quest_type=quest_type,
        enabled=enabled,
        repeatable=repeatable,
        cooldown_hours=cooldown_hours,
        criteria=criteria or {"target_count": 3},
        notification_message="Quest Complete! You earned {tokens} tokens.",
    )


@pytest.fixture(autouse=True)
def clean_state():
    """Reset module-level state before and after each test."""
    reset_quest_progress()
    qs._quest_definitions.clear()
    qs._quest_settings.clear()
    yield
    reset_quest_progress()
    qs._quest_definitions.clear()
    qs._quest_settings.clear()


# ---------------------------------------------------------------------------
# QuestDefinition
# ---------------------------------------------------------------------------


class TestQuestDefinition:
    def test_from_dict_minimal(self):
        data = {"id": "q1"}
        defn = QuestDefinition.from_dict(data)
        assert defn.id == "q1"
        assert defn.name == "Unnamed Quest"
        assert defn.reward_tokens == 0
        assert defn.quest_type == QuestType.CUSTOM
        assert defn.enabled is True
        assert defn.repeatable is False
        assert defn.cooldown_hours == 0

    def test_from_dict_full(self):
        data = {
            "id": "q2",
            "name": "Full Quest",
            "description": "A full quest",
            "reward_tokens": 50,
            "type": "issue_count",
            "enabled": False,
            "repeatable": True,
            "cooldown_hours": 24,
            "criteria": {"target_count": 5},
            "notification_message": "You earned {tokens}!",
        }
        defn = QuestDefinition.from_dict(data)
        assert defn.id == "q2"
        assert defn.name == "Full Quest"
        assert defn.reward_tokens == 50
        assert defn.quest_type == QuestType.ISSUE_COUNT
        assert defn.enabled is False
        assert defn.repeatable is True
        assert defn.cooldown_hours == 24
        assert defn.criteria == {"target_count": 5}
        assert defn.notification_message == "You earned {tokens}!"

    def test_from_dict_invalid_type_raises(self):
        data = {"id": "q3", "type": "not_a_real_type"}
        with pytest.raises(ValueError):
            QuestDefinition.from_dict(data)


# ---------------------------------------------------------------------------
# QuestProgress
# ---------------------------------------------------------------------------


class TestQuestProgress:
    def test_to_dict_roundtrip(self):
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.IN_PROGRESS,
            current_value=2,
            target_value=5,
            started_at="2026-01-01T00:00:00",
            metadata={"key": "val"},
        )
        d = progress.to_dict()
        assert d["quest_id"] == "q1"
        assert d["agent_id"] == "agent_a"
        assert d["status"] == "in_progress"
        assert d["current_value"] == 2
        assert d["target_value"] == 5
        assert d["metadata"] == {"key": "val"}

    def test_to_dict_defaults(self):
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.NOT_STARTED,
        )
        d = progress.to_dict()
        assert d["completion_count"] == 0
        assert d["started_at"] == ""
        assert d["completed_at"] == ""


# ---------------------------------------------------------------------------
# _get_progress_key
# ---------------------------------------------------------------------------


def test_get_progress_key():
    assert _get_progress_key("q1", "agent_a") == "agent_a:q1"


def test_get_progress_key_different_agents():
    key_a = _get_progress_key("q1", "agent_a")
    key_b = _get_progress_key("q1", "agent_b")
    assert key_a != key_b


# ---------------------------------------------------------------------------
# load_quest_config
# ---------------------------------------------------------------------------


class TestLoadQuestConfig:
    def test_missing_file_returns_empty(self, tmp_path):
        missing = tmp_path / "nonexistent.yaml"
        with patch.object(qs, "QUEST_CONFIG_PATH", missing):
            defs, settings = load_quest_config()
        assert defs == {}
        assert settings == {}

    def test_valid_yaml_loads_quests(self, tmp_path):
        config_path = tmp_path / "quests.yaml"
        config_path.write_text(
            """
quests:
  first_quest:
    name: First Quest
    description: Do stuff
    reward_tokens: 25
    type: issue_count
    enabled: true
    repeatable: false
    cooldown_hours: 0
    criteria:
      target_count: 3
    notification_message: "Done! {tokens} tokens"
settings:
  some_setting: true
"""
        )
        with patch.object(qs, "QUEST_CONFIG_PATH", config_path):
            defs, settings = load_quest_config()

        assert "first_quest" in defs
        assert defs["first_quest"].name == "First Quest"
        assert defs["first_quest"].reward_tokens == 25
        assert settings == {"some_setting": True}

    def test_invalid_yaml_returns_empty(self, tmp_path):
        config_path = tmp_path / "quests.yaml"
        config_path.write_text(":: not valid yaml ::")
        with patch.object(qs, "QUEST_CONFIG_PATH", config_path):
            defs, settings = load_quest_config()
        assert defs == {}
        assert settings == {}

    def test_non_dict_yaml_returns_empty(self, tmp_path):
        config_path = tmp_path / "quests.yaml"
        config_path.write_text("- item1\n- item2\n")
        with patch.object(qs, "QUEST_CONFIG_PATH", config_path):
            defs, settings = load_quest_config()
        assert defs == {}
        assert settings == {}

    def test_bad_quest_entry_is_skipped(self, tmp_path):
        config_path = tmp_path / "quests.yaml"
        config_path.write_text(
            """
quests:
  good_quest:
    name: Good
    type: issue_count
    reward_tokens: 10
    enabled: true
    repeatable: false
    cooldown_hours: 0
    criteria: {}
    notification_message: "{tokens}"
  bad_quest:
    type: invalid_type_that_does_not_exist
"""
        )
        with patch.object(qs, "QUEST_CONFIG_PATH", config_path):
            defs, _ = load_quest_config()
        assert "good_quest" in defs
        assert "bad_quest" not in defs


# ---------------------------------------------------------------------------
# get_quest_definitions / get_quest_definition / get_active_quests
# ---------------------------------------------------------------------------


class TestQuestLookup:
    def setup_method(self):
        q1 = _make_quest("q1", enabled=True)
        q2 = _make_quest("q2", enabled=False)
        qs._quest_definitions.update({"q1": q1, "q2": q2})

    def test_get_quest_definitions_returns_all(self):
        defs = get_quest_definitions()
        assert "q1" in defs
        assert "q2" in defs

    def test_get_quest_definition_found(self):
        defn = get_quest_definition("q1")
        assert defn is not None
        assert defn.id == "q1"

    def test_get_quest_definition_not_found(self):
        assert get_quest_definition("missing") is None

    def test_get_active_quests_only_enabled(self):
        active = get_active_quests()
        ids = [q.id for q in active]
        assert "q1" in ids
        assert "q2" not in ids


# ---------------------------------------------------------------------------
# _get_target_value
# ---------------------------------------------------------------------------


class TestGetTargetValue:
    def test_issue_count(self):
        q = _make_quest(quest_type=QuestType.ISSUE_COUNT, criteria={"target_count": 7})
        assert _get_target_value(q) == 7

    def test_issue_reduce(self):
        q = _make_quest(quest_type=QuestType.ISSUE_REDUCE, criteria={"target_reduction": 5})
        assert _get_target_value(q) == 5

    def test_daily_run(self):
        q = _make_quest(quest_type=QuestType.DAILY_RUN, criteria={"min_sessions": 3})
        assert _get_target_value(q) == 3

    def test_docs_update(self):
        q = _make_quest(quest_type=QuestType.DOCS_UPDATE, criteria={"min_files_changed": 2})
        assert _get_target_value(q) == 2

    def test_test_improve(self):
        q = _make_quest(quest_type=QuestType.TEST_IMPROVE, criteria={"min_new_tests": 4})
        assert _get_target_value(q) == 4

    def test_custom_defaults_to_one(self):
        q = _make_quest(quest_type=QuestType.CUSTOM, criteria={})
        assert _get_target_value(q) == 1

    def test_missing_criteria_key_defaults_to_one(self):
        q = _make_quest(quest_type=QuestType.ISSUE_COUNT, criteria={})
        assert _get_target_value(q) == 1


# ---------------------------------------------------------------------------
# get_or_create_progress / get_quest_progress
# ---------------------------------------------------------------------------


class TestProgressCreation:
    def setup_method(self):
        qs._quest_definitions["q1"] = _make_quest("q1", criteria={"target_count": 5})

    def test_creates_new_progress(self):
        progress = get_or_create_progress("q1", "agent_a")
        assert progress.quest_id == "q1"
        assert progress.agent_id == "agent_a"
        assert progress.status == QuestStatus.NOT_STARTED
        assert progress.target_value == 5
        assert progress.current_value == 0

    def test_returns_existing_progress(self):
        p1 = get_or_create_progress("q1", "agent_a")
        p1.current_value = 3
        p2 = get_or_create_progress("q1", "agent_a")
        assert p2.current_value == 3
        assert p1 is p2

    def test_raises_for_unknown_quest(self):
        with pytest.raises(ValueError, match="Quest unknown not found"):
            get_or_create_progress("unknown", "agent_a")

    def test_get_quest_progress_none_before_creation(self):
        assert get_quest_progress("q1", "agent_a") is None

    def test_get_quest_progress_after_creation(self):
        get_or_create_progress("q1", "agent_a")
        progress = get_quest_progress("q1", "agent_a")
        assert progress is not None


# ---------------------------------------------------------------------------
# update_quest_progress
# ---------------------------------------------------------------------------


class TestUpdateQuestProgress:
    def setup_method(self):
        qs._quest_definitions["q1"] = _make_quest("q1", criteria={"target_count": 3})

    def test_updates_current_value(self):
        progress = update_quest_progress("q1", "agent_a", 2)
        assert progress.current_value == 2
        assert progress.status == QuestStatus.NOT_STARTED

    def test_marks_completed_when_target_reached(self):
        progress = update_quest_progress("q1", "agent_a", 3)
        assert progress.status == QuestStatus.COMPLETED
        assert progress.completed_at != ""

    def test_marks_completed_when_value_exceeds_target(self):
        progress = update_quest_progress("q1", "agent_a", 10)
        assert progress.status == QuestStatus.COMPLETED

    def test_does_not_re_complete_already_completed(self):
        p = update_quest_progress("q1", "agent_a", 3)
        first_completed_at = p.completed_at
        p2 = update_quest_progress("q1", "agent_a", 5)
        # should not change completed_at again
        assert p2.completed_at == first_completed_at

    def test_does_not_re_complete_claimed_quest(self):
        p = update_quest_progress("q1", "agent_a", 3)
        p.status = QuestStatus.CLAIMED
        p2 = update_quest_progress("q1", "agent_a", 5)
        assert p2.status == QuestStatus.CLAIMED

    def test_updates_metadata(self):
        progress = update_quest_progress("q1", "agent_a", 1, metadata={"info": "value"})
        assert progress.metadata["info"] == "value"

    def test_merges_metadata(self):
        update_quest_progress("q1", "agent_a", 1, metadata={"a": 1})
        progress = update_quest_progress("q1", "agent_a", 2, metadata={"b": 2})
        assert progress.metadata["a"] == 1
        assert progress.metadata["b"] == 2


# ---------------------------------------------------------------------------
# _is_on_cooldown
# ---------------------------------------------------------------------------


class TestIsOnCooldown:
    def test_non_repeatable_never_on_cooldown(self):
        quest = _make_quest(repeatable=False, cooldown_hours=24)
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.CLAIMED,
            last_completed_at=datetime.now(UTC).isoformat(),
        )
        assert _is_on_cooldown(progress, quest) is False

    def test_no_last_completed_not_on_cooldown(self):
        quest = _make_quest(repeatable=True, cooldown_hours=24)
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.NOT_STARTED,
            last_completed_at="",
        )
        assert _is_on_cooldown(progress, quest) is False

    def test_zero_cooldown_not_on_cooldown(self):
        quest = _make_quest(repeatable=True, cooldown_hours=0)
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.CLAIMED,
            last_completed_at=datetime.now(UTC).isoformat(),
        )
        assert _is_on_cooldown(progress, quest) is False

    def test_recent_completion_is_on_cooldown(self):
        quest = _make_quest(repeatable=True, cooldown_hours=24)
        recent = datetime.now(UTC) - timedelta(hours=1)
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.NOT_STARTED,
            last_completed_at=recent.isoformat(),
        )
        assert _is_on_cooldown(progress, quest) is True

    def test_expired_cooldown_not_on_cooldown(self):
        quest = _make_quest(repeatable=True, cooldown_hours=24)
        old = datetime.now(UTC) - timedelta(hours=25)
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.NOT_STARTED,
            last_completed_at=old.isoformat(),
        )
        assert _is_on_cooldown(progress, quest) is False

    def test_invalid_last_completed_returns_false(self):
        quest = _make_quest(repeatable=True, cooldown_hours=24)
        progress = QuestProgress(
            quest_id="q1",
            agent_id="agent_a",
            status=QuestStatus.NOT_STARTED,
            last_completed_at="not-a-date",
        )
        assert _is_on_cooldown(progress, quest) is False


# ---------------------------------------------------------------------------
# claim_quest_reward
# ---------------------------------------------------------------------------


class TestClaimQuestReward:
    def setup_method(self):
        qs._quest_definitions["q1"] = _make_quest("q1", reward_tokens=25)

    def test_returns_none_if_no_progress(self):
        assert claim_quest_reward("q1", "agent_a") is None

    def test_returns_none_if_not_completed(self):
        get_or_create_progress("q1", "agent_a")
        assert claim_quest_reward("q1", "agent_a") is None

    def test_returns_none_if_quest_not_found(self):
        assert claim_quest_reward("nonexistent", "agent_a") is None

    def test_successful_claim(self):
        progress = get_or_create_progress("q1", "agent_a")
        progress.status = QuestStatus.COMPLETED
        progress.completed_at = datetime.now(UTC).isoformat()

        mock_invoice = MagicMock()
        mock_invoice.payment_hash = "quest_q1_agent_a_123"

        with (
            patch("timmy.quest_system.create_invoice_entry", return_value=mock_invoice),
            patch("timmy.quest_system.mark_settled"),
        ):
            result = claim_quest_reward("q1", "agent_a")

        assert result is not None
        assert result["tokens_awarded"] == 25
        assert result["quest_id"] == "q1"
        assert result["agent_id"] == "agent_a"
        assert result["completion_count"] == 1

    def test_successful_claim_marks_claimed(self):
        progress = get_or_create_progress("q1", "agent_a")
        progress.status = QuestStatus.COMPLETED
        progress.completed_at = datetime.now(UTC).isoformat()

        mock_invoice = MagicMock()
        mock_invoice.payment_hash = "phash"

        with (
            patch("timmy.quest_system.create_invoice_entry", return_value=mock_invoice),
            patch("timmy.quest_system.mark_settled"),
        ):
            claim_quest_reward("q1", "agent_a")

        assert progress.status == QuestStatus.CLAIMED

    def test_repeatable_quest_resets_after_claim(self):
        qs._quest_definitions["rep"] = _make_quest(
            "rep", repeatable=True, cooldown_hours=0, reward_tokens=10
        )
        progress = get_or_create_progress("rep", "agent_a")
        progress.status = QuestStatus.COMPLETED
        progress.completed_at = datetime.now(UTC).isoformat()
        progress.current_value = 5

        mock_invoice = MagicMock()
        mock_invoice.payment_hash = "phash"

        with (
            patch("timmy.quest_system.create_invoice_entry", return_value=mock_invoice),
            patch("timmy.quest_system.mark_settled"),
        ):
            result = claim_quest_reward("rep", "agent_a")

        assert result is not None
        assert progress.status == QuestStatus.NOT_STARTED
        assert progress.current_value == 0
        assert progress.completed_at == ""

    def test_on_cooldown_returns_none(self):
        qs._quest_definitions["rep"] = _make_quest("rep", repeatable=True, cooldown_hours=24)
        progress = get_or_create_progress("rep", "agent_a")
        progress.status = QuestStatus.COMPLETED
        recent = datetime.now(UTC) - timedelta(hours=1)
        progress.last_completed_at = recent.isoformat()

        assert claim_quest_reward("rep", "agent_a") is None

    def test_ledger_error_returns_none(self):
        progress = get_or_create_progress("q1", "agent_a")
        progress.status = QuestStatus.COMPLETED
        progress.completed_at = datetime.now(UTC).isoformat()

        with patch(
            "timmy.quest_system.create_invoice_entry", side_effect=Exception("ledger error")
        ):
            result = claim_quest_reward("q1", "agent_a")

        assert result is None


# ---------------------------------------------------------------------------
# check_issue_count_quest
# ---------------------------------------------------------------------------


class TestCheckIssueCountQuest:
    def setup_method(self):
        qs._quest_definitions["iq"] = _make_quest(
            "iq",
            quest_type=QuestType.ISSUE_COUNT,
            criteria={"target_count": 2, "issue_labels": ["bug"]},
        )

    def test_counts_matching_issues(self):
        issues = [
            {"labels": [{"name": "bug"}]},
            {"labels": [{"name": "bug"}, {"name": "priority"}]},
            {"labels": [{"name": "feature"}]},  # doesn't match
        ]
        progress = check_issue_count_quest(qs._quest_definitions["iq"], "agent_a", issues)
        assert progress.current_value == 2
        assert progress.status == QuestStatus.COMPLETED

    def test_empty_issues_returns_zero(self):
        progress = check_issue_count_quest(qs._quest_definitions["iq"], "agent_a", [])
        assert progress.current_value == 0

    def test_no_labels_filter_counts_all_labeled(self):
        q = _make_quest(
            "nolabel",
            quest_type=QuestType.ISSUE_COUNT,
            criteria={"target_count": 1, "issue_labels": []},
        )
        qs._quest_definitions["nolabel"] = q
        issues = [
            {"labels": [{"name": "bug"}]},
            {"labels": [{"name": "feature"}]},
        ]
        progress = check_issue_count_quest(q, "agent_a", issues)
        assert progress.current_value == 2


# ---------------------------------------------------------------------------
# check_issue_reduce_quest
# ---------------------------------------------------------------------------


class TestCheckIssueReduceQuest:
    def setup_method(self):
        qs._quest_definitions["ir"] = _make_quest(
            "ir", quest_type=QuestType.ISSUE_REDUCE, criteria={"target_reduction": 5}
        )

    def test_computes_reduction(self):
        progress = check_issue_reduce_quest(qs._quest_definitions["ir"], "agent_a", 20, 15)
        assert progress.current_value == 5
        assert progress.status == QuestStatus.COMPLETED

    def test_negative_reduction_treated_as_zero(self):
        progress = check_issue_reduce_quest(qs._quest_definitions["ir"], "agent_a", 10, 15)
        assert progress.current_value == 0

    def test_no_change_yields_zero(self):
        progress = check_issue_reduce_quest(qs._quest_definitions["ir"], "agent_a", 10, 10)
        assert progress.current_value == 0


# ---------------------------------------------------------------------------
# check_daily_run_quest
# ---------------------------------------------------------------------------


class TestCheckDailyRunQuest:
    def setup_method(self):
        qs._quest_definitions["dr"] = _make_quest(
            "dr", quest_type=QuestType.DAILY_RUN, criteria={"min_sessions": 2}
        )

    def test_tracks_sessions(self):
        progress = check_daily_run_quest(qs._quest_definitions["dr"], "agent_a", 2)
        assert progress.current_value == 2
        assert progress.status == QuestStatus.COMPLETED

    def test_incomplete_sessions(self):
        progress = check_daily_run_quest(qs._quest_definitions["dr"], "agent_a", 1)
        assert progress.current_value == 1
        assert progress.status != QuestStatus.COMPLETED


# ---------------------------------------------------------------------------
# evaluate_quest_progress
# ---------------------------------------------------------------------------


class TestEvaluateQuestProgress:
    def setup_method(self):
        qs._quest_definitions["iq"] = _make_quest(
            "iq", quest_type=QuestType.ISSUE_COUNT, criteria={"target_count": 1}
        )
        qs._quest_definitions["dis"] = _make_quest("dis", enabled=False)

    def test_disabled_quest_returns_none(self):
        result = evaluate_quest_progress("dis", "agent_a", {})
        assert result is None

    def test_missing_quest_returns_none(self):
        result = evaluate_quest_progress("nonexistent", "agent_a", {})
        assert result is None

    def test_issue_count_quest_evaluated(self):
        context = {"closed_issues": [{"labels": [{"name": "bug"}]}]}
        result = evaluate_quest_progress("iq", "agent_a", context)
        assert result is not None
        assert result.current_value == 1

    def test_issue_reduce_quest_evaluated(self):
        qs._quest_definitions["ir"] = _make_quest(
            "ir", quest_type=QuestType.ISSUE_REDUCE, criteria={"target_reduction": 3}
        )
        context = {"previous_issue_count": 10, "current_issue_count": 7}
        result = evaluate_quest_progress("ir", "agent_a", context)
        assert result is not None
        assert result.current_value == 3

    def test_daily_run_quest_evaluated(self):
        qs._quest_definitions["dr"] = _make_quest(
            "dr", quest_type=QuestType.DAILY_RUN, criteria={"min_sessions": 1}
        )
        context = {"sessions_completed": 2}
        result = evaluate_quest_progress("dr", "agent_a", context)
        assert result is not None
        assert result.current_value == 2

    def test_custom_quest_returns_existing_progress(self):
        qs._quest_definitions["cust"] = _make_quest("cust", quest_type=QuestType.CUSTOM)
        # No progress yet => None (custom quests don't auto-create progress here)
        result = evaluate_quest_progress("cust", "agent_a", {})
        assert result is None

    def test_cooldown_prevents_evaluation(self):
        q = _make_quest(
            "rep_iq",
            quest_type=QuestType.ISSUE_COUNT,
            repeatable=True,
            cooldown_hours=24,
            criteria={"target_count": 1},
        )
        qs._quest_definitions["rep_iq"] = q
        progress = get_or_create_progress("rep_iq", "agent_a")
        recent = datetime.now(UTC) - timedelta(hours=1)
        progress.last_completed_at = recent.isoformat()

        context = {"closed_issues": [{"labels": [{"name": "bug"}]}]}
        result = evaluate_quest_progress("rep_iq", "agent_a", context)
        # Should return existing progress without updating
        assert result is progress


# ---------------------------------------------------------------------------
# reset_quest_progress
# ---------------------------------------------------------------------------


class TestResetQuestProgress:
    def setup_method(self):
        qs._quest_definitions["q1"] = _make_quest("q1")
        qs._quest_definitions["q2"] = _make_quest("q2")

    def test_reset_all(self):
        get_or_create_progress("q1", "agent_a")
        get_or_create_progress("q2", "agent_a")
        count = reset_quest_progress()
        assert count == 2
        assert get_quest_progress("q1", "agent_a") is None
        assert get_quest_progress("q2", "agent_a") is None

    def test_reset_specific_quest(self):
        get_or_create_progress("q1", "agent_a")
        get_or_create_progress("q2", "agent_a")
        count = reset_quest_progress(quest_id="q1")
        assert count == 1
        assert get_quest_progress("q1", "agent_a") is None
        assert get_quest_progress("q2", "agent_a") is not None

    def test_reset_specific_agent(self):
        get_or_create_progress("q1", "agent_a")
        get_or_create_progress("q1", "agent_b")
        count = reset_quest_progress(agent_id="agent_a")
        assert count == 1
        assert get_quest_progress("q1", "agent_a") is None
        assert get_quest_progress("q1", "agent_b") is not None

    def test_reset_specific_quest_and_agent(self):
        get_or_create_progress("q1", "agent_a")
        get_or_create_progress("q1", "agent_b")
        count = reset_quest_progress(quest_id="q1", agent_id="agent_a")
        assert count == 1

    def test_reset_empty_returns_zero(self):
        count = reset_quest_progress()
        assert count == 0


# ---------------------------------------------------------------------------
# get_quest_leaderboard
# ---------------------------------------------------------------------------


class TestGetQuestLeaderboard:
    def setup_method(self):
        qs._quest_definitions["q1"] = _make_quest("q1", reward_tokens=10)
        qs._quest_definitions["q2"] = _make_quest("q2", reward_tokens=20)

    def test_empty_progress_returns_empty(self):
        assert get_quest_leaderboard() == []

    def test_leaderboard_sorted_by_tokens(self):
        p_a = get_or_create_progress("q1", "agent_a")
        p_a.completion_count = 1
        p_b = get_or_create_progress("q2", "agent_b")
        p_b.completion_count = 2

        board = get_quest_leaderboard()
        assert board[0]["agent_id"] == "agent_b"  # 40 tokens
        assert board[1]["agent_id"] == "agent_a"  # 10 tokens

    def test_leaderboard_aggregates_multiple_quests(self):
        p1 = get_or_create_progress("q1", "agent_a")
        p1.completion_count = 2  # 20 tokens
        p2 = get_or_create_progress("q2", "agent_a")
        p2.completion_count = 1  # 20 tokens

        board = get_quest_leaderboard()
        assert len(board) == 1
        assert board[0]["total_tokens"] == 40
        assert board[0]["total_completions"] == 3

    def test_leaderboard_counts_unique_quests(self):
        p1 = get_or_create_progress("q1", "agent_a")
        p1.completion_count = 2
        p2 = get_or_create_progress("q2", "agent_a")
        p2.completion_count = 1

        board = get_quest_leaderboard()
        assert board[0]["unique_quests_completed"] == 2


# ---------------------------------------------------------------------------
# get_agent_quests_status
# ---------------------------------------------------------------------------


class TestGetAgentQuestsStatus:
    def setup_method(self):
        qs._quest_definitions["q1"] = _make_quest("q1", reward_tokens=10)

    def test_returns_status_structure(self):
        result = get_agent_quests_status("agent_a")
        assert result["agent_id"] == "agent_a"
        assert isinstance(result["quests"], list)
        assert "total_tokens_earned" in result
        assert "total_quests_completed" in result
        assert "active_quests_count" in result

    def test_includes_quest_info(self):
        result = get_agent_quests_status("agent_a")
        quest_info = result["quests"][0]
        assert quest_info["quest_id"] == "q1"
        assert quest_info["reward_tokens"] == 10
        assert quest_info["status"] == QuestStatus.NOT_STARTED.value

    def test_accumulates_tokens_from_completions(self):
        p = get_or_create_progress("q1", "agent_a")
        p.completion_count = 3
        result = get_agent_quests_status("agent_a")
        assert result["total_tokens_earned"] == 30
        assert result["total_quests_completed"] == 3

    def test_cooldown_hours_remaining_calculated(self):
        q = _make_quest("qcool", repeatable=True, cooldown_hours=24, reward_tokens=5)
        qs._quest_definitions["qcool"] = q
        p = get_or_create_progress("qcool", "agent_a")
        recent = datetime.now(UTC) - timedelta(hours=2)
        p.last_completed_at = recent.isoformat()
        p.completion_count = 1

        result = get_agent_quests_status("agent_a")
        qcool_info = next(qi for qi in result["quests"] if qi["quest_id"] == "qcool")
        assert qcool_info["on_cooldown"] is True
        assert qcool_info["cooldown_hours_remaining"] > 0
