"""
Tests for InsAIts AI Monitor v3.0
=================================
Tests all detectors, intervention engine, AIMonitor, ClaudeCodeMonitor,
and the @monitored decorator.
"""

import pytest
from unittest.mock import MagicMock

from insa_its.ai_monitor import (
    AIMonitor,
    ClaudeCodeMonitor,
    AIAnomaly,
    AIMonitorResult,
    AnomalyType,
    AISeverity,
    InterventionAction,
    AIInterventionEngine,
    BlankResponseDetector,
    WaitingForDescriptionDetector,
    IncompleteCodeDetector,
    TruncatedOutputDetector,
    RepetitionLoopDetector,
    ContextCollapseDetector,
    monitored,
)


# ─────────────────────────────────────
# BlankResponseDetector
# ─────────────────────────────────────

class TestBlankResponseDetector:
    def setup_method(self):
        self.detector = BlankResponseDetector()

    def test_empty_string_detected(self):
        result = self.detector.check("", "Write a function")
        assert result is not None
        assert result.anomaly_type == AnomalyType.BLANK_RESPONSE
        assert result.severity == AISeverity.CRITICAL
        assert result.action == InterventionAction.RETRY

    def test_whitespace_only_detected(self):
        result = self.detector.check("   \n  \t  ", "Implement X")
        assert result is not None
        assert result.anomaly_type == AnomalyType.BLANK_RESPONSE

    def test_short_placeholder_detected(self):
        result = self.detector.check("ok.", "Build the registry")
        assert result is not None
        assert result.severity in (AISeverity.CRITICAL, AISeverity.HIGH)

    def test_sure_placeholder_detected(self):
        result = self.detector.check("Sure.", "Fix the bug")
        assert result is not None

    def test_meaningful_response_passes(self):
        response = (
            "Here is the implementation of the skill registry class "
            "that handles lookups using FAISS cosine similarity."
        )
        result = self.detector.check(response, "Build the registry")
        assert result is None

    def test_short_but_real_response_below_threshold(self):
        result = self.detector.check("Yes, done!", "Fix typo")
        assert result is not None  # 10 chars < 20 threshold


# ─────────────────────────────────────
# WaitingForDescriptionDetector
# ─────────────────────────────────────

class TestWaitingForDescriptionDetector:
    def setup_method(self):
        self.detector = WaitingForDescriptionDetector()

    def test_clarification_request_detected(self):
        response = (
            "Could you please provide more details about what specific "
            "functionality you'd like me to implement?"
        )
        result = self.detector.check(response, "Add auth to the app")
        assert result is not None
        assert result.anomaly_type == AnomalyType.WAITING_FOR_DESCRIPTION
        assert result.action == InterventionAction.INJECT_HINT

    def test_stalling_with_before_i_can(self):
        response = (
            "Before I can proceed, I would need more information about "
            "the expected API format and the authentication method."
        )
        result = self.detector.check(response, "Add API endpoint")
        assert result is not None

    def test_genuine_answer_with_followup_passes(self):
        response = (
            "Here is the implementation of the authentication middleware. "
            "It uses JWT tokens for session management and validates against "
            "the Supabase auth provider. The middleware checks the Authorization "
            "header on each request and decodes the token. If the token is invalid, "
            "it returns a 401 response. For the database layer, I used the existing "
            "supabase_client.py repository pattern. Could you clarify whether you "
            "also want refresh token rotation?"
        )
        result = self.detector.check(response, "Add JWT auth")
        # Should pass because the question ratio is low relative to content
        assert result is None

    def test_short_stalling_detected(self):
        response = "Let me know what you'd like me to do."
        result = self.detector.check(response, "Fix the bug in auth.py")
        assert result is not None


# ─────────────────────────────────────
# IncompleteCodeDetector
# ─────────────────────────────────────

class TestIncompleteCodeDetector:
    def setup_method(self):
        self.detector = IncompleteCodeDetector()

    def test_todo_in_code_detected(self):
        response = (
            "```python\n"
            "class SkillRegistry:\n"
            "    def __init__(self):\n"
            "        pass  # TODO: implement\n"
            "```"
        )
        result = self.detector.check(response, "Build registry")
        assert result is not None
        assert result.anomaly_type == AnomalyType.INCOMPLETE_CODE
        assert result.action == InterventionAction.RETRY

    def test_raise_not_implemented_detected(self):
        response = (
            "```python\n"
            "def process(self, data):\n"
            "    raise NotImplementedError\n"
            "```"
        )
        result = self.detector.check(response, "Implement process")
        assert result is not None

    def test_ellipsis_rest_detected(self):
        response = (
            "```python\n"
            "class Handler:\n"
            "    # ... rest of the implementation\n"
            "```"
        )
        result = self.detector.check(response, "Build handler")
        assert result is not None

    def test_complete_code_passes(self):
        response = (
            "```python\n"
            "class SkillRegistry:\n"
            "    def __init__(self):\n"
            "        self.skills = {}\n"
            "\n"
            "    def register(self, name, skill):\n"
            "        self.skills[name] = skill\n"
            "        return self.skills[name]\n"
            "```"
        )
        result = self.detector.check(response, "Build registry")
        assert result is None

    def test_non_code_response_skipped(self):
        response = "The algorithm uses binary search to find the optimal threshold."
        result = self.detector.check(response, "Explain algorithm")
        assert result is None

    def test_unbalanced_brackets_detected(self):
        response = (
            "```python\n"
            "def build():\n"
            "    data = {\n"
            "        'a': [1, 2, 3],\n"
            "        'b': {\n"
            "            'nested': [\n"
            "```"
        )
        result = self.detector.check(response, "Build function")
        assert result is not None
        assert result.severity == AISeverity.MEDIUM


# ─────────────────────────────────────
# TruncatedOutputDetector
# ─────────────────────────────────────

class TestTruncatedOutputDetector:
    def setup_method(self):
        self.detector = TruncatedOutputDetector()

    def test_ends_with_conjunction_detected(self):
        response = "The function processes data in three stages, and"
        result = self.detector.check(response, "Explain the function")
        assert result is not None
        assert result.anomaly_type == AnomalyType.TRUNCATED_OUTPUT

    def test_ends_with_comma_detected(self):
        response = "We need to handle these cases: authentication, authorization,"
        result = self.detector.check(response, "List security concerns")
        assert result is not None

    def test_proper_ending_passes(self):
        response = "The implementation is complete and all tests pass."
        result = self.detector.check(response, "Status update")
        assert result is None

    def test_ends_with_question_mark_passes(self):
        response = "Should I also add error handling?"
        result = self.detector.check(response, "Review code")
        assert result is None


# ─────────────────────────────────────
# RepetitionLoopDetector
# ─────────────────────────────────────

class TestRepetitionLoopDetector:
    def setup_method(self):
        self.detector = RepetitionLoopDetector()

    def test_repeated_content_detected(self):
        response = (
            "The algorithm uses binary search to find the optimal value. "
            "It starts by checking the midpoint of the range. "
            "The algorithm uses binary search to find the optimal value. "
            "It starts by checking the midpoint of the range. "
            "The algorithm uses binary search to find the optimal value. "
            "It starts by checking the midpoint of the range. "
            "Then it narrows the range based on the comparison result."
        )
        result = self.detector.check(response, "Explain binary search")
        assert result is not None
        assert result.anomaly_type == AnomalyType.REPETITION_LOOP

    def test_unique_content_passes(self):
        response = (
            "First, we initialize the search range. "
            "Then, we compute the midpoint. "
            "Next, we compare the midpoint value. "
            "If it matches, we return the index. "
            "If it's less, we narrow to the upper half. "
            "If it's greater, we narrow to the lower half. "
            "This continues until the element is found or range is empty."
        )
        result = self.detector.check(response, "Explain binary search")
        assert result is None

    def test_short_response_skipped(self):
        response = "It works. Done."
        result = self.detector.check(response, "Status")
        assert result is None


# ─────────────────────────────────────
# ContextCollapseDetector
# ─────────────────────────────────────

class TestContextCollapseDetector:
    def setup_method(self):
        self.detector = ContextCollapseDetector()

    def test_off_topic_response_detected(self):
        prompt = "Implement the FAISS index builder in yuyai/knowledge/index.py"
        response = (
            "The capital of France is Paris, which has a long history "
            "dating back to the Roman era when it was known as Lutetia. "
            "The city is known for its beautiful architecture and culture."
        )
        result = self.detector.check(response, prompt)
        assert result is not None
        assert result.anomaly_type == AnomalyType.CONTEXT_COLLAPSE

    def test_on_topic_response_passes(self):
        prompt = "Implement the FAISS index builder in yuyai/knowledge/index.py"
        response = (
            "Here is the FAISS index builder implementation for the knowledge "
            "module. It creates an index from document embeddings and supports "
            "cosine similarity search. The builder loads vectors from the "
            "knowledge store and builds an IVF index for fast retrieval."
        )
        result = self.detector.check(response, prompt)
        assert result is None

    def test_short_prompt_skipped(self):
        result = self.detector.check("Some long answer here " * 10, "Fix it")
        assert result is None

    def test_short_response_skipped(self):
        result = self.detector.check("Done.", "Implement the full FAISS builder")
        assert result is None  # Too short to evaluate context


# ─────────────────────────────────────
# AIInterventionEngine
# ─────────────────────────────────────

class TestAIInterventionEngine:
    def setup_method(self):
        self.engine = AIInterventionEngine()

    def test_build_retry_prompt_includes_anomalies(self):
        anomalies = [
            AIAnomaly(
                anomaly_type=AnomalyType.BLANK_RESPONSE,
                severity=AISeverity.CRITICAL,
                description="Response is empty.",
                confidence=0.97,
                action=InterventionAction.RETRY,
                intervention="Provide a complete response.",
            ),
        ]
        prompt = self.engine.build_retry_prompt(
            original_prompt="Build the registry",
            broken_response="",
            anomalies=anomalies,
        )
        assert "INSAITS QUALITY GATE" in prompt
        assert "CRITICAL" in prompt
        assert "Blank Response" in prompt
        assert "Build the registry" in prompt

    def test_build_hint_injection(self):
        anomalies = [
            AIAnomaly(
                anomaly_type=AnomalyType.REPETITION_LOOP,
                severity=AISeverity.MEDIUM,
                description="Repeating.",
                confidence=0.76,
                action=InterventionAction.INJECT_HINT,
                intervention="Stop repeating content.",
            ),
        ]
        hint = self.engine.build_hint_injection(anomalies)
        assert "[InsAIts hint" in hint
        assert "Stop repeating" in hint

    def test_hint_injection_empty_for_retry_only(self):
        anomalies = [
            AIAnomaly(
                anomaly_type=AnomalyType.BLANK_RESPONSE,
                severity=AISeverity.CRITICAL,
                description="Empty.",
                confidence=0.97,
                action=InterventionAction.RETRY,
                intervention="Retry.",
            ),
        ]
        hint = self.engine.build_hint_injection(anomalies)
        assert hint == ""

    def test_select_action_escalates_to_worst(self):
        anomalies = [
            AIAnomaly(
                anomaly_type=AnomalyType.REPETITION_LOOP,
                severity=AISeverity.MEDIUM,
                description="Loop.",
                confidence=0.76,
                action=InterventionAction.INJECT_HINT,
                intervention="Break loop.",
            ),
            AIAnomaly(
                anomaly_type=AnomalyType.BLANK_RESPONSE,
                severity=AISeverity.CRITICAL,
                description="Empty.",
                confidence=0.97,
                action=InterventionAction.RETRY,
                intervention="Retry.",
            ),
        ]
        action = self.engine.select_action(anomalies)
        assert action == InterventionAction.RETRY


# ─────────────────────────────────────
# AIMonitor
# ─────────────────────────────────────

class TestAIMonitor:
    def test_inspect_clean_response(self):
        monitor = AIMonitor(model_id="test-model")
        result = monitor.inspect(
            "Here is the complete implementation of the skill registry "
            "class with FAISS-based lookup.",
            "Build the skill registry",
        )
        assert result.clean is True
        assert len(result.anomalies) == 0

    def test_inspect_blank_response(self):
        monitor = AIMonitor(model_id="test-model")
        result = monitor.inspect("", "Build something")
        assert result.clean is False
        assert any(
            a.anomaly_type == AnomalyType.BLANK_RESPONSE
            for a in result.anomalies
        )

    def test_call_with_good_llm(self):
        def mock_llm(prompt):
            return (
                "The implementation handles all edge cases and includes "
                "proper error handling with custom exceptions."
            )

        monitor = AIMonitor(model_id="test", llm_fn=mock_llm)
        result = monitor.call("Implement error handling")
        assert result.clean is True
        assert result.retries == 0

    def test_call_retries_on_blank(self):
        call_count = 0

        def mock_llm(prompt):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return ""  # First call returns blank
            return (
                "Here is the complete implementation with all methods "
                "fully implemented and tested."
            )

        monitor = AIMonitor(model_id="test", llm_fn=mock_llm, max_retries=2)
        result = monitor.call("Build it")
        assert call_count == 2
        assert result.retries == 1

    def test_call_without_llm_fn_raises(self):
        monitor = AIMonitor(model_id="test")
        with pytest.raises(ValueError, match="llm_fn not set"):
            monitor.call("Build it")

    def test_session_stats(self):
        monitor = AIMonitor(model_id="test")
        monitor.inspect("", "Build it")
        monitor.inspect(
            "This is a complete and detailed response to the task.",
            "Another task",
        )
        stats = monitor.get_session_stats()
        assert stats["messages_seen"] == 2
        assert stats["anomalies_total"] >= 1
        assert stats["model_id"] == "test"

    def test_on_anomaly_callback_called(self):
        callback = MagicMock()
        monitor = AIMonitor(model_id="test", on_anomaly=callback)
        monitor.inspect("", "Build it")
        assert callback.called

    def test_max_retries_respected(self):
        call_count = 0

        def always_blank(prompt):
            nonlocal call_count
            call_count += 1
            return ""

        monitor = AIMonitor(model_id="test", llm_fn=always_blank, max_retries=3)
        result = monitor.call("Build it")
        assert call_count == 4  # 1 initial + 3 retries
        assert result.resolved is False


# ─────────────────────────────────────
# ClaudeCodeMonitor
# ─────────────────────────────────────

class TestClaudeCodeMonitor:
    def test_clean_response_returns_empty(self):
        cc = ClaudeCodeMonitor(project_name="test")
        feedback = cc.inspect_and_respond(
            "Here is the full implementation of the requested feature "
            "with all edge cases handled properly.",
            "Add feature X",
        )
        assert feedback == ""

    def test_blank_response_returns_intervention(self):
        cc = ClaudeCodeMonitor(project_name="test")
        feedback = cc.inspect_and_respond("", "Build the registry")
        assert feedback != ""
        assert "InsAIts" in feedback
        assert "BLANK RESPONSE" in feedback

    def test_stalling_response_returns_intervention(self):
        cc = ClaudeCodeMonitor(project_name="test")
        feedback = cc.inspect_and_respond(
            "Could you please provide more details about what you need?",
            "Add auth middleware",
        )
        assert feedback != ""
        assert "InsAIts" in feedback

    def test_assert_task_ready_specific_task(self):
        cc = ClaudeCodeMonitor(project_name="test")
        check = cc.assert_task_ready(
            "Implement SkillRegistry.lookup() in "
            "yuyai/skills/registry.py using FAISS cosine search"
        )
        assert check["ready"] is True

    def test_assert_task_ready_vague_task(self):
        cc = ClaudeCodeMonitor(project_name="test")
        check = cc.assert_task_ready("Fix something in the code")
        assert check["ready"] is False
        assert len(check["warnings"]) > 0

    def test_assert_task_ready_short_task(self):
        cc = ClaudeCodeMonitor(project_name="test")
        check = cc.assert_task_ready("Fix it")
        assert check["ready"] is False


# ─────────────────────────────────────
# @monitored decorator
# ─────────────────────────────────────

class TestMonitoredDecorator:
    def test_decorator_wraps_function(self):
        @monitored(model_id="test-decorator")
        def my_agent(prompt):
            return (
                "This is a complete response with all necessary details "
                "about the implementation and testing strategy."
            )

        result = my_agent("Build the feature")
        assert isinstance(result, AIMonitorResult)
        assert result.clean is True

    def test_decorator_preserves_name(self):
        @monitored(model_id="test")
        def my_custom_agent(prompt):
            return "Complete response with enough detail to pass checks."

        assert my_custom_agent.__name__ == "my_custom_agent"


# ─────────────────────────────────────
# Data model properties
# ─────────────────────────────────────

class TestAIMonitorResult:
    def test_clean_property(self):
        result = AIMonitorResult(
            original_response="test",
            final_response="test",
            anomalies=[],
            retries=0,
            latency_ms=10.0,
            session_id="s1",
            message_id="m1",
            timestamp="2026-01-01T00:00:00",
            resolved=True,
        )
        assert result.clean is True
        assert result.critical is False

    def test_critical_property(self):
        anomaly = AIAnomaly(
            anomaly_type=AnomalyType.BLANK_RESPONSE,
            severity=AISeverity.CRITICAL,
            description="Empty.",
            confidence=0.97,
            action=InterventionAction.RETRY,
            intervention="Retry.",
        )
        result = AIMonitorResult(
            original_response="",
            final_response="",
            anomalies=[anomaly],
            retries=0,
            latency_ms=10.0,
            session_id="s1",
            message_id="m1",
            timestamp="2026-01-01T00:00:00",
            resolved=False,
        )
        assert result.clean is False
        assert result.critical is True
