"""Exercise the Termux sequencing boundary with real child executables."""
from __future__ import annotations

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

import pytest
from ruamel.yaml import YAML


ROOT = Path(__file__).resolve().parents[2]
DRIVER = ROOT / "scripts/termux/build.py"
pytestmark = pytest.mark.platforms("posix")


@pytest.fixture
def build_fixture(tmp_path):
    repo = tmp_path / "source with spaces"
    payload = tmp_path / "prepared payload"
    out = tmp_path / "deb output"
    tools = tmp_path / "tools"
    tools.mkdir()
    payload.mkdir()
    for name in ("scripts/build/node-deps.mjs", "scripts/build/tui.mjs", "scripts/termux/build_deb.sh"):
        script = repo / name
        script.parent.mkdir(parents=True, exist_ok=True)
        script.touch()
    log = tmp_path / "calls.jsonl"
    recorder = f"""#!{sys.executable}
import json
import os
from pathlib import Path
import sys

args = sys.argv[1:]
assert Path(args[0]).is_file(), args
stage = Path(args[0]).name
with Path(os.environ['CALL_LOG']).open('a') as stream:
    stream.write(json.dumps({{'tool': Path(sys.argv[0]).name, 'args': args,
        'cwd': os.getcwd(), 'image': os.environ.get('TERMUX_BUILDER_IMAGE')}}) + '\\n')
if stage == os.environ.get('FAIL_STAGE'):
    sys.exit(37)
if stage == 'tui.mjs':
    product = Path(args[args.index('--out') + 1])
    (product / 'dist').mkdir(parents=True, exist_ok=True)
    (product / 'dist/entry.js').write_text('fixture TUI product')
    (product / 'package.json').write_text('{{"type":"module"}}')
if stage == 'build_deb.sh':
    product = Path(args[args.index('--tui-product') + 1])
    assert (product / 'dist/entry.js').read_text() == 'fixture TUI product'
    destination = Path(args[args.index('--out') + 1])
    destination.mkdir(parents=True)
    (destination / 'assembly-received-product').write_text(str(product))
"""
    for name in ("node", "bash"):
        tool = tools / name
        tool.write_text(recorder, encoding="utf-8")
        tool.chmod(0o755)
    env = dict(os.environ, PATH=f"{tools}{os.pathsep}{os.environ['PATH']}",
               CALL_LOG=str(log), TERMUX_BUILDER_IMAGE="fixture-pinned-image")
    return repo, payload, out, log, env


def invoke(build_fixture, *identity, extra=()):
    repo, payload, out, _, env = build_fixture
    return subprocess.run(
        [sys.executable, str(DRIVER), "--repo", str(repo), "--payload", str(payload),
         "--out", str(out), *identity, *extra],
        cwd=repo.parent, env=env, capture_output=True, text=True, timeout=30,
    )


def recorded_calls(log):
    return [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []


@pytest.mark.parametrize("identity", [("--tag", "v1.2.3"), ("--commit", "a" * 40)])
def test_driver_prepares_only_tui_then_passes_product_to_deb(build_fixture, identity):
    repo, payload, out, log, _ = build_fixture
    result = invoke(build_fixture, *identity)
    assert result.returncode == 0, result.stdout + result.stderr
    product = repo / ".build/termux/tui"
    assert recorded_calls(log) == [
        {"tool": "node", "args": [str(repo / "scripts/build/node-deps.mjs"),
          "--source", str(repo), "--workspace", "ui-tui"],
         "cwd": str(repo), "image": "fixture-pinned-image"},
        {"tool": "node", "args": [str(repo / "scripts/build/tui.mjs"),
          "--source", str(repo), "--out", str(product)],
         "cwd": str(repo), "image": "fixture-pinned-image"},
        {"tool": "bash", "args": [str(repo / "scripts/termux/build_deb.sh"),
          "--repo", str(repo), "--payload", str(payload), "--out", str(out),
          "--tui-product", str(product), *identity],
         "cwd": str(repo), "image": "fixture-pinned-image"},
    ]
    assert (out / "assembly-received-product").read_text() == str(product)


@pytest.mark.parametrize("identity", [
    (), ("--tag", "v1.2.3", "--commit", "a" * 40),
    ("--ta", "v1.2.3"), ("--commit", "short"),
    ("--commit", "A" * 40), ("--tag", ""), ("--tag", "latest"),
    ("--tag", "v1.2.3", "--skip-build"),
])
def test_invalid_cli_never_starts_a_build(build_fixture, identity):
    _, payload, out, log, _ = build_fixture
    result = invoke(build_fixture, *identity)
    assert result.returncode == 2, result.stdout + result.stderr
    assert "error:" in result.stderr
    assert recorded_calls(log) == []
    assert not (payload / ".work").exists()
    assert not out.exists()


@pytest.mark.parametrize("missing", ["--repo", "--payload", "--out"])
def test_all_paths_are_explicit(build_fixture, missing):
    repo, payload, out, log, env = build_fixture
    paths = {"--repo": repo, "--payload": payload, "--out": out}
    args = [value for option, path in paths.items() if option != missing
            for value in (option, str(path))]
    result = subprocess.run([sys.executable, str(DRIVER), *args, "--tag", "v1.2.3"],
                            cwd=repo.parent, env=env, capture_output=True, text=True, timeout=30)
    assert result.returncode == 2
    assert missing in result.stderr
    assert recorded_calls(log) == []


def test_missing_prepared_payload_is_rejected_before_frontend_work(build_fixture):
    _, payload, out, log, _ = build_fixture
    payload.rmdir()
    result = invoke(build_fixture, "--tag", "v1.2.3")
    assert result.returncode == 2, result.stdout + result.stderr
    assert "--payload" in result.stderr
    assert recorded_calls(log) == []
    assert not payload.exists()
    assert not out.exists()


@pytest.mark.parametrize("stage, reached", [
    ("node-deps.mjs", ["node-deps.mjs"]),
    ("tui.mjs", ["node-deps.mjs", "tui.mjs"]),
    ("build_deb.sh", ["node-deps.mjs", "tui.mjs", "build_deb.sh"]),
])
def test_child_failure_stops_sequence_even_with_stale_product(build_fixture, stage, reached):
    repo, payload, out, log, env = build_fixture
    stale = repo / ".build/termux/tui/dist/entry.js"
    stale.parent.mkdir(parents=True)
    stale.write_text("stale product")
    env["FAIL_STAGE"] = stage
    result = invoke(build_fixture, "--tag", "v1.2.3")
    assert result.returncode == 37, result.stdout + result.stderr
    assert [Path(call["args"][0]).name for call in recorded_calls(log)] == reached
    assert not out.exists()


@pytest.mark.parametrize("identity", [("--tag", "v1.2.3"), ("--commit", "a" * 40)])
def test_release_workflow_runs_the_shared_sequence(build_fixture, identity):
    repo, payload, _, log, env = build_fixture
    (repo / "scripts/termux/build.py").symlink_to(DRIVER)
    (repo / "termux-build").mkdir()
    (repo / "termux-build/payload").symlink_to(payload, target_is_directory=True)
    tools = log.parent / "tools"
    (tools / "python3").symlink_to(sys.executable)
    env.update(HERMES_BUILD_COMMIT=identity[1] if identity[0] == "--commit" else "",
               HERMES_PAYLOAD_TAG=identity[1] if identity[0] == "--tag" else "")
    workflow = YAML(typ="safe").load((ROOT / ".github/workflows/desktop-bundled-release.yml").read_text())
    step = next(step for step in workflow["jobs"]["termux-deb"]["steps"]
                if step.get("name") == "Assemble the .deb")
    bash = shutil.which("bash")
    assert bash is not None
    result = subprocess.run([bash, "-euo", "pipefail", "-c", step["run"]],
                            cwd=repo, env=env, capture_output=True, text=True, timeout=30)
    assert result.returncode == 0, result.stdout + result.stderr
    calls = recorded_calls(log)
    assert [Path(call["args"][0]).name for call in calls] == [
        "node-deps.mjs", "tui.mjs", "build_deb.sh",
    ]
    assert calls[-1]["args"][-2:] == list(identity)
    assert (repo / "termux-build/deb/assembly-received-product").read_text() == str(
        repo / ".build/termux/tui")


@pytest.mark.parametrize("changed_input", ["Dockerfile", "base"])
def test_builder_image_identity_covers_all_inputs(tmp_path, changed_input):
    repo = tmp_path / "source with spaces"
    scripts = repo / "scripts/termux"
    scripts.mkdir(parents=True)
    for name in ("build_builder_image.sh", "termux-builder.Dockerfile"):
        shutil.copy2(ROOT / "scripts/termux" / name, scripts / name)
    shutil.copytree(ROOT / "pm", repo / "pm", ignore=shutil.ignore_patterns("__pycache__"))
    tools = tmp_path / "tools"
    tools.mkdir()
    (tools / "python3").symlink_to(sys.executable)
    # Only inspect an already-published image; any build/push call is a failure.
    command = 'docker() { [[ "$1 $2" == "manifest inspect" ]]; }; export -f docker; bash "$1"'
    env = {**os.environ, "PATH": f"{tools}{os.pathsep}{os.environ['PATH']}",
           "GITHUB_REPOSITORY_OWNER": "FiXtUrE"}

    def image():
        result = subprocess.run(["bash", "-c", command, "fixture", str(scripts / "build_builder_image.sh")],
                                env=env, capture_output=True, text=True, timeout=30)
        assert result.returncode == 0, result.stderr
        return result.stdout.splitlines()[-1]

    original = image()
    assert original.startswith("ghcr.io/fixture/hermes-termux-builder:")
    assert image() == original
    changed = scripts / "termux-builder.Dockerfile" if changed_input == "Dockerfile" else repo / "pm/lock.json"
    before = changed.read_bytes()
    if changed_input == "Dockerfile":
        changed.write_bytes(before + b"\nRUN apt install -y git\n")
    else:
        pin = json.loads(before)
        digest = pin["packages"]["termux-docker"]["version"]
        pin["packages"]["termux-docker"]["version"] = digest[:-1] + ("a" if digest[-1] != "a" else "b")
        changed.write_text(json.dumps(pin))
    assert image() != original
    changed.write_bytes(before)
    assert image() == original
