from __future__ import annotations

import json
import importlib.util
import subprocess
import sys
from pathlib import Path


REPO = Path(__file__).resolve().parents[1]
AUDIT_SCRIPT = REPO / "scripts" / "audit_agent_source_dependencies.py"


def _audit_module():
    spec = importlib.util.spec_from_file_location("audit_agent_source_dependencies", AUDIT_SCRIPT)
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def _run_audit() -> dict[str, object]:
    proc = subprocess.run(
        [sys.executable, str(AUDIT_SCRIPT), str(REPO)],
        cwd=REPO,
        capture_output=True,
        text=True,
        check=True,
    )
    return json.loads(proc.stdout)


def _run_markdown_audit() -> str:
    proc = subprocess.run(
        [sys.executable, str(AUDIT_SCRIPT), str(REPO), "--format", "markdown"],
        cwd=REPO,
        capture_output=True,
        text=True,
        check=True,
    )
    return proc.stdout


def _class_by_id(report: dict[str, object]) -> dict[str, dict[str, object]]:
    classes = report["dependency_classes"]
    assert isinstance(classes, list)
    return {item["id"]: item for item in classes}


def _anchors(dep_class: dict[str, object]) -> set[tuple[str, str]]:
    findings = dep_class["findings"]
    assert isinstance(findings, list)
    return {
        (str(finding["path"]), str(finding["anchor"]))
        for finding in findings
    }


def _texts(dep_class: dict[str, object]) -> list[str]:
    findings = dep_class["findings"]
    assert isinstance(findings, list)
    return [str(finding["text"]) for finding in findings]


def _findings(dep_class: dict[str, object]) -> list[dict[str, object]]:
    findings = dep_class["findings"]
    assert isinstance(findings, list)
    return findings


def test_audit_reports_expected_dependency_classes():
    report = _run_audit()

    assert report["schema_version"] == 1
    classes = _class_by_id(report)
    assert set(classes) == {
        "docker_agent_source_volume",
        "startup_dependency_install",
        "runtime_agent_execution",
        "runtime_auxiliary_model_metadata",
        "runtime_session_state",
        "runtime_gateway_provider",
        "webui_local_or_client_package",
    }
    for dep_class in classes.values():
        assert dep_class["finding_count"] > 0
        assert dep_class["findings"]
        assert dep_class["replacement_surface"]


def test_audit_reports_compose_source_volume_anchors():
    classes = _class_by_id(_run_audit())
    anchors = _anchors(classes["docker_agent_source_volume"])
    texts = _texts(classes["docker_agent_source_volume"])

    assert ("docker-compose.two-container.yml", "hermes-agent-src") in anchors
    assert ("docker-compose.three-container.yml", "hermes-agent-src") in anchors
    assert any("hermes-agent-src:/opt/hermes" in text for text in texts)
    assert any(
        "hermes-agent-src:/home/hermeswebui/.hermes/hermes-agent:ro" in text
        for text in texts
    )


def test_audit_reports_startup_install_dependencies():
    classes = _class_by_id(_run_audit())
    anchors = _anchors(classes["startup_dependency_install"])
    texts = _texts(classes["startup_dependency_install"])

    assert ("api/startup.py", "HERMES_WEBUI_AGENT_DIR") in anchors
    assert ("bootstrap.py", "HERMES_WEBUI_AGENT_DIR") in anchors
    assert ("start.ps1", "HERMES_WEBUI_AGENT_DIR") in anchors
    assert ("api/startup.py", "auto_install_agent_deps") in anchors
    assert ("server.py", "auto_install_agent_deps") in anchors
    assert any("uv pip install" in text and "[all]" in text for text in texts)


def test_audit_reports_runtime_agent_execution_imports():
    classes = _class_by_id(_run_audit())
    anchors = _anchors(classes["runtime_agent_execution"])

    assert ("api/agent_runtime.py", "run_agent") in anchors
    assert ("api/routes.py", "tools.skills_tool") in anchors
    assert ("api/streaming.py", "tools.approval") in anchors
    assert ("api/routes.py", "cron.jobs") in anchors


def test_audit_reports_runtime_auxiliary_and_model_metadata_imports():
    classes = _class_by_id(_run_audit())
    anchors = _anchors(classes["runtime_auxiliary_model_metadata"])

    assert ("api/streaming.py", "agent.auxiliary_client") in anchors
    assert ("api/streaming.py", "agent.model_metadata") in anchors
    assert ("api/config.py", "hermes_cli.models") in anchors
    assert ("api/providers.py", "agent.account_usage") in anchors


def test_audit_embedded_worker_import_line_anchors_are_source_lines():
    classes = _class_by_id(_run_audit())
    provider_findings = _findings(classes["runtime_auxiliary_model_metadata"])
    account_usage = next(
        finding
        for finding in provider_findings
        if finding["path"] == "api/providers.py" and finding["anchor"] == "agent.account_usage"
    )

    expected_text = "from agent.account_usage import fetch_account_usage"
    assert account_usage["text"] == expected_text
    # The reported line number must be an ACCURATE source anchor — i.e. that line
    # in the real file actually contains the import. Asserting the exact number is
    # brittle (any edit above it shifts it); asserting the line CONTENT at the
    # reported line is the real invariant ("line anchors are source lines").
    source_lines = (REPO / account_usage["path"]).read_text(encoding="utf-8").splitlines()
    assert account_usage["line"] >= 1
    assert source_lines[account_usage["line"] - 1].strip() == expected_text


def test_audit_reports_runtime_state_and_provider_imports():
    classes = _class_by_id(_run_audit())
    state_anchors = _anchors(classes["runtime_session_state"])
    provider_anchors = _anchors(classes["runtime_gateway_provider"])

    assert ("api/streaming.py", "hermes_state") in state_anchors
    assert ("api/state_sync.py", "hermes_state") in state_anchors
    assert ("api/streaming.py", "hermes_cli.runtime_provider") in provider_anchors
    assert ("api/routes.py", "hermes_cli.runtime_provider") in provider_anchors


def test_runtime_import_scan_includes_root_python_entrypoints():
    root = AUDIT_SCRIPT.parents[1]
    audit_module = _audit_module()
    paths = {
        path.relative_to(root).as_posix()
        for path in audit_module._iter_python_files(root)
    }

    assert "api/routes.py" in paths
    assert "server.py" in paths
    assert "bootstrap.py" in paths


def test_audit_keeps_client_package_candidates_visible():
    classes = _class_by_id(_run_audit())
    anchors = _anchors(classes["webui_local_or_client_package"])

    assert ("api/streaming.py", "hermes_constants") in anchors
    assert ("api/routes.py", "agent.skill_utils") in anchors
    assert ("api/routes.py", "hermes_cli.plugins") in anchors
    assert ("api/providers.py", "agent.credential_pool") in anchors


def test_markdown_output_is_utf8_safe_on_windows_stdout():
    markdown = _run_markdown_audit()

    assert "# Agent Source Dependency Audit" in markdown
    assert "runtime_agent_execution" in markdown


def test_contract_index_links_agent_api_contract():
    contracts = (REPO / "docs" / "CONTRACTS.md").read_text(encoding="utf-8")

    assert "docs/architecture/agent-api-contract.md" in contracts
    assert "issue #2491" in contracts
    assert "source mounts can be removed" in contracts
