"""Tests for v0.52.0:
  - DeepChainScorer / RetrievalCascadeScorer (item B)
  - Browser agent chain adapter / step_normalizer "browser" format (item C)
  - MultiTurnGuard.score_factual_turn() (item A)
"""
from __future__ import annotations

import sys
import warnings
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))


# ── A: DeepChainScorer / RetrievalCascadeScorer ────────────────────────────────

class TestRetrievalCascadeScorer:

    def _make_steps(self, sims):
        """Create fake Search steps with controlled Jaccard similarity."""
        steps = []
        for i, sim in enumerate(sims):
            # Make action_arg and observation share `sim` fraction of tokens
            shared = [f"tok{j}" for j in range(int(sim * 10))]
            unique_a = [f"a{i}{j}" for j in range(10 - len(shared))]
            unique_b = [f"b{i}{j}" for j in range(10 - len(shared))]
            steps.append({
                "action_type": "Search",
                "action_arg":  " ".join(shared + unique_a),
                "observation": " ".join(shared + unique_b),
                "thought": "",
            })
        steps.append({"action_type": "Finish", "action_arg": "ans", "observation": "", "thought": ""})
        return steps

    def test_import(self):
        from llm_guard import RetrievalCascadeScorer
        assert RetrievalCascadeScorer is not None

    def test_returns_float_in_range(self):
        from llm_guard import RetrievalCascadeScorer
        rc = RetrievalCascadeScorer()
        steps = self._make_steps([0.2, 0.3, 0.5, 0.7])
        score = rc.score(steps)
        assert 0.0 <= score <= 1.0

    def test_improving_retrieval_lower_risk(self):
        """Improving quality trajectory should yield lower risk than declining."""
        from llm_guard import RetrievalCascadeScorer
        rc = RetrievalCascadeScorer()
        improving = self._make_steps([0.1, 0.3, 0.6, 0.9])   # quality goes up
        declining = self._make_steps([0.9, 0.6, 0.3, 0.1])   # quality goes down
        assert rc.score(improving) < rc.score(declining)

    def test_neutral_on_short_chain(self):
        """Fewer than min_search_steps → 0.5 neutral."""
        from llm_guard import RetrievalCascadeScorer
        rc = RetrievalCascadeScorer(min_search_steps=2)
        steps = self._make_steps([0.5])  # only 1 search step
        assert rc.score(steps) == pytest.approx(0.5)

    def test_score_chain_wrapper(self):
        from llm_guard import RetrievalCascadeScorer
        rc = RetrievalCascadeScorer()
        chain = {"steps": self._make_steps([0.2, 0.4, 0.6])}
        score = rc.score_chain(chain)
        assert 0.0 <= score <= 1.0


class TestDeepChainScorer:

    def _make_chain(self, n_search, correct=True):
        steps = []
        for i in range(n_search):
            steps.append({
                "action_type": "Search",
                "action_arg":  f"query {i}",
                "observation": f"result {i}" if i % 2 == 0 else "",
                "thought": f"thinking {i}",
            })
        steps.append({"action_type": "Finish", "action_arg": "answer", "observation": "", "thought": ""})
        return {"question": "Q?", "steps": steps,
                "final_answer": "answer", "correct": correct}

    def test_import(self):
        from llm_guard import DeepChainScorer, DeepChainResult
        assert DeepChainScorer is not None
        assert DeepChainResult is not None

    def test_short_chain_uses_scold(self):
        from llm_guard import DeepChainScorer
        scorer = DeepChainScorer(chain_length_threshold=4)
        chain  = self._make_chain(2)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = scorer.score(chain["question"], chain["steps"], chain["final_answer"])
        assert result.scorer_used == "sc_old"
        assert result.n_search_steps == 2

    def test_long_chain_uses_retrieval_cascade(self):
        from llm_guard import DeepChainScorer
        scorer = DeepChainScorer(chain_length_threshold=4)
        chain  = self._make_chain(5)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = scorer.score(chain["question"], chain["steps"], chain["final_answer"])
        assert result.scorer_used == "retrieval_cascade"
        assert result.n_search_steps == 5

    def test_result_risk_in_range(self):
        from llm_guard import DeepChainScorer
        scorer = DeepChainScorer()
        chain  = self._make_chain(4)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = scorer.score(chain["question"], chain["steps"], chain["final_answer"])
        assert 0.0 <= result.risk_score <= 1.0

    def test_threshold_zero_always_uses_cascade(self):
        from llm_guard import DeepChainScorer
        scorer = DeepChainScorer(chain_length_threshold=0)
        chain  = self._make_chain(1)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = scorer.score(chain["question"], chain["steps"], chain["final_answer"])
        assert result.scorer_used == "retrieval_cascade"


# ── B: Browser agent chain adapter ─────────────────────────────────────────────

class TestBrowserStepNormalizer:

    def test_playwright_click_navigate(self):
        """Playwright-style steps: {type, selector, url, result}"""
        from llm_guard.features.step_normalizer import normalize_steps
        steps = [
            {"type": "navigate", "url": "https://example.com", "result": "Home page loaded"},
            {"type": "click",    "selector": "button#search",  "result": ""},
            {"type": "fill",     "selector": "input#q", "value": "Paris population",
             "result": "filled"},
            {"type": "done",     "result": "Paris has 2.1 million people"},
        ]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            normed = normalize_steps(steps, agent_format="browser")
        assert len(normed) == 4
        # navigate → Search
        assert normed[0]["action_type"] == "Search"
        assert "https://example.com" in normed[0]["action_arg"]
        assert normed[0]["observation"] == "Home page loaded"
        # done → Finish
        assert normed[3]["action_type"] == "Finish"

    def test_openhands_browse_format(self):
        """OpenHands-style: {action: 'browse', args: {url}, observation: {content}}"""
        from llm_guard.features.step_normalizer import normalize_steps
        steps = [
            {"action": "browse",
             "args": {"url": "https://wiki.org/Paris"},
             "observation": {"content": "Paris is the capital of France"}},
            {"action": "finish",
             "args": {"answer": "Paris"},
             "observation": {"content": ""}},
        ]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            normed = normalize_steps(steps, agent_format="browser")
        assert len(normed) == 2
        assert normed[0]["action_type"] == "Search"
        assert "wiki.org" in normed[0]["action_arg"]
        assert "Paris" in normed[0]["observation"]
        assert normed[1]["action_type"] == "Finish"

    def test_auto_detects_browser_format(self):
        """agent_format='auto' should detect browser format."""
        from llm_guard.features.step_normalizer import normalize_steps
        steps = [
            {"type": "navigate", "url": "https://example.com"},
            {"type": "click", "selector": "#btn"},
        ]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            normed = normalize_steps(steps, agent_format="auto")
        assert all(s["action_type"] in ("Search", "Finish") for s in normed)

    def test_browser_format_named_explicitly(self):
        from llm_guard.features.step_normalizer import normalize_steps
        steps = [{"type": "extract", "selector": "h1", "result": "Paris"}]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            normed = normalize_steps(steps, agent_format="browser")
        assert len(normed) == 1
        # Single step with no Finish → last step promoted to Finish
        assert normed[0]["action_type"] == "Finish"

    def test_observation_captured_from_result_field(self):
        from llm_guard.features.step_normalizer import normalize_steps
        steps = [
            {"type": "navigate", "url": "http://x.com", "result": "page content here"},
            {"type": "done"},
        ]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            normed = normalize_steps(steps, agent_format="browser")
        assert normed[0]["observation"] == "page content here"

    def test_browser_in_validate_step_coverage(self):
        from llm_guard.features.step_normalizer import normalize_steps, validate_step_coverage
        steps = [
            {"type": "navigate", "url": "http://x.com", "result": "content"},
            {"type": "navigate", "url": "http://y.com", "result": "more"},
            {"type": "submit"},
        ]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            normed = normalize_steps(steps, agent_format="browser")
        cov = validate_step_coverage(normed)
        assert cov["n_steps"] == 3


# ── C: MultiTurnGuard.score_factual_turn() ────────────────────────────────────

class TestMultiTurnFactualTurn:

    def _make_conv(self, pairs):
        msgs = []
        for q, a in pairs:
            msgs.append({"role": "user", "content": q})
            msgs.append({"role": "assistant", "content": a})
        return msgs

    def test_method_exists(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard
        assert hasattr(MultiTurnGuard, "score_factual_turn")
        assert callable(MultiTurnGuard.score_factual_turn)

    def test_no_api_key_warns_and_returns_result(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard, MultiTurnResult
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            guard = MultiTurnGuard()  # no api_key
        guard._api_key = ""  # ensure no key

        conv = self._make_conv([
            ("What is the capital of France?", "Paris is the capital of France."),
            ("What is 2+2?", "4"),
        ])
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            result = guard.score_factual_turn(conv)

        assert isinstance(result, MultiTurnResult)
        # Warning about missing API key should fire
        warn_msgs = [str(wi.message) for wi in w if issubclass(wi.category, UserWarning)]
        assert any("api key" in m.lower() or "factual" in m.lower() for m in warn_msgs)

    def test_returns_multi_turn_result(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard, MultiTurnResult
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            guard = MultiTurnGuard()
        conv = self._make_conv([
            ("What is 2+2?", "4"),
        ])
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = guard.score_factual_turn(conv)
        assert isinstance(result, MultiTurnResult)
        assert 0.0 <= result.risk_score <= 1.0

    def test_is_factual_reliability_signal_true(self):
        """score_factual_turn() sets is_factual_reliability_signal=True."""
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            guard = MultiTurnGuard()
        conv = self._make_conv([("Q?", "A.")])
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = guard.score_factual_turn(conv)
        assert result.is_factual_reliability_signal is True

    def test_score_turn_still_false(self):
        """score_turn() still returns is_factual_reliability_signal=False."""
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            guard = MultiTurnGuard()
        conv = self._make_conv([("Q?", "A.")])
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = guard.score_turn(conv, use_ptrue=False)
        assert result.is_factual_reliability_signal is False

    def test_quality_preference_set(self):
        from llm_guard.multi_turn.multi_turn_guard import MultiTurnGuard
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            guard = MultiTurnGuard()
        conv = self._make_conv([("What year did WWII end?", "1945")])
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            result = guard.score_factual_turn(conv)
        assert result.quality_preference in ("excellent", "good", "degraded", "poor")
