"""tests/test_pipeline_continual_scorer.py — Unit tests for ContinualScorer."""
import pytest
import numpy as np
from unittest.mock import MagicMock, patch

pytestmark = pytest.mark.unit

RUNS = [
    {"question": f"Q{i}", "steps": [
        {"thought": "t", "action_type": "Search", "action_arg": f"q{i}",
         "observation": f"obs {i}"}
    ], "final_answer": f"A{i}", "correct": i % 2 == 0}
    for i in range(20)
]


class TestImport:
    def test_import(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        assert ContinualScorer is not None


class TestInstantiation:
    def test_default_construction(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        assert s.lambda_ewc == 400.0
        assert not s.is_fitted

    def test_custom_lambda(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer(lambda_ewc=100.0)
        assert s.lambda_ewc == 100.0

    def test_score_raises_unfitted(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        with pytest.raises(RuntimeError):
            s.score("Q", [], "A")


class TestFit:
    def _make_mock_verifier(self):
        mock_v = MagicMock()
        mock_v._fitted = True
        mock_v.score.return_value = (0.4, 0.1)
        return mock_v

    def test_fit_creates_verifier(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        from llm_guard.scoring.deep_verifier import DeepLocalVerifier
        s = ContinualScorer(n_boot=2)
        with patch.object(DeepLocalVerifier, "fit", return_value=None) as mock_fit:
            # Mock the internal verifier creation
            s._verifier = self._make_mock_verifier()
            s._verifier._fitted = True
        assert s.is_fitted

    def test_is_fitted_property(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        s._verifier = self._make_mock_verifier()
        assert s.is_fitted

    def test_score_after_mock_fit(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        s._verifier = self._make_mock_verifier()
        risk, unc = s.score("Q", [], "A")
        assert 0.0 <= risk <= 1.0
        assert 0.0 <= unc <= 1.0


class TestAnchorFisher:
    def test_anchor_requires_fitted(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        with pytest.raises((RuntimeError, AttributeError)):
            s.anchor_fisher(RUNS)

    def test_anchor_completes_without_torch(self):
        """anchor_fisher gracefully degrades when torch is unavailable or no models."""
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        mock_v = MagicMock()
        mock_v._fitted = True
        # _models not set → no torch models available → fisher = None (graceful)
        mock_v._models = []
        s._verifier = mock_v
        # Should not raise
        s.anchor_fisher(RUNS[:5])
        # With empty models, fisher falls back to None
        assert s._fisher is None or isinstance(s._fisher, dict)


class TestPartialFit:
    def test_partial_fit_without_cpc(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        s = ContinualScorer()
        mock_v = MagicMock()
        mock_v._fitted = True
        mock_v.partial_fit.return_value = None
        s._verifier = mock_v
        s.partial_fit(RUNS[:5], cpc_protect=False)
        mock_v.partial_fit.assert_called_once()

    def test_partial_fit_with_cpc_no_fisher_warns(self):
        from llm_guard.pipeline.continual_scorer import ContinualScorer
        import warnings
        s = ContinualScorer()
        mock_v = MagicMock()
        mock_v._fitted = True
        mock_v.partial_fit.return_value = None
        s._verifier = mock_v
        # No fisher anchored → should warn or fall back to regular partial_fit
        with warnings.catch_warnings(record=True):
            warnings.simplefilter("always")
            s.partial_fit(RUNS[:5], cpc_protect=True)
        mock_v.partial_fit.assert_called()
