import pytest

from api.services.investigation_v2 import (
    Stage,
    can_terminate,
    classify_ambiguity,
    compute_entropy,
    new_state,
    update_state_from_tool,
)
from api.services.tool_router_v2 import ToolRouter, ToolRouterV2Error


def test_entropy_and_ambiguity_classification():
    # concentrated in one directory => low entropy
    e1 = compute_entropy(["backend", "backend", "backend"])
    assert classify_ambiguity(e1) == "LOW"

    # spread across a few dirs => medium entropy
    e2 = compute_entropy(["backend", "frontend", "backend", "docs"])
    assert classify_ambiguity(e2) in ("MEDIUM", "HIGH")


def test_state_updates_from_search_and_read_and_hypothesis():
    s = new_state("req-1")
    assert s.stage == Stage.SEARCH

    update_state_from_tool(
        s,
        "Search",
        {"query": "ToolExecutor", "match_count": 3, "files": ["backend/api/a.py", "backend/api/b.py"]},
    )
    assert s.stage == Stage.READ
    assert s.searches and s.searches[0].query == "ToolExecutor"
    assert s.ambiguity_level in ("LOW", "MEDIUM", "HIGH")

    update_state_from_tool(s, "ReadFile", {"path": "backend/api/a.py", "hash": "h1", "symbols": [], "lines": 10})
    assert s.stage == Stage.VALIDATE
    assert "backend/api/a.py" in s.files_read

    update_state_from_tool(s, "ReadFile", {"path": "backend/api/b.py", "hash": "h2", "symbols": [], "lines": 12})
    assert len(s.files_read) == 2

    update_state_from_tool(
        s, "Hypothesis", {"statement": "ToolExecutor executes tools", "files": ["backend/api/a.py"], "confidence": 0.8}
    )
    assert s.hypotheses and s.hypotheses[0].confidence == 0.8


def test_can_terminate_gates_low_ambiguity():
    s = new_state("req-2")

    # Not enough evidence initially
    ok, reason = can_terminate(s)
    assert not ok
    assert "No searches performed" in reason

    # One search + 2 reads + hypothesis => should pass for LOW ambiguity
    update_state_from_tool(s, "Search", {"query": "x", "match_count": 1, "files": ["backend/a.py", "backend/b.py"]})
    update_state_from_tool(s, "ReadFile", {"path": "backend/a.py", "hash": "h1", "symbols": [], "lines": 1})
    update_state_from_tool(s, "ReadFile", {"path": "backend/b.py", "hash": "h2", "symbols": [], "lines": 1})
    update_state_from_tool(s, "Hypothesis", {"statement": "x", "files": ["backend/a.py"], "confidence": 0.5})

    ok, reason = can_terminate(s)
    assert ok, reason


def test_can_terminate_gates_medium_entropy_requires_3_reads():
    s = new_state("req-3")
    # Spread across 3 dirs => medium entropy typically; enforce 3 reads requirement if MEDIUM.
    update_state_from_tool(
        s,
        "Search",
        {"query": "x", "match_count": 3, "files": ["backend/a.py", "frontend/b.ts", "docs/c.md"]},
    )
    update_state_from_tool(s, "ReadFile", {"path": "backend/a.py", "hash": "h1", "symbols": [], "lines": 1})
    update_state_from_tool(s, "ReadFile", {"path": "frontend/b.ts", "hash": "h2", "symbols": [], "lines": 1})
    update_state_from_tool(s, "Hypothesis", {"statement": "x", "files": ["backend/a.py"], "confidence": 0.7})

    ok, reason = can_terminate(s)
    if s.ambiguity_level == "MEDIUM":
        assert not ok
        assert "Medium entropy requires more validation reads" in reason
        update_state_from_tool(s, "ReadFile", {"path": "docs/c.md", "hash": "h3", "symbols": [], "lines": 1})
        ok2, reason2 = can_terminate(s)
        assert ok2, reason2
    else:
        # If it classified as LOW or HIGH, just assert it follows its own rules.
        assert isinstance(ok, bool)


def test_tool_router_blocks_terminate_until_gates_pass():
    router = ToolRouter()
    s = router.init_request("req-4")

    with pytest.raises(ToolRouterV2Error):
        router.execute(s, "Terminate", {})

    router.execute(s, "Search", {"query": "x", "match_count": 1, "files": ["backend/a.py", "backend/b.py"]})
    router.execute(s, "ReadFile", {"path": "backend/a.py", "hash": "h1", "symbols": [], "lines": 1})
    router.execute(s, "ReadFile", {"path": "backend/b.py", "hash": "h2", "symbols": [], "lines": 1})
    router.execute(s, "Hypothesis", {"statement": "x", "files": ["backend/a.py"], "confidence": 0.9})

    # Now termination should be allowed
    router.execute(s, "Terminate", {})


def test_can_terminate_requires_grounded_reads():
    s = new_state("req-grounded")
    update_state_from_tool(s, "Search", {"query": "x", "match_count": 2, "files": ["backend/a.py", "backend/b.py"]})
    # Read files not in search results
    update_state_from_tool(s, "ReadFile", {"path": "backend/other.py", "hash": "h1", "symbols": [], "lines": 1})
    update_state_from_tool(s, "ReadFile", {"path": "backend/another.py", "hash": "h2", "symbols": [], "lines": 1})
    update_state_from_tool(s, "Hypothesis", {"statement": "x", "files": ["backend/other.py"], "confidence": 0.5})
    ok, reason = can_terminate(s)
    assert not ok
    assert "grounded" in reason.lower()


def test_tool_executor_glob_accepts_glob_pattern_alias():
    from api.services.tool_executor import ToolExecutor

    te = ToolExecutor(workspace_root=".")
    # Should not raise "Missing required argument: pattern" when glob_pattern is used.
    out = te.execute_tool("Glob", {"glob_pattern": "**/*.py"})
    assert isinstance(out, list)


def test_extract_tool_calls_from_markup():
    from api.services.complete.agentic_controller_v2 import _extract_tool_calls_from_markup

    txt = (
        '<tool_call>Bash<arg_key>command</arg_key><arg_value>echo hi</arg_value>'
        '<arg_key>description</arg_key><arg_value>test</arg_value></tool_call>'
    )
    tcs = _extract_tool_calls_from_markup(txt)
    assert isinstance(tcs, list) and len(tcs) == 1
    assert tcs[0]["function"]["name"] == "Bash"


def test_tool_honesty_system_message_is_prepended_idempotently():
    from api.models import CompleteRequest
    from api.services.complete.agentic_controller_v2 import add_tool_honesty_system_message

    msgs = [CompleteRequest.Message(role="user", content="hello")]
    out1 = add_tool_honesty_system_message(msgs)
    assert len(out1) == 2
    assert out1[0].role == "system"
    assert "TOOL HONESTY (MANDATORY)" in (out1[0].content or "")

    out2 = add_tool_honesty_system_message(out1)
    assert len(out2) == 2


def test_detects_unbacked_tool_claim_without_any_tool_evidence():
    from api.models import CompleteRequest
    from api.services.complete.agentic_controller_v2 import looks_like_unbacked_tool_claim

    msgs = [
        CompleteRequest.Message(role="user", content="please debug this"),
        CompleteRequest.Message(role="assistant", content="The grep output shows compression_ratio is 1.0 everywhere."),
    ]
    assert looks_like_unbacked_tool_claim(msgs) is True


def test_shell_path_relinking(tmp_path):
    # Create a workspace structure
    ws = tmp_path / "workspace"
    ws.mkdir()
    (ws / "scripts").mkdir()
    (ws / "scripts" / "x.txt").write_text("hi", encoding="utf-8")

    from api.services.tool_executor import ToolExecutor

    te = ToolExecutor(workspace_root=str(ws))
    # Simulate Cursor absolute path; should be relinked to ws/scripts.
    cmd = f'ls -1 "{"/Users/someone/Desktop/Anything/scripts"}"'
    out = te.execute_tool("Shell", {"command": cmd})
    assert isinstance(out, dict)
    # If relinking worked, stdout should include x.txt (command succeeds).
    assert "x.txt" in (out.get("stdout") or "") or (out.get("returncode") == 0)


def test_shell_result_includes_executed_command(tmp_path):
    ws = tmp_path / "workspace"
    ws.mkdir()
    from api.services.tool_executor import ToolExecutor

    te = ToolExecutor(workspace_root=str(ws))
    out = te.execute_tool("Shell", {"command": "echo hi"})
    assert isinstance(out, dict)
    assert "command" in out
