"""Capability layer / hosting boundary: the generic capability modules (Auth, Forms,
Feature Packs, and the shared lifecycle/runtime/dependency primitives) must never
import hosting-provider-specific code directly. Capabilities declare WHAT runtime
behavior they need (``src.domain.capabilities.runtime``); only
``RuntimeProjectionAdapter`` (``src.domain.hosting.runtime_projection``) and its
concrete adapters decide HOW that gets satisfied for a given provider.

This mirrors the import-linter contract in setup.cfg
(``[importlinter:contract:2]``) with a dependency-free, AST-based check so the
boundary is enforced even where ``import-linter`` isn't installed. It intentionally
checks each module's own *direct* imports only -- not the full transitive call
graph, which also reaches legitimate worker/codegen orchestration that has nothing
to do with a capability making a hosting decision (see the ``ignore_imports`` note
in setup.cfg for the one such edge import-linter has to carve out).
"""
from __future__ import annotations

import ast
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parent.parent

CAPABILITY_LAYER_PACKAGES = (
    "src/domain/capabilities",
    "src/domain/project_auth",
    "src/domain/forms",
    "src/domain/feature_packs",
)

# Exactly the examples the architecture tranche calls out, plus the provider adapter
# package and the hosting-side half of the runtime projection seam itself --
# capabilities must only ever go *into* that seam via their own runtime_requirements(),
# never import it.
FORBIDDEN_MODULE_PREFIXES = (
    "src.domain.hosting.adapters",
    "src.domain.hosting.adapters.cloudflare_pages",
    "src.domain.hosting.adapters.cloudflare_w4p",
    "src.domain.hosting.adapters.w4p_static",
    "src.domain.hosting.adapters.stub_w4p",
    "src.domain.hosting.runtime_projection",
    "src.domain.hosting.w4p_activation",
    "src.domain.hosting.w4p_workflow",
    "src.domain.hosting.routing_plane",
)

# The one legitimate, already-reviewed exception: FeaturePackProvisioner.reconcile()
# delegates to the pre-existing worker task (CLAUDE.md: "heavy operations belong in
# workers"). That task's own, much deeper call graph is worker/codegen plumbing, not
# this layer making a hosting decision -- see setup.cfg's matching ignore_imports.
ALLOWED_IMPORTS = {
    ("src/domain/capabilities/adapters.py", "src.workers.tasks.reconcile_project_capabilities"),
}


def _python_files(package_dir: str) -> list[Path]:
    return sorted((REPO_ROOT / package_dir).rglob("*.py"))


def _imported_modules(path: Path) -> set[str]:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    modules: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            modules.update(alias.name for alias in node.names)
        elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
            modules.add(node.module)
    return modules


def _relative_path(path: Path) -> str:
    return path.relative_to(REPO_ROOT).as_posix()


@pytest.mark.parametrize("package_dir", CAPABILITY_LAYER_PACKAGES)
def test_capability_layer_does_not_import_hosting_provider_modules(package_dir: str):
    violations: list[str] = []
    for path in _python_files(package_dir):
        rel = _relative_path(path)
        for module in _imported_modules(path):
            if (rel, module) in ALLOWED_IMPORTS:
                continue
            if any(
                module == prefix or module.startswith(prefix + ".")
                for prefix in FORBIDDEN_MODULE_PREFIXES
            ):
                violations.append(f"{rel} imports {module}")

    assert not violations, "\n".join(violations)
