from __future__ import annotations

import asyncio
import json
from types import SimpleNamespace

from autodebug.agent import sdk_bridge


class _FakeAgent:
    def __init__(self):
        self._function_tools = {}

    def tool(self, fn):
        self._function_tools[fn.__name__] = type("Tool", (), {"function": fn})()
        return fn


def _ctx(tmp_path):
    return SimpleNamespace(deps=SimpleNamespace(project_path=str(tmp_path)))


def test_compact_run_tests_adapter_returns_envelope(tmp_path):
    agent = _FakeAgent()

    @agent.tool
    async def run_tests(ctx, path: str = "tests/", pattern: str | None = None, maxfail: int = 10, quick: bool = False, verbose: bool = False):
        return "**Test Result: FAILED**\n\n2 failed, 3 passed in 1.23s\npkg/test_api.py::test_one"

    sdk_bridge.populate_registry_from_agent(agent, profile="compact")
    entries = {name: (func, schema) for name, func, _, schema in sdk_bridge._TOOL_REGISTRY}
    wrapped, _schema = entries["run_tests"]
    raw = asyncio.run(wrapped(_ctx(tmp_path), path="tests/test_api.py"))
    payload = json.loads(raw)
    assert payload["status"] in {"ok", "partial", "error"}
    assert payload["data"]["fail_count"] == 2
    assert payload["data"]["pass_count"] == 3
    assert isinstance(payload["data"]["failed_tests"], list)


def test_compact_review_files_adapter_schema_override_and_output(tmp_path):
    agent = _FakeAgent()

    @agent.tool
    async def review_files(
        ctx, paths: list[str], focus: str = "general",
        max_files_per_turn: int = 15, max_workers_per_file: int = 3,
        max_total_worker_invocations: int = 30,
    ):
        return "### src/app.py (reviewed by: quality)\n- Line 10 [HIGH|LIKELY|0.9]: sample issue"

    sdk_bridge.populate_registry_from_agent(agent, profile="compact")
    entries = {name: (func, schema) for name, func, _, schema in sdk_bridge._TOOL_REGISTRY}
    wrapped, schema = entries["review_files"]
    assert "context_hint" in schema.get("properties", {})
    assert "worker_modes" in schema.get("properties", {})
    raw = asyncio.run(wrapped(_ctx(tmp_path), paths=["src/app.py"], focus="security", context_hint="auth flow"))
    payload = json.loads(raw)
    assert payload["status"] in {"ok", "partial", "error"}
    assert "severity_counts" in payload["data"]
    assert payload["data"]["files"][0]["path"] == "src/app.py"


def test_compact_update_findings_adapter_returns_structured_store_path(tmp_path):
    agent = _FakeAgent()

    @agent.tool
    async def update_findings(
        ctx,
        action: str = "list",
        issue_id: str = "",
        file: str = "",
        line: int = 0,
        severity: str = "MEDIUM",
        description: str = "",
        status: str = "open",
    ):
        return "OK. 1 open / 2 total findings. (action=add, id=B-01)"

    sdk_bridge.populate_registry_from_agent(agent, profile="compact")
    entries = {name: (func, schema) for name, func, _, schema in sdk_bridge._TOOL_REGISTRY}
    wrapped, schema = entries["update_findings"]
    assert schema["properties"]["action"]["enum"] == ["list", "add", "update", "remove"]
    assert schema["properties"]["severity"]["enum"] == ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
    raw = asyncio.run(wrapped(_ctx(tmp_path), action="add", issue_id="B-01", file="a.py", line=1, severity="HIGH", description="x"))
    payload = json.loads(raw)
    assert payload["status"] == "ok"
    assert payload["data"]["updated_count"] == 1
    assert payload["data"]["store_path"].endswith("/.hikaflow/findings.json")


def test_legacy_alias_wrapper_logs_metric(monkeypatch):
    calls = []

    def _fake_inc(metric: str, value: int = 1):
        calls.append((metric, value))

    class _Metrics:
        @staticmethod
        def inc(metric: str, value: int = 1):
            _fake_inc(metric, value)

    monkeypatch.setitem(__import__("sys").modules, "api.metrics", _Metrics)

    async def _target(ctx, **kwargs):
        return "{}"

    entries = [
        ("incident_overview", _target, "x", {"type": "object", "properties": {}}),
    ]
    monkeypatch.setenv("HIKAFLOW_ENABLE_LEGACY_ALIASES", "1")
    out = sdk_bridge._with_legacy_aliases(entries)  # noqa: SLF001 - intentional internal behavior check
    alias_entry = next(e for e in out if e[0] == "fetch_production_errors")
    asyncio.run(alias_entry[1](SimpleNamespace(deps=SimpleNamespace(project_path="."))))
    assert any(m[0] == "tool.legacy_alias.used" for m in calls)


def test_mcp_feature_flag_disables_update_findings_adapter(monkeypatch, tmp_path):
    agent = _FakeAgent()

    @agent.tool
    async def update_findings(
        ctx,
        action: str = "list",
        issue_id: str = "",
        file: str = "",
        line: int = 0,
        severity: str = "MEDIUM",
        description: str = "",
        status: str = "open",
    ):
        return "LEGACY_TEXT"

    monkeypatch.setenv("ENABLE_MCP_UPDATE_FINDINGS", "0")
    sdk_bridge.populate_registry_from_agent(agent, profile="compact")
    entries = {name: (func, schema) for name, func, _, schema in sdk_bridge._TOOL_REGISTRY}
    wrapped, schema = entries["update_findings"]

    # Legacy schema path should not inject frozen enum constraints.
    assert "enum" not in schema["properties"]["action"]
    raw = asyncio.run(wrapped(_ctx(tmp_path), action="list"))
    assert raw == "LEGACY_TEXT"


# ---------------------------------------------------------------------------
# Gap 4: review_files budget params in compact adapter
# ---------------------------------------------------------------------------
def test_compact_review_files_adapter_has_budget_params(tmp_path):
    agent = _FakeAgent()

    @agent.tool
    async def review_files(
        ctx,
        paths: list[str],
        focus: str = "general",
        max_files_per_turn: int = 15,
        max_workers_per_file: int = 3,
        max_total_worker_invocations: int = 30,
    ):
        return f"Reviewed {len(paths)} files (budget: {max_files_per_turn}/{max_workers_per_file}/{max_total_worker_invocations})"

    sdk_bridge.populate_registry_from_agent(agent, profile="compact")
    entries = {name: (func, schema) for name, func, _, schema in sdk_bridge._TOOL_REGISTRY}
    _, schema = entries["review_files"]

    # Schema should expose budget parameters
    props = schema.get("properties", {})
    assert "max_files_per_turn" in props
    assert "max_workers_per_file" in props
    assert "max_total_worker_invocations" in props
    assert props["max_files_per_turn"]["default"] == 15
    assert props["max_workers_per_file"]["default"] == 3
    assert props["max_total_worker_invocations"]["default"] == 30


def test_compact_review_files_adapter_passes_budget_through(tmp_path):
    agent = _FakeAgent()
    _received = {}

    @agent.tool
    async def review_files(
        ctx,
        paths: list[str],
        focus: str = "general",
        max_files_per_turn: int = 15,
        max_workers_per_file: int = 3,
        max_total_worker_invocations: int = 30,
    ):
        _received["max_files_per_turn"] = max_files_per_turn
        _received["max_workers_per_file"] = max_workers_per_file
        _received["max_total_worker_invocations"] = max_total_worker_invocations
        return "### test.py (reviewed by: quality)\nNo issues found."

    sdk_bridge.populate_registry_from_agent(agent, profile="compact")
    entries = {name: (func, schema) for name, func, _, schema in sdk_bridge._TOOL_REGISTRY}
    wrapped, _ = entries["review_files"]
    result = asyncio.run(wrapped(
        _ctx(tmp_path), paths=["test.py"],
        max_files_per_turn=5, max_workers_per_file=1, max_total_worker_invocations=10,
    ))
    parsed = json.loads(result)
    assert parsed["status"] == "ok"
    assert _received["max_files_per_turn"] == 5
    assert _received["max_workers_per_file"] == 1
    assert _received["max_total_worker_invocations"] == 10


# ---------------------------------------------------------------------------
# Gap 4: pending_reviews parsing
# ---------------------------------------------------------------------------
def test_parse_review_files_output_extracts_pending_reviews():
    raw = (
        "### a.py (reviewed by: security)\nNo issues found.\n\n"
        "**Budget limit reached.** 2 file(s) deferred:\n"
        "- `b.py` (worker_budget_exhausted)\n"
        "- `c.py` (worker_budget_exhausted)\n"
    )
    parsed = sdk_bridge._parse_review_files_output(raw, ["a.py", "b.py", "c.py"])
    assert len(parsed["data"]["pending_reviews"]) == 2
    assert parsed["data"]["pending_reviews"][0]["path"] == "b.py"
    assert parsed["data"]["pending_reviews"][1]["path"] == "c.py"


# ---------------------------------------------------------------------------
# Structured findings in review_files envelope
# ---------------------------------------------------------------------------
def test_parse_review_files_output_extracts_structured_findings():
    raw = (
        "### api/main.py (reviewed by: security, quality)\n"
        "- Line 42 [HIGH|++|0.91][security]: SQL injection via unsanitized input\n"
        "- Line 88 [MEDIUM|+|0.65][quality]: Unused variable 'temp'\n"
        "\n### api/db.py (reviewed by: quality)\n"
        "No issues found.\n"
    )
    parsed = sdk_bridge._parse_review_files_output(raw, ["api/main.py", "api/db.py"])
    findings = parsed["data"]["findings"]
    assert len(findings) == 2
    assert findings[0]["file"] == "api/main.py"
    assert findings[0]["line"] == 42
    assert findings[0]["severity"] == "HIGH"
    assert findings[0]["worker"] == "security"
    assert "SQL injection" in findings[0]["message"]
    assert findings[1]["line"] == 88
    assert findings[1]["severity"] == "MEDIUM"


def test_parse_review_files_output_empty_has_no_findings():
    parsed = sdk_bridge._parse_review_files_output(
        "### test.py (reviewed by: quality)\nNo issues found.", ["test.py"]
    )
    assert parsed["data"]["findings"] == []


# ---------------------------------------------------------------------------
# Gap 5: OTel spans in MCP adapters
# ---------------------------------------------------------------------------
def test_otel_tracer_import_is_optional():
    """_mcp_tracer should be None or a real tracer — never crash the module."""
    # Just verify the module imported successfully with the tracer attribute
    assert hasattr(sdk_bridge, "_mcp_tracer")


def test_otel_span_emitted_when_tracer_available(monkeypatch, tmp_path):
    """When _mcp_tracer is set, tool calls should create spans."""
    spans_created = []

    class _FakeSpan:
        def __init__(self, name, attributes=None):
            self.name = name
            self.attributes = attributes or {}
            spans_created.append(self)

        def end(self):
            pass

    class _FakeTracer:
        def start_span(self, name, attributes=None):
            return _FakeSpan(name, attributes=attributes)

    monkeypatch.setattr(sdk_bridge, "_mcp_tracer", _FakeTracer())

    agent = _FakeAgent()

    @agent.tool
    async def knowledge_memory(ctx, action: str, payload=""):
        return "OK"

    sdk_bridge.populate_registry_from_agent(agent, profile="compact")

    # We need to verify the tracer is called during MCP server tool invocation.
    # Since create_hikaflow_mcp_server requires the claude_agent_sdk import,
    # we test via source inspection that the span creation code exists.
    import inspect
    src = inspect.getsource(sdk_bridge.create_hikaflow_mcp_server)
    assert "_mcp_tracer" in src
    assert "mcp.tool." in src
    assert "tool.duration_ms" in src
    assert "tool.result_status" in src


def test_otel_span_not_emitted_when_tracer_none(monkeypatch):
    """When _mcp_tracer is None, no span errors should occur."""
    monkeypatch.setattr(sdk_bridge, "_mcp_tracer", None)
    # Verify the module still works with no tracer
    import inspect
    src = inspect.getsource(sdk_bridge.create_hikaflow_mcp_server)
    assert "if _mcp_tracer is not None" in src
