"""Offline plugin command fixtures; only tool acquisition is substituted.

The actual client, worker, resolver, lock, publication callbacks and receipts
run unchanged. The prepared test interpreter hosts PM in a separate process;
fresh selected interpreters never inherit its site-packages.
"""
from __future__ import annotations

import json
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import sys


import pytest
import hermes_yaml as yaml

from tests.pm._fixtures import _wheel, isolated_python as isolated_python


def worker_command(worker: Path, uv: str, python: str, *, prelude: str = "", runtime_python: str | None = None) -> list[str]:
    script = (
        "import runpy, sys; from pathlib import Path; "
        f"sys.path.insert(0, {str(worker.parent.parent)!r}); import pm._uv; "
        f"pm._uv._toolchain = lambda **kwargs: (Path({uv!r}), Path({python!r}))\n"
        + prelude + "\n"
        f"runpy.run_path({str(worker)!r}, run_name='__main__')"
    )
    return [runtime_python or python, "-I", "-B", "-c", script]


def git(repo: Path, *args: str) -> str:
    env = {k: v for k, v in os.environ.items() if k not in {"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"}}
    env.update(GIT_AUTHOR_NAME="fixture", GIT_AUTHOR_EMAIL="fixture@example.invalid",
               GIT_COMMITTER_NAME="fixture", GIT_COMMITTER_EMAIL="fixture@example.invalid")
    result = subprocess.run(["git", *args], cwd=repo, env=env, capture_output=True, text=True, timeout=30)
    assert result.returncode == 0, result.stderr
    return result.stdout.strip()


def version(repo: Path, *, name="plugin-worker-proof", surface="python_dependencies", suffix="yaml", pin="1.0", capabilities=(), dependency="plugin-proof-dep") -> str:
    manifest: dict = {"name": name, "version": pin}
    if capabilities:
        manifest["capabilities"] = list(capabilities)
    specs = [f"{dependency}=={pin}"]
    if surface and surface != "pyproject":
        manifest[surface] = specs
    (repo / f"plugin.{suffix}").write_text(yaml.safe_dump(manifest), encoding="utf-8")
    (repo / "__init__.py").write_text(
        f"import {dependency.replace('-', '_')} as plugin_proof_dep\nVERSION = {pin!r}\n"
        "def register(ctx):\n    return (VERSION, plugin_proof_dep.__version__)\n", encoding="utf-8")
    if surface == "pyproject":
        (repo / "pyproject.toml").write_text(
            f'[project]\nname={json.dumps(name)}\nversion="{pin}"\nrequires-python=">=3.11"\n'
            f'dependencies={json.dumps(specs)}\n[tool.uv]\npackage=false\n', encoding="utf-8")
    git(repo, "add", ".")
    git(repo, "commit", "-qm", f"fixture {pin}")
    return git(repo, "rev-parse", "HEAD")


class PluginWorld:
    runtime_python: str

    def __init__(self, root: Path):
        self.root = root
        self.home = root / "home"
        self.core = root / "core"

    def origin(self, **kwargs) -> tuple[Path, str]:
        repo = self.root / (kwargs.get("name", "plugin-worker-proof") + "-origin")
        repo.mkdir()
        git(repo, "init", "-qb", "main")
        return repo, version(repo, **kwargs)

    def command(self, action: str, **kwargs) -> None:
        from hermes_cli.plugins_cmd import plugins_command
        from hermes_cli.subcommands.plugins import build_plugins_parser

        parser = argparse.ArgumentParser()
        build_plugins_parser(parser.add_subparsers(), cmd_plugins=plugins_command)
        positional = {"install": ("identifier",), "enable": ("name",), "disable": ("name",),
                      "pack": ("pack_action", "source")}[action]
        argv = ["plugins", action, *(str(kwargs.pop(key)) for key in positional)]
        for key, value in kwargs.items():
            if value is True:
                argv.append("--" + key.replace("_", "-"))
            elif value is not None and value is not False:
                argv.extend(["--" + key.replace("_", "-"), str(value)])
        plugins_command(parser.parse_args(argv))

    def selected(self) -> Path:
        from pm.environments import selected_venv
        return selected_venv(self.core)

    def enabled(self) -> list[str]:
        return yaml.safe_load((self.home / "config.yaml").read_text(encoding="utf-8"))["plugins"]["enabled"]

    def imports(self, name="plugin-worker-proof", pin="1.0") -> None:
        selected = self.selected()
        python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
        code = (
            "import importlib.util, pathlib; import plugin_core_dep, plugin_proof_dep; "
            f"assert pathlib.Path(plugin_core_dep.__file__).is_relative_to({str(selected)!r}); "
            f"assert pathlib.Path(plugin_proof_dep.__file__).is_relative_to({str(selected)!r}); "
            f"spec = importlib.util.spec_from_file_location('loaded_plugin', {str(self.home / 'plugins' / name / '__init__.py')!r}); "
            "plugin = importlib.util.module_from_spec(spec); spec.loader.exec_module(plugin); "
            f"assert plugin.register(None) == ({pin!r}, {pin!r})"
        )
        result = subprocess.run([str(python), "-I", "-B", "-c", code], cwd=self.root,
                                capture_output=True, text=True, timeout=30)
        assert result.returncode == 0, result.stderr


def publish_plugins(world: PluginWorld, plugins: dict[str, list[str]]) -> Path:
    """Enable exactly ``plugins`` (name → declared requirements) and publish the generation PM
    builds for them. Each plugin imports its requirements, leaves an ``imported`` marker, and
    ``request(specs)`` asks ``install_specs`` for more on its own behalf."""
    from pm import client

    for name, requirements in plugins.items():
        plugin = world.home / "plugins" / name
        if plugin.is_dir():
            continue
        plugin.mkdir(parents=True)
        (plugin / "plugin.yaml").write_text(yaml.safe_dump(
            {"name": name, "version": "1.0", "python_dependencies": requirements}), encoding="utf-8")
        modules = [spec.split("=")[0].split("<")[0].split(">")[0].replace("-", "_") for spec in requirements]
        (plugin / "__init__.py").write_text(
            "".join(f"import {module}\n" for module in modules)
            + "from pathlib import Path\nPath(__file__).with_name('imported').touch()\n"
            "def register(ctx):\n    pass\n"
            "def request(specs):\n    from tools.lazy_deps import install_specs\n    return install_specs(specs)\n",
            encoding="utf-8")
    (world.home / "config.yaml").write_text(yaml.safe_dump(
        {"plugins": {"enabled": sorted(plugins), "disabled": []}}), encoding="utf-8")
    client.sync_venv(explicit=True)
    return world.selected()


@pytest.fixture
def boot(plugin_world, monkeypatch):
    """``boot(environment)``: this test process imports from ``environment`` the way a Hermes process
    booted on it does. Adoption rewrites sys.path and PATH (monkeypatch restores both); modules
    imported from the world are forgotten afterwards, so a later test imports its own."""
    from pm.environments import site_packages, venv_bin_dir

    def run_from(environment: Path) -> None:
        monkeypatch.setenv("PATH", os.pathsep.join([str(venv_bin_dir(environment)), os.defpath]))
        monkeypatch.syspath_prepend(str(site_packages(environment)))

    yield run_from
    for name, module in list(sys.modules.items()):
        if str(getattr(module, "__file__", None) or "").startswith(str(plugin_world.root)):
            del sys.modules[name]


@pytest.fixture
def plugin_world(tmp_path, monkeypatch, isolated_python):
    from pm import client, paths

    world = PluginWorld(tmp_path)
    uv = shutil.which("uv")
    assert uv, "real uv is required for the plugin lifecycle"
    for key in ("HERMES_MANAGED_MODE", "HERMES_INSTALL_ROOT", "VIRTUAL_ENV"):
        monkeypatch.delenv(key, raising=False)
    for key, value in {"HOME": tmp_path, "USERPROFILE": tmp_path, "HERMES_HOME": world.home,
                       "HERMES_RUNTIME_DIR": tmp_path / "store", "UV_CACHE_DIR": tmp_path / "cache",
                       "XDG_CONFIG_HOME": tmp_path / "config", "XDG_CONFIG_DIRS": tmp_path / "config",
                       "HERMES_DISABLE_LAZY_INSTALLS": "1", "UV_OFFLINE": "1"}.items():
        monkeypatch.setenv(key, str(value))
    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    monkeypatch.setattr(paths, "repo_root", lambda: world.core)
    monkeypatch.setattr(paths, "lockfile_path", lambda: tmp_path / "lock.json")
    world.runtime_python = str(isolated_python)
    monkeypatch.setattr(client, "runtime_command", lambda worker, **kwargs:
                        worker_command(worker, uv, sys.executable, runtime_python=world.runtime_python))
    assert not client.is_runtime(), "fixture must exercise the client/worker boundary"
    wheels = tmp_path / "wheels"
    wheels.mkdir()
    _wheel(wheels, "plugin_core_dep")
    _wheel(wheels, "plugin_proof_other")
    for pin in ("1.0", "2.0"):
        _wheel(wheels, "plugin_proof_dep", pin)
    world.home.mkdir()
    (world.home / "config.yaml").write_text("plugins:\n  enabled: []\n  disabled: []\n", encoding="utf-8")
    world.core.mkdir()
    (world.core / "pyproject.toml").write_text(
        '[project]\nname="plugin-proof-core"\nversion="1"\nrequires-python=">=3.11"\n'
        'dependencies=["plugin-core-dep==1.0"]\n[tool.uv]\npackage=false\nno-index=true\n'
        f'find-links=[{json.dumps(wheels.as_posix())}]\n', encoding="utf-8")
    result = subprocess.run([uv, "lock", "--python", sys.executable], cwd=world.core,
                            capture_output=True, text=True, timeout=60)
    assert result.returncode == 0, result.stderr
    return world