"""Import-graph regression tests for slim entry points (#1436).

Two guards keep import-time regressions from creeping back:

1. A heavy-optional-dependency ban: importing the primary runtime must not
   import provider SDKs, storage engines, or ML/data stacks; those load on
   first use.
   Slim entry points additionally must not import the nio matrix client or the
   mcp SDK, which the primary runtime genuinely needs at boot.
2. A third-party allowlist: each slim entry point may only load the
   third-party packages it loads today. Any new package in the graph fails
   loudly — either defer the import (see the CLAUDE.md import rule) or extend
   the allowlist as a conscious, reviewed decision. The orchestrator uses the
   narrower heavy-optional-dependency ban because its core boot graph is large.

Each probe runs in a subprocess so the assertion sees exactly what the import
graph pulls in.
"""

from __future__ import annotations

import json
import subprocess
import sys

import pytest

_PROVIDER_SDK_ROOTS = (
    "anthropic",
    "boto3",
    "cerebras",
    "google.genai",
    "groq",
    "ollama",
    "openai",
)
_HEAVY_OPTIONAL_RUNTIME_ROOTS = (
    # Provider-specific clients and authentication.
    *_PROVIDER_SDK_ROOTS,
    "agno.models.vertexai.claude",
    "google.auth",
    "google.oauth2",
    # Storage engines.
    "agno.vectordb.chroma",
    "chromadb",
    "mem0",
    # ML and data stacks.
    "numpy",
    "pandas",
    "scipy",
    "sentence_transformers",
    "sklearn",
    "torch",
    "transformers",
)
_SLIM_ONLY_ROOTS = ("mcp", "nio")

# Small, stable third-party footprints shared by the config layer and the
# registry chain: pydantic and yaml for config, structlog/rich for logging,
# cryptography for credentials, dotenv for runtime env files.
_CONFIG_LAYER_ROOTS = frozenset(
    {
        "annotated_types",
        "attr",
        "colorama",
        "cryptography",
        "cython_runtime",
        "dotenv",
        "greenlet",
        "pydantic",
        "pydantic_core",
        "pygments",
        "rich",
        "structlog",
        "typing_extensions",
        "typing_inspection",
        "yaml",
    },
)
# The registry additionally carries httpx (sandbox proxy transport) and click
# (tool CLI plumbing).
_REGISTRY_ROOTS = _CONFIG_LAYER_ROOTS | frozenset({"brotli", "click", "httpx", "idna", "zstandard"})
# The full tool-module sweep adds the agno toolkit runtime (redis-backed run
# cancellation, opentelemetry via redis) and the httpx dependency tree.
_TOOLS_ROOTS = _REGISTRY_ROOTS | frozenset(
    {
        "agno",
        "anyio",
        "attrs",
        "certifi",
        "docstring_parser",
        "h11",
        "h2",
        "hpack",
        "httpcore",
        "hyperframe",
        "importlib_metadata",
        "opentelemetry",
        "outcome",
        "packaging",
        "psutil",
        "redis",
        "sniffio",
        "sortedcontainers",
        "trio",
        "xxhash",
        "zipp",
    },
)
# Top-level CLI help only needs command metadata and presentation libraries.
# Command implementations must defer networking, crypto, config validation,
# and provider imports until the selected command runs.
_CLI_ROOTS = frozenset(
    {
        "annotated_doc",
        "attr",
        "click",
        "dotenv",
        "pygments",
        "rich",
        "shellingham",
        "typer",
    },
)
_ALLOWED_THIRD_PARTY_ROOTS: dict[str, frozenset[str]] = {
    "mindroom.cli.main": _CLI_ROOTS,
    # Doctor may use the CLI, config, and HTTP stacks at import time, but no
    # provider, storage, or other feature-specific dependency.
    "mindroom.cli.doctor": _CLI_ROOTS | _REGISTRY_ROOTS,
    "mindroom.config.main": _CONFIG_LAYER_ROOTS,
    "mindroom.model_loading": _CONFIG_LAYER_ROOTS | frozenset({"agno"}),
    "mindroom.tool_system.declarations": frozenset({"dotenv"}),
    "mindroom.tool_system.metadata": _REGISTRY_ROOTS,
    "mindroom.tool_system.catalog": _REGISTRY_ROOTS,
    "mindroom.tool_system.registration": _CONFIG_LAYER_ROOTS,
    "mindroom.tools": _TOOLS_ROOTS,
    # The sandbox runner is the API server for worker tool execution: fastapi
    # and its dependency tree on top of the full registry footprint.
    "mindroom.api.sandbox_runner": _TOOLS_ROOTS
    | frozenset(
        {
            "annotated_doc",
            "authlib",
            "bcrypt",
            "chardet",
            "charset_normalizer",
            "email_validator",
            "fastapi",
            "joserfc",
            "orjson",
            "python_multipart",
            "requests",
            "socks",
            "starlette",
            "urllib3",
        },
    ),
}

_BAN_PROBE_TEMPLATE = """
import importlib, json, sys

importlib.import_module({module!r})
roots = {roots!r}
loaded = sorted(
    name
    for name in sys.modules
    if any(name == root or name.startswith(root + ".") for root in roots)
)
print(json.dumps(loaded))
"""

_SLIM_PROBE_TEMPLATE = """
import importlib, json, sys

stdlib = set(sys.stdlib_module_names)
baseline = {{name.split(".")[0] for name in sys.modules}}
importlib.import_module({module!r})
banned_roots = {banned_roots!r}
banned = sorted(
    name
    for name in sys.modules
    if any(name == root or name.startswith(root + ".") for root in banned_roots)
)
# mypyc-compiled dependencies register hash-named helper modules (e.g.
# "4ef79d...__mypyc") that vary per build, so they are excluded from the
# footprint along with private roots and mindroom itself.
third_party = sorted(
    root
    for root in {{name.split(".")[0] for name in sys.modules}} - stdlib - baseline
    if not root.startswith("_") and not root.endswith("__mypyc") and not root.startswith("mindroom")
)
print(json.dumps({{"banned": banned, "third_party": third_party}}))
"""


def _run_probe(probe: str) -> list[str]:
    result = subprocess.run(
        [sys.executable, "-c", probe],
        capture_output=True,
        text=True,
        check=True,
        timeout=120,
    )
    return json.loads(result.stdout)


def _run_slim_probe(module: str) -> tuple[list[str], frozenset[str]]:
    result = subprocess.run(
        [
            sys.executable,
            "-c",
            _SLIM_PROBE_TEMPLATE.format(
                module=module,
                banned_roots=_PROVIDER_SDK_ROOTS + _SLIM_ONLY_ROOTS,
            ),
        ],
        capture_output=True,
        text=True,
        check=True,
        timeout=120,
    )
    payload = json.loads(result.stdout)
    return payload["banned"], frozenset(payload["third_party"])


def _assert_probe_clean(module: str, roots: tuple[str, ...]) -> None:
    loaded = _run_probe(_BAN_PROBE_TEMPLATE.format(module=module, roots=roots))
    assert loaded == [], f"importing {module} pulled in banned modules: {loaded}"


@pytest.mark.parametrize("module", sorted(_ALLOWED_THIRD_PARTY_ROOTS))
def test_slim_entry_point_import_contract(module: str) -> None:
    """One isolated import must satisfy both the banned-module and third-party contracts."""
    banned, third_party = _run_slim_probe(module)
    assert banned == [], f"importing {module} pulled in banned modules: {banned}"
    unexpected = sorted(third_party - _ALLOWED_THIRD_PARTY_ROOTS[module])
    assert not unexpected, (
        f"importing {module} now loads third-party packages not in its allowlist: {unexpected}. "
        "Defer the import to first use (see the CLAUDE.md function-level import rule), "
        "or extend the allowlist here if the dependency is genuinely needed at import time."
    )


def test_primary_runtime_defers_heavy_optional_dependencies() -> None:
    """The orchestrator import must leave provider, storage, and ML/data engines unloaded."""
    _assert_probe_clean("mindroom.orchestrator", _HEAVY_OPTIONAL_RUNTIME_ROOTS)


def test_openai_wire_models_import_only_the_openai_sdk() -> None:
    """Agno's azure package init pulls the anthropic SDK; only the azure branch should pay that."""
    non_openai_roots = tuple(root for root in _PROVIDER_SDK_ROOTS if root != "openai")
    _assert_probe_clean("mindroom.openai_models", non_openai_roots)


def test_builtin_tool_manifest_does_not_import_runtime_catalog() -> None:
    """Built-in registration may write registry state without loading catalog behavior."""
    _assert_probe_clean(
        "mindroom.tools",
        ("mindroom.tool_system.catalog", "mindroom.tool_system.metadata"),
    )


def test_tool_auto_install_smoke_entrypoint_imports() -> None:
    """The repository smoke entry point must use the post-split tool-system surfaces."""
    subprocess.run(
        [sys.executable, "-c", "import scripts.testing.tool_auto_install_smoke"],
        check=True,
        timeout=120,
    )
