"""Tests for the verify_claims graph node, routing logic, and phase injection.

Tests that the verification checkpoint:
- Always fires once in scan/debug mode (unconditional)
- Does NOT fire in chat/fix mode
- Fires only once (verify_claims_fired flag)
- Is NOT bypassed by prior bash/run_tests execution

Tests that phase injection:
- Returns empty for chat/fix modes
- Returns INVESTIGATE/VERIFY/CONCLUDE based on budget progress
- Includes draft/verified counts when findings exist
"""

import pytest
from unittest.mock import Mock

from autodebug.agent.langgraph_runner import should_continue, verify_claims_node, _phase_status_line


def _make_ai_message(content="Here are my findings...", tool_calls=None):
    """Create a mock AI message."""
    msg = Mock()
    msg.content = content
    msg.tool_calls = tool_calls or []
    return msg


def _make_state(mode="scan", verify_claims_fired=False, nudge_attempted=True, **overrides):
    """Create a minimal state dict for routing tests."""
    state = {
        "messages": [_make_ai_message()],
        "mode": mode,
        "verify_claims_fired": verify_claims_fired,
        "nudge_attempted": nudge_attempted,
    }
    state.update(overrides)
    return state


# ---------------------------------------------------------------------------
# should_continue routing — verify_claims path
# ---------------------------------------------------------------------------


class TestShouldContinueVerifyClaims:
    """Test that should_continue routes to verify_claims correctly."""

    def test_scan_mode_routes_to_verify(self):
        """Agent in scan mode should always get verification checkpoint."""
        state = _make_state(mode="scan", verify_claims_fired=False)
        assert should_continue(state) == "verify_claims"

    def test_debug_mode_routes_to_verify(self):
        """Agent in debug mode should always get verification checkpoint."""
        state = _make_state(mode="debug", verify_claims_fired=False)
        assert should_continue(state) == "verify_claims"

    def test_chat_mode_does_not_verify(self):
        """Chat mode should end directly, no verification needed."""
        state = _make_state(mode="chat", verify_claims_fired=False)
        assert should_continue(state) == "end"

    def test_fix_mode_does_not_verify(self):
        """Fix mode uses verify_node for edits, not verify_claims."""
        state = _make_state(mode="fix", verify_claims_fired=False)
        assert should_continue(state) == "end"

    def test_already_fired_ends(self):
        """If verify_claims already fired, should end normally."""
        state = _make_state(mode="scan", verify_claims_fired=True)
        assert should_continue(state) == "end"

    def test_tool_calls_take_precedence(self):
        """If agent produced tool calls, route to tools regardless."""
        msg = _make_ai_message(tool_calls=[{"name": "read_file", "args": {}}])
        state = _make_state(mode="scan", verify_claims_fired=False, messages=[msg])
        assert should_continue(state) == "tools"

    def test_nudge_before_verify_claims(self):
        """Nudge fires first if not attempted, verify_claims fires after."""
        state = _make_state(mode="scan", verify_claims_fired=False, nudge_attempted=False)
        # Nudge should fire first (substantive content > 20 chars)
        assert should_continue(state) == "nudge"

    def test_verify_claims_after_nudge(self):
        """After nudge was attempted, verify_claims fires."""
        state = _make_state(mode="scan", verify_claims_fired=False, nudge_attempted=True)
        assert should_continue(state) == "verify_claims"

    def test_unconditional_even_after_bash(self):
        """verify_claims fires even if bash already ran (unconditional gate).

        This is the h6 bypass fix: running run_tests or bash early in the
        session must NOT prevent the verification checkpoint from firing.
        The gate is unconditional — it always fires once in scan/debug mode.
        """
        # Simulate state where bash/run_tests already ran but gate hasn't fired
        state = _make_state(mode="scan", verify_claims_fired=False)
        assert should_continue(state) == "verify_claims"

    def test_unconditional_even_after_run_tests(self):
        """verify_claims fires even if run_tests already ran.

        The h6 scenario: agent runs test suite first, then does code
        analysis and reports without testing conclusions. The old design
        let run_tests set claims_verified=True, bypassing the gate.
        """
        state = _make_state(mode="debug", verify_claims_fired=False)
        assert should_continue(state) == "verify_claims"


# ---------------------------------------------------------------------------
# verify_claims_node function
# ---------------------------------------------------------------------------


class TestVerifyClaimsNode:
    """Test the verify_claims_node function."""

    @pytest.mark.asyncio
    async def test_returns_nudge_message(self):
        config = {"configurable": {}}
        state = _make_state()
        result = await verify_claims_node(state, config)

        assert "messages" in result
        assert len(result["messages"]) == 1
        assert "verify" in result["messages"][0].content.lower()
        assert "run" in result["messages"][0].content.lower()

    @pytest.mark.asyncio
    async def test_sets_verify_claims_fired_flag(self):
        config = {"configurable": {}}
        state = _make_state()
        result = await verify_claims_node(state, config)

        assert result["verify_claims_fired"] is True

    @pytest.mark.asyncio
    async def test_calls_display_fn(self):
        display_fn = Mock()
        config = {"configurable": {"display_fn": display_fn}}
        state = _make_state()
        await verify_claims_node(state, config)

        display_fn.assert_called_once_with("verify_claims")

    @pytest.mark.asyncio
    async def test_does_not_set_claims_verified(self):
        """verify_claims_node should NOT set claims_verified (removed field)."""
        config = {"configurable": {}}
        state = _make_state()
        result = await verify_claims_node(state, config)

        assert "claims_verified" not in result


# ---------------------------------------------------------------------------
# Nudge content — regression tests for h11 fixes
# ---------------------------------------------------------------------------


class TestVerifyClaimsNudgeContent:
    """Test that the nudge message contains correct guidance.

    These catch the h11 root causes:
    - Nudge suggested python -c with dangerous strings (sandbox trap)
    - Nudge didn't mention echo as a safe alternative
    - Nudge didn't explain the three-state model
    - Nudge referenced wrong function names
    """

    @pytest.fixture
    def nudge_text(self):
        """Extract the nudge message text for content assertions."""
        import asyncio
        config = {"configurable": {}}
        state = _make_state()
        result = asyncio.get_event_loop().run_until_complete(
            verify_claims_node(state, config)
        )
        return result["messages"][0].content

    def test_requires_verification_with_code(self, nudge_text):
        """Nudge must demand code execution before reporting."""
        assert "verify" in nudge_text.lower()
        assert "bash" in nudge_text.lower() or "run_tests" in nudge_text.lower()

    def test_mentions_run_tests(self, nudge_text):
        """Nudge should suggest run_tests as a verification method."""
        assert "run_tests" in nudge_text

    def test_mentions_bash_example(self, nudge_text):
        """Nudge should include a bash example for verification."""
        assert "bash(command=" in nudge_text or "bash(" in nudge_text

    def test_does_not_suggest_dangerous_python_c_example(self, nudge_text):
        """Nudge should NOT contain dangerous command strings in examples."""
        import re
        python_c_blocks = re.findall(r'python -c ["\'].*?["\']', nudge_text)
        for block in python_c_blocks:
            assert "rm -rf" not in block, f"python -c example contains dangerous string: {block}"

    def test_mentions_draft_findings_workflow(self, nudge_text):
        """Nudge should explain the draft → open/wontfix workflow."""
        lower = nudge_text.lower()
        assert "draft" in lower
        assert "open" in lower
        assert "wontfix" in lower

    def test_mentions_update_findings(self, nudge_text):
        """Nudge should tell agent to update findings status after verification."""
        assert "update_findings" in nudge_text or "update" in nudge_text.lower()

    def test_nudge_requires_running_code(self, nudge_text):
        """Nudge must be explicit about running code, not just reading."""
        assert "must verify" in nudge_text.lower() or "must run" in nudge_text.lower()


# ---------------------------------------------------------------------------
# Phase status line injection
# ---------------------------------------------------------------------------


class TestPhaseStatusLine:
    """Test that _phase_status_line computes correct phase and guidance."""

    def test_empty_for_chat_mode(self):
        state = {"mode": "chat", "turn_count": 0, "turn_budget": 10}
        assert _phase_status_line(state) == ""

    def test_empty_for_fix_mode(self):
        state = {"mode": "fix", "turn_count": 0, "turn_budget": 10}
        assert _phase_status_line(state) == ""

    def test_investigate_phase_early_turns(self):
        state = {"mode": "scan", "turn_count": 0, "turn_budget": 10, "project_path": ""}
        line = _phase_status_line(state)
        assert "INVESTIGATE" in line
        assert "Turn 1/10" in line

    def test_verify_phase_mid_turns(self):
        state = {"mode": "scan", "turn_count": 4, "turn_budget": 10, "project_path": ""}
        line = _phase_status_line(state)
        assert "VERIFY" in line
        assert "Turn 5/10" in line
        assert "update_findings" in line

    def test_conclude_phase_late_turns(self):
        state = {"mode": "scan", "turn_count": 7, "turn_budget": 10, "project_path": ""}
        line = _phase_status_line(state)
        assert "CONCLUDE" in line
        assert "Turn 8/10" in line

    def test_debug_mode_also_gets_phases(self):
        state = {"mode": "debug", "turn_count": 0, "turn_budget": 10, "project_path": ""}
        line = _phase_status_line(state)
        assert "INVESTIGATE" in line

    def test_phase_boundaries(self):
        """40% = VERIFY, 70% = CONCLUDE."""
        # Turn 3/10 = 30% → INVESTIGATE
        assert "INVESTIGATE" in _phase_status_line(
            {"mode": "scan", "turn_count": 3, "turn_budget": 10, "project_path": ""})
        # Turn 4/10 = 40% → VERIFY
        assert "VERIFY" in _phase_status_line(
            {"mode": "scan", "turn_count": 4, "turn_budget": 10, "project_path": ""})
        # Turn 6/10 = 60% → VERIFY
        assert "VERIFY" in _phase_status_line(
            {"mode": "scan", "turn_count": 6, "turn_budget": 10, "project_path": ""})
        # Turn 7/10 = 70% → CONCLUDE
        assert "CONCLUDE" in _phase_status_line(
            {"mode": "scan", "turn_count": 7, "turn_budget": 10, "project_path": ""})
