"""tests/test_adversarial_novelty_detector.py — Unit tests for LLMNoveltyDetector and QuestionComplexityScorer."""
import pytest
import warnings

pytestmark = pytest.mark.unit

CHAINS = [
    {"question": f"Who did what in context {i}?", "steps": [
        {"thought": "searching", "action_type": "Search",
         "action_arg": f"query {i}", "observation": f"Result for {i}"}
    ], "final_answer": f"Answer {i}"}
    for i in range(35)
]


class TestImport:
    def test_import_detector(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        assert LLMNoveltyDetector is not None

    def test_import_result(self):
        from llm_guard.adversarial.novelty_detector import NoveltyResult
        assert NoveltyResult is not None

    def test_import_complexity(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer
        assert QuestionComplexityScorer is not None

    def test_import_extract_vec(self):
        from llm_guard.adversarial.novelty_detector import _extract_vec
        assert _extract_vec is not None


class TestExtractVec:
    def test_returns_array(self):
        from llm_guard.adversarial.novelty_detector import _extract_vec
        import numpy as np
        vec = _extract_vec(CHAINS[0])
        assert isinstance(vec, np.ndarray)
        assert len(vec) >= 9

    def test_empty_chain(self):
        from llm_guard.adversarial.novelty_detector import _extract_vec
        import numpy as np
        vec = _extract_vec({"question": "", "steps": [], "final_answer": ""})
        assert isinstance(vec, np.ndarray)


class TestLLMNoveltyDetectorInstantiation:
    def test_default_construction(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        det = LLMNoveltyDetector()
        assert not det._fitted

    def test_custom_thresholds(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        det = LLMNoveltyDetector(force_judge_threshold=0.9, uncertain_threshold=0.6)
        assert det._force_threshold == 0.9
        assert det._uncertain_threshold == 0.6

    def test_score_raises_unfitted(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        det = LLMNoveltyDetector()
        with pytest.raises(RuntimeError):
            det.score(CHAINS[0])

    def test_fit_empty_raises(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        det = LLMNoveltyDetector()
        with pytest.raises(ValueError):
            det.fit([])


class TestLLMNoveltyDetectorFit:
    def _fitted_det(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        det = LLMNoveltyDetector(min_cal_for_maha=5, use_isolation_forest=True)
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            det.fit(CHAINS)
        return det

    def test_fit_marks_fitted(self):
        det = self._fitted_det()
        assert det._fitted

    def test_n_cal_set(self):
        det = self._fitted_det()
        assert det._n_cal == len(CHAINS)

    def test_score_returns_result(self):
        from llm_guard.adversarial.novelty_detector import NoveltyResult
        det = self._fitted_det()
        result = det.score(CHAINS[0])
        assert isinstance(result, NoveltyResult)

    def test_ensemble_score_range(self):
        det = self._fitted_det()
        result = det.score(CHAINS[0])
        assert 0.0 <= result.ensemble_score <= 1.0

    def test_routing_values(self):
        det = self._fitted_det()
        result = det.score(CHAINS[0])
        assert result.routing in ("trusted", "uncertain", "force_judge")

    def test_score_batch(self):
        det = self._fitted_det()
        results = det.score_batch(CHAINS[:3])
        assert len(results) == 3

    def test_calibration_summary_keys(self):
        det = self._fitted_det()
        summary = det.calibration_summary()
        assert summary["fitted"] is True
        assert "n_cal" in summary

    def test_calibration_summary_unfitted(self):
        from llm_guard.adversarial.novelty_detector import LLMNoveltyDetector
        det = LLMNoveltyDetector()
        s = det.calibration_summary()
        assert s["fitted"] is False


class TestQuestionComplexityScorer:
    def test_import(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer
        assert QuestionComplexityScorer is not None

    def test_fit_and_score(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer, ComplexityResult
        scorer = QuestionComplexityScorer()
        scorer.fit(CHAINS)
        result = scorer.score(CHAINS[0])
        assert isinstance(result, ComplexityResult)
        assert 0.0 <= result.complexity_score <= 1.0

    def test_score_raises_unfitted(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer
        scorer = QuestionComplexityScorer()
        with pytest.raises(RuntimeError):
            scorer.score(CHAINS[0])

    def test_routing_in_valid_values(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer
        scorer = QuestionComplexityScorer()
        scorer.fit(CHAINS)
        result = scorer.score({"question": "What is the capital of France?"})
        assert result.routing in ("trusted", "uncertain", "force_judge")

    def test_feature_names_count(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer
        assert len(QuestionComplexityScorer.FEATURE_NAMES) == 7

    def test_score_batch(self):
        from llm_guard.adversarial.novelty_detector import QuestionComplexityScorer
        scorer = QuestionComplexityScorer()
        scorer.fit(CHAINS)
        results = scorer.score_batch(CHAINS[:5])
        assert len(results) == 5
