"""
Tests for insaits/detectors/ - Advanced detection algorithms
"""

import numpy as np
import pytest
from insa_its.detectors.semantic_drift import SemanticDriftDetector, DriftResult
from insa_its.detectors.hallucination_chain import HallucinationChainDetector
from insa_its.detectors.jargon_drift import JargonDriftDetector


# ============================================
# Semantic Drift Detector
# ============================================

class TestSemanticDriftDetector:
    def _make_embedding(self, seed: int, dim: int = 384) -> np.ndarray:
        rng = np.random.RandomState(seed)
        emb = rng.randn(dim)
        return emb / np.linalg.norm(emb)

    def test_first_message_no_detection(self):
        d = SemanticDriftDetector()
        emb = self._make_embedding(0)
        result = d.check(emb)
        assert result.detected is False
        assert result.window == 0
        assert "First message" in result.description

    def test_warmup_period(self):
        d = SemanticDriftDetector(min_window=3)
        for i in range(3):
            result = d.check(self._make_embedding(i))
        # Should still be warming up after 2 distances (3 messages = 2 intervals)
        assert result.window == 2
        assert "Warming up" in result.description

    def test_similar_messages_no_detection(self):
        d = SemanticDriftDetector(min_window=2)
        base = self._make_embedding(42)
        # Send similar messages
        for _ in range(5):
            noise = np.random.randn(384) * 0.01
            similar = base + noise
            similar = similar / np.linalg.norm(similar)
            d.check(similar)

        result = d.check(base)
        assert result.detected is False

    def test_drift_detection(self):
        d = SemanticDriftDetector(sensitivity=1.5, min_window=3)
        # Build baseline with similar embeddings
        base = self._make_embedding(42)
        for _ in range(5):
            noise = np.random.randn(384) * 0.01
            similar = base + noise
            similar = similar / np.linalg.norm(similar)
            d.check(similar)

        # Send a completely different embedding
        different = self._make_embedding(999)
        result = d.check(different)
        assert result.detected is True
        assert result.score > result.threshold

    def test_reset(self):
        d = SemanticDriftDetector()
        d.check(self._make_embedding(0))
        d.check(self._make_embedding(1))
        d.reset()
        result = d.check(self._make_embedding(2))
        assert result.window == 0

    def test_drift_result_frozen(self):
        result = DriftResult(
            detected=False, score=0.1, baseline=0.05,
            threshold=0.2, window=5, description="test",
        )
        with pytest.raises(AttributeError):
            result.detected = True

    def test_invalid_sensitivity(self):
        with pytest.raises(ValueError):
            SemanticDriftDetector(sensitivity=0)
        with pytest.raises(ValueError):
            SemanticDriftDetector(sensitivity=-1)

    def test_invalid_min_window(self):
        with pytest.raises(ValueError):
            SemanticDriftDetector(min_window=0)

    def test_zero_norm_embedding(self):
        d = SemanticDriftDetector()
        d.check(np.ones(384))
        result = d.check(np.zeros(384))
        # Should handle gracefully (distance = 1.0 for zero vector)
        assert result.score == 1.0


# ============================================
# Hallucination Chain Detector
# ============================================

class TestHallucinationChainDetector:
    def test_no_detection_on_clean_text(self):
        d = HallucinationChainDetector()
        result = d.check("The system processes data efficiently.", 1)
        assert result["detected"] is False
        assert result["promotion_flags"] == []
        assert result["numeric_drift_flags"] == []

    def test_hedge_extraction(self):
        d = HallucinationChainDetector()
        # Message with hedging
        result = d.check(
            "It might cost around $500. I think the delivery could take weeks.",
            1,
        )
        assert result["confidence_level"] == "hedged"

    def test_assertion_extraction(self):
        d = HallucinationChainDetector()
        result = d.check(
            "The cost is definitely $500. It is confirmed to arrive tomorrow.",
            1,
        )
        assert result["confidence_level"] == "asserted"

    def test_promotion_detection(self):
        d = HallucinationChainDetector(lookback=3)
        # Message 1: hedged claim about quantum computing performance
        d.check(
            "The quantum computing system might process around 1000 calculations per second. "
            "I think the quantum performance could potentially improve significantly.",
            1,
        )
        # Message 2: same claim now asserted confidently
        result = d.check(
            "The quantum computing system definitely processes 1000 calculations per second. "
            "It is confirmed that quantum performance has certainly improved significantly.",
            2,
        )
        assert result["detected"] is True
        assert len(result["promotion_flags"]) > 0

    def test_numeric_drift(self):
        d = HallucinationChainDetector()
        d.check("The cost is $500 for 100 units.", 1)
        result = d.check("The cost is $450 for 120 units.", 2)
        # Numbers changed: 500->450, 100->120
        assert result["detected"] is True
        assert len(result["numeric_drift_flags"]) > 0

    def test_reset(self):
        d = HallucinationChainDetector()
        d.check("Some hedged claim might be true.", 1)
        d.reset()
        result = d.check("The claim is definitely true.", 2)
        # After reset, no prior history = no promotion detection
        assert result["promotion_flags"] == []

    def test_invalid_lookback(self):
        with pytest.raises(ValueError):
            HallucinationChainDetector(lookback=0)


# ============================================
# Jargon Drift Detector
# ============================================

class TestJargonDriftDetector:
    def test_no_detection_during_grace_period(self):
        d = JargonDriftDetector()
        # First messages are grace period
        result = d.check("The XYZ system uses ABC protocol.")
        assert result["detected"] is False
        assert result["undefined_acronyms"] == []

    def test_undefined_acronym_after_grace(self):
        d = JargonDriftDetector()
        # Build vocabulary over grace period
        d.check("The system processes data using standard methods.")
        d.check("We use standard methods for processing.")
        d.check("Standard methods are applied for data processing.")

        # Now introduce undefined acronym
        result = d.check("The system now uses XYZW protocol for QRS transfer.")
        assert result["detected"] is True
        assert "XYZW" in result["undefined_acronyms"] or "QRS" in result["undefined_acronyms"]

    def test_defined_terms_not_flagged(self):
        d = JargonDriftDetector()
        d.check("Processing data normally.")
        d.check("Normal data processing continues.")
        d.check("Data processing is ongoing.")

        # Define the term in the message
        result = d.check("XYZW (Xtreme Yielding Zone Width) is used here.")
        # XYZW is explicitly defined, so it should not be flagged
        # (the definition pattern catches it)
        assert "XYZW" not in result.get("undefined_acronyms", [])

    def test_novel_terms_after_grace(self):
        d = JargonDriftDetector()
        d.check("Simple text with basic words and nothing special.")
        d.check("More basic words and common terminology here.")
        d.check("Continuing with basic vocabulary and common terms.")
        d.check("Still using basic common everyday vocabulary here.")

        # Introduce novel repeated terms
        result = d.check(
            "The defenestration of defenestration shows "
            "defenestration patterns in defenestration analysis."
        )
        # "defenestration" appears 4 times and was never seen before
        if result["novel_terms"]:
            assert "defenestration" in result["novel_terms"]

    def test_vocabulary_grows(self):
        d = JargonDriftDetector()
        d.check("Hello world this is a test message.")
        size1 = d._vocabulary.total() if hasattr(d._vocabulary, 'total') else sum(d._vocabulary.values())
        d.check("Another message with different words and terms.")
        size2 = d._vocabulary.total() if hasattr(d._vocabulary, 'total') else sum(d._vocabulary.values())
        assert size2 > size1

    def test_reset(self):
        d = JargonDriftDetector()
        d.check("Test message with words.")
        assert d._message_count == 1
        d.reset()
        assert d._message_count == 0
        assert len(d._vocabulary) == 0
