"""
Tests for task_classifier.py — LLM-based task classification with conversation state.
"""

import sys
import time
import types
import pytest

# task_classifier imports `requests` which may not be installed in test env.
# Provide a minimal stub so the module loads.
if "requests" not in sys.modules:
    _stub = types.ModuleType("requests")
    _stub.Timeout = type("Timeout", (Exception,), {})
    _stub.post = lambda *a, **kw: None
    sys.modules["requests"] = _stub

from api.services.task_classifier import (
    TaskType,
    TASK_PROTOCOL_MAP,
    get_protocol_flags,
    classify_task_cached,
    _cache,
    _cache_key,
    _cache_get,
    _cache_put,
    _extract_conversation_state,
    _CACHE_TTL_SECONDS,
)


def _msg(role, content, tool_calls=None):
    class FakeMsg:
        pass
    m = FakeMsg()
    m.role = role
    m.content = content
    m.tool_calls = tool_calls
    return m


# ===========================================================================
# TaskType enum
# ===========================================================================

class TestTaskType:
    def test_all_types_have_protocol_map(self):
        for tt in TaskType:
            assert tt in TASK_PROTOCOL_MAP

    def test_bug_finding_has_bug_review_flag(self):
        flags = get_protocol_flags(TaskType.BUG_FINDING)
        assert flags.get("include_bug_review") is True

    def test_general_has_no_flags(self):
        flags = get_protocol_flags(TaskType.GENERAL)
        assert flags == {}

    def test_deep_refactoring_has_deep_change(self):
        flags = get_protocol_flags(TaskType.DEEP_REFACTORING)
        assert flags.get("include_deep_change") is True

    def test_repo_exploration_has_overview(self):
        flags = get_protocol_flags(TaskType.REPO_EXPLORATION)
        assert flags.get("include_repo_overview") is True


# ===========================================================================
# Cache
# ===========================================================================

class TestCache:
    def setup_method(self):
        _cache.clear()

    def test_cache_put_and_get(self):
        _cache_put("key1", TaskType.BUG_FINDING)
        assert _cache_get("key1") == TaskType.BUG_FINDING

    def test_cache_miss(self):
        assert _cache_get("nonexistent") is None

    def test_cache_expiry(self):
        _cache["key_old"] = (TaskType.GENERAL, time.time() - _CACHE_TTL_SECONDS - 10)
        assert _cache_get("key_old") is None

    def test_cache_eviction_on_overflow(self):
        for i in range(600):
            _cache_put(f"k{i}", TaskType.GENERAL)
        assert len(_cache) <= 500

    def test_cache_key_deterministic(self):
        assert _cache_key("hello world") == _cache_key("hello world")
        assert _cache_key("Hello World") == _cache_key("hello world")

    def test_cache_key_strips_whitespace(self):
        assert _cache_key("  hello  ") == _cache_key("hello")


# ===========================================================================
# Conversation state extraction
# ===========================================================================

class TestConversationState:
    def test_empty_messages(self):
        state = _extract_conversation_state([])
        assert state["recent_edits_by_user"] is False
        assert state["turn_count"] == 0
        assert state["has_code_in_recent_user_msg"] is False

    def test_detects_recent_edits(self):
        msgs = [
            _msg("user", "fix the bug"),
            _msg("assistant", None, tool_calls=[
                {"function": {"name": "StrReplace", "arguments": '{"path": "a.py"}'}},
            ]),
        ]
        state = _extract_conversation_state(msgs)
        assert state["recent_edits_by_user"] is True

    def test_detects_code_in_user_message(self):
        msgs = [
            _msg("user", "Is this correct?\n```python\ndef foo():\n    return 1\n```"),
        ]
        state = _extract_conversation_state(msgs)
        assert state["has_code_in_recent_user_msg"] is True

    def test_counts_assistant_turns(self):
        msgs = [
            _msg("user", "hi"),
            _msg("assistant", "hello"),
            _msg("user", "do something"),
            _msg("assistant", "done"),
            _msg("user", "more"),
            _msg("assistant", "ok"),
            _msg("user", "even more"),
            _msg("assistant", "sure"),
        ]
        state = _extract_conversation_state(msgs)
        assert state["turn_count"] == 4

    def test_tracks_recent_tool_calls(self):
        msgs = [
            _msg("assistant", None, tool_calls=[
                {"function": {"name": "Read", "arguments": "{}"}},
                {"function": {"name": "Grep", "arguments": "{}"}},
            ]),
        ]
        state = _extract_conversation_state(msgs)
        assert "read" in state["recent_tool_calls"]
        assert "grep" in state["recent_tool_calls"]


# ===========================================================================
# classify_task_cached (cache-only, no LLM call)
# ===========================================================================

class TestClassifyTaskCached:
    def setup_method(self):
        _cache.clear()

    def test_returns_none_when_not_cached(self):
        msgs = [_msg("user", "find bugs in the codebase")]
        assert classify_task_cached(msgs) is None

    def test_returns_cached_value(self):
        msgs = [_msg("user", "find bugs in the codebase")]
        from api.services.intent_utils import intent_detection_text
        user_text = intent_detection_text(msgs, window=6)
        if user_text:
            _cache_put(_cache_key(user_text), TaskType.BUG_FINDING)
            result = classify_task_cached(msgs)
            assert result == TaskType.BUG_FINDING

    def test_empty_messages_returns_none(self):
        assert classify_task_cached([]) is None
