"""Real interpreter, environment and RPC contracts for both execution modes."""

import json
import os
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

from tests.tools._child_env_fixtures import child_env, project_python, run_code  # noqa: F401
from tools import code_execution_env as ce
from tools.code_execution_tool import build_execute_code_schema


@pytest.mark.platforms("linux", "macos", "windows")
@pytest.mark.parametrize("mode", ["strict", "project"])
@pytest.mark.parametrize("activation", ["VIRTUAL_ENV", "CONDA_PREFIX"])
def test_selected_interpreter_environment_and_real_rpc(child_env, project_python, monkeypatch, mode, activation):
    python, prefix = project_python
    monkeypatch.setenv(activation, str(prefix))
    repo = Path(__file__).resolve().parents[2]
    site = Path(sys.prefix) / ("Lib/site-packages" if os.name == "nt" else
                              f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages")
    user_lib = child_env / "user-lib"
    user_lib.mkdir()
    (user_lib / "user_probe.py").write_text("VALUE = 'user → 雪'\n", encoding="utf-8")
    witness = child_env / "rpc-witness.txt"
    witness.write_text("RPC → 雪\n", encoding="utf-8")
    monkeypatch.setenv("PYTHONPATH", os.pathsep.join(map(str, [repo, site, repo, user_lib, user_lib])))
    monkeypatch.setenv("OPENAI_API_KEY", "fake-provider-secret")
    before = dict(os.environ)
    result = run_code(f'''
import importlib.util, json, os, sys
import hermes_tools, user_probe
from hermes_tools import read_file
print(json.dumps({{
    "executable": sys.executable, "prefix": sys.prefix, "cwd": os.getcwd(),
    "pythonpath": os.environ["PYTHONPATH"].split(os.pathsep),
    "sys_path": sys.path,
    "staging": os.path.dirname(hermes_tools.__file__),
    "encoding": [os.environ.get("PYTHONIOENCODING"), os.environ.get("PYTHONUTF8")],
    "essentials": {{k: os.environ.get(k) for k in ("SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT")}},
    "secret": os.environ.get("OPENAI_API_KEY"),
    "project": (__import__("project_only_probe").VALUE
                if importlib.util.find_spec("project_only_probe") else None),
    "user": user_probe.VALUE, "rpc": read_file({str(witness)!r}),
}}, ensure_ascii=False))
''', mode)
    assert result["encoding"] == ["utf-8", "1"]
    if os.name == "nt":
        assert result["essentials"] == {k: before.get(k) for k in result["essentials"]}
    assert result["secret"] is None
    assert result["user"] == "user → 雪"
    assert "RPC → 雪" in result["rpc"]["content"]
    assert result["rpc"]["total_lines"] == 1
    expected_python, expected_prefix = (python, prefix) if mode == "project" else (Path(sys.executable), Path(sys.prefix))
    assert Path(result["executable"]) == expected_python
    assert Path(result["prefix"]).resolve() == expected_prefix.resolve()
    assert result["project"] == ("project-only → 雪" if mode == "project" else None)
    if mode == "project":
        assert os.path.normcase(str(site)) not in list(map(os.path.normcase, result["sys_path"]))
    expected_cwd = child_env if mode == "project" else Path(result["staging"])
    assert Path(result["cwd"]).resolve() == expected_cwd.resolve()
    controlled = [result["staging"]] + ([str(repo)] if mode == "strict" else [])
    # macOS: the staging dir is minted under /var/tmp (a symlink to /private/var/tmp) and
    # `hermes_tools.__file__` reports the resolved path, so compare realpaths.
    def _canon(path: str) -> str:
        return os.path.normcase(os.path.realpath(path))
    assert list(map(_canon, result["pythonpath"])) == list(map(_canon, controlled + [str(user_lib)] * 2))
    assert dict(os.environ) == before


@pytest.mark.platforms("linux", "macos", "windows")
@pytest.mark.parametrize("mode", ["strict", "project"])
def test_credential_policy_and_whitelist_in_real_child(child_env, monkeypatch, mode):
    from tools.env_passthrough import register_env_passthrough
    blocked = (
        "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GITHUB_TOKEN", "MY_SECRET",
        "DB_PASSWORD", "VAULT_CREDENTIAL", "LDAP_PASSWD", "AUTH_TOKEN",
        "SENTRY_DSN", "SLACK_WEBHOOK", "HOME_APIKEY", "USER_CREDS", "TERM_BEARER",
        "HERMES_BASE_URL", "HERMES_INTERACTIVE", "BUZZ_PRIVATE_KEY", "RANDOM_UNKNOWN",
    )
    allowed = ("HERMES_PROFILE", "HERMES_CONFIG", "HERMES_ENV", "LC_ENV_TEST", "TENOR_API_KEY")
    for name in blocked + allowed:
        monkeypatch.setenv(name, "fake-" + name)
    # Registration is real: a skill cannot tunnel a provider or Buzz credential.
    register_env_passthrough(["TENOR_API_KEY", "OPENAI_API_KEY", "BUZZ_PRIVATE_KEY"])
    result = run_code(f'''
import json, os, hermes_tools
print(json.dumps({{"env": {{k: os.environ.get(k) for k in {blocked + allowed!r}}},
                  "recursive": hasattr(hermes_tools, "execute_code"),
                  "delegation": hasattr(hermes_tools, "delegate_task")}}))
''', mode, enabled_tools=("read_file", "execute_code", "delegate_task"))
    assert result["env"] == {**dict.fromkeys(blocked), **{k: "fake-" + k for k in allowed}}
    assert result["recursive"] is False
    assert result["delegation"] is False


@pytest.mark.parametrize("helper,cache,failed,success", [
    (ce._is_usable_python, ce._usable_python_cache, False, True),
    (ce._python_environment_prefix, ce._python_prefix_cache, "", os.path.realpath("recovered-prefix")),
])
def test_probe_retries_transient_failure_and_caches_success(helper, cache, failed, success):
    cache.clear()
    try:
        with patch("subprocess.run", side_effect=[
            subprocess.TimeoutExpired(cmd=[], timeout=5),
            subprocess.CompletedProcess([], 0, "recovered-prefix\n"),
        ]) as run:
            assert helper("probe-python") == failed
            assert helper("probe-python") == success
            assert helper("probe-python") == success
        assert run.call_count == 2
    finally:
        cache.clear()


@pytest.mark.parametrize("outcome", [
    OSError("missing interpreter"), subprocess.CompletedProcess([], 1, ""),
    subprocess.CompletedProcess([], 0, " \n"),
])
def test_unknown_prefix_is_not_cached(outcome):
    ce._python_prefix_cache.clear()
    with patch("subprocess.run", side_effect=outcome if isinstance(outcome, Exception) else None,
               return_value=outcome) as run:
        assert ce._python_environment_prefix("bad-python") == ""
        assert ce._python_environment_prefix("bad-python") == ""
    assert run.call_count == 2
    assert "bad-python" not in ce._python_prefix_cache


def test_current_interpreter_needs_no_probe():
    with patch("subprocess.run", side_effect=AssertionError("unexpected probe")):
        assert ce._uses_hermes_python_environment(sys.executable)


def test_project_without_active_venv_falls_back(child_env):
    assert ce._resolve_child_python("project") == sys.executable


def test_project_stale_cwd_falls_through_then_uses_process_cwd(child_env, monkeypatch):
    from tools import terminal_tool
    monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
    monkeypatch.setattr(terminal_tool, "_session_cwd", {})
    terminal_tool.register_task_env_overrides("stale", {"cwd": str(child_env)})
    terminal_tool.record_session_cwd("stale", str(child_env / "deleted"))
    monkeypatch.setenv("TERMINAL_CWD", str(child_env / "missing"))
    assert ce._resolve_child_cwd("project", "staging", task_id="stale") == str(child_env)
    assert ce._resolve_child_cwd("project", "staging") == os.getcwd()


@pytest.mark.parametrize("mode,description", [("strict", "temp dir"), ("project", "session")])
def test_mode_schema_matches_config_without_claiming_isolation(mode, description):
    with patch("tools.code_execution_tool._load_config", return_value={"mode": mode}):
        text = build_execute_code_schema()["description"].lower()
    assert description in text
    assert not any(word in text for word in ("sandbox", "isolated", "cloud"))
