"""Real artifacts and isolated PM workers shared by lifecycle tests."""
from __future__ import annotations

from functools import partial
from contextlib import contextmanager
import hashlib
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
import importlib
import io
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tarfile
import threading
import zipfile

import pytest


def _wheel(directory: Path, name: str, version: str = "1.0", requirements=()) -> Path:
    metadata = f"{name}-{version}.dist-info"
    entries = {
        f"{name}/__init__.py": f"__version__ = {version!r}\n",
        f"{metadata}/METADATA": f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n"
        + "".join(f"Requires-Dist: {requirement}\n" for requirement in requirements),
        f"{metadata}/WHEEL": "Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
    }
    entries[f"{metadata}/RECORD"] = "".join(f"{path},,\n" for path in entries)
    wheel = directory / f"{name}-{version}-py3-none-any.whl"
    with zipfile.ZipFile(wheel, "w") as archive:
        for path, body in entries.items():
            archive.writestr(path, body)
    return wheel


def stage_host_python(python: Path) -> Path:
    """Place a runnable copy of the test host's interpreter at ``python`` (``<root>/bin/python…``).

    CI's test interpreter is a relocatable python-build-standalone whose prefix is wherever the
    binary sits, so a bare copy cannot find its stdlib (``No module named 'encodings'``). Copy the
    host's stdlib beside it (a real copy, not a link: payload guards refuse links that escape the
    tree); site-packages and the test suite stay out. Production's package stage ships the whole
    distribution.
    """
    python.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(Path(sys._base_executable).resolve(), python)
    host_lib = Path(sys.base_prefix) / "lib"
    stdlib = next((d for d in host_lib.glob("python3.*") if (d / "os.py").is_file()), None)
    if stdlib is not None and not (python.parent.parent / "lib" / stdlib.name).exists():
        shutil.copytree(stdlib, python.parent.parent / "lib" / stdlib.name,
                        ignore=shutil.ignore_patterns("site-packages", "test", "__pycache__", "idlelib", "tkinter"),
                        symlinks=True)
    return python


def _run(command, *, cwd: Path, env: dict) -> str:
    result = subprocess.run(command, cwd=cwd, env=env, capture_output=True, text=True, timeout=60)
    assert result.returncode == 0, result.stderr
    return result.stdout.strip()


def _ar_member(name: str, data: bytes) -> bytes:
    hdr = (
        name.ljust(16).encode()
        + b"0".ljust(12)
        + b"0".ljust(6)
        + b"0".ljust(6)
        + b"100644".ljust(8)
        + str(len(data)).encode().ljust(10)
        + b"`\n"
    )
    pad = b"\n" if len(data) % 2 else b""
    return hdr + data + pad



def make_tar(docroot: Path, name: str, files: dict[str, str]) -> tuple[str, str]:
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w:gz") as tf:
        for rel, content in files.items():
            data = content.encode()
            info = tarfile.TarInfo(rel)
            info.size = len(data)
            info.mode = 0o755
            tf.addfile(info, io.BytesIO(data))
    payload = buf.getvalue()
    (docroot / name).write_bytes(payload)
    return name, hashlib.sha256(payload).hexdigest()


@contextmanager
def threaded_server(handler):
    with ThreadingHTTPServer(("127.0.0.1", 0), handler) as server:
        server.daemon_threads = True
        thread = threading.Thread(target=server.serve_forever, daemon=True)
        thread.start()
        try:
            yield server
        finally:
            server.shutdown()
            thread.join(timeout=5)


@pytest.fixture
def served(tmp_path):
    docroot = tmp_path / "www"
    docroot.mkdir()
    handler = partial(SimpleHTTPRequestHandler, directory=str(docroot))
    with threaded_server(handler) as server:
        yield docroot, f"http://127.0.0.1:{server.server_port}"


@pytest.fixture(scope="module")
def isolated_python(tmp_path_factory):
    from pm.runtime_stage import stage_runtime

    root = tmp_path_factory.mktemp("pm-python")
    uv = shutil.which("uv")
    assert uv, "the worker contract requires real uv"
    python = stage_runtime(Path(uv), Path(sys.executable), root)
    _run([str(python), "-I", "-c", "import importlib.util; assert importlib.util.find_spec('yaml') is None"],
         cwd=root, env=dict(os.environ))
    return python


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

    client = importlib.import_module("pm.client")
    monkeypatch.setattr("pm.runtime.runtime_python", lambda **kwargs: isolated_python)
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
    monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "store"))
    monkeypatch.setattr(paths, "lockfile_path", lambda: tmp_path / "lock.json")
    return client


def worker_toolchain(client, monkeypatch, isolated_python, injection=""):
    uv = shutil.which("uv")
    assert uv
    worker = Path(client.__file__).with_name("worker.py")
    script = (
        "import runpy, sys, os; 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({sys.executable!r}));\n"
        + injection + f"\nrunpy.run_path({str(worker)!r}, run_name='__main__')"
    )
    monkeypatch.setattr(client, "runtime_command", lambda path, **kwargs: [str(isolated_python), "-I", "-B", "-c", script])



@pytest.fixture
def build_worker(client, isolated_python, monkeypatch):
    """Inject prepared real tools only inside the independent worker process."""
    worker_toolchain(client, monkeypatch, isolated_python)
    monkeypatch.setattr(client, "is_runtime", lambda: False)

    def caller_engine(*args, **kwargs):
        pytest.fail("dependency engine ran in the caller, not the PM worker")

    for module, names in {
        "pm.operations": ("build_environment", "lock_project"),
        "pm.build_operations": ("build_requirements_environment", "export_requirements", "check_project_lock"),
    }.items():
        for name in names:
            monkeypatch.setattr(importlib.import_module(module), name, caller_engine)
    return client