"""Contract tests for scripts/deploy/deploy-service.sh.

Why these live here: the deploy runner and the human whole-node deploy both go
through this script, and what must stay identical between them is the argv it
hands to `docker compose`. docker compose derives the project name from the
directory of the FIRST -f file — a caller that assembles those arguments
differently lands in a different project, where `--remove-orphans` tears down
the running stack (the 2026-06-25 control-plane wipe on VPS).

Each test runs the real script with a stub `docker` on PATH that records its
argv, so nothing is executed against a daemon.
"""
from __future__ import annotations

import os
import subprocess
from pathlib import Path

import pytest

REPO = Path(__file__).resolve().parents[3]
SCRIPT = REPO / "scripts" / "deploy" / "deploy-service.sh"


@pytest.fixture
def env(tmp_path: Path):
    """A fake repo + host dir + a `docker` stub that logs its arguments."""
    repo = tmp_path / "repo"
    (repo / "services" / "vikunja").mkdir(parents=True)
    (repo / "services" / "vikunja" / "docker-compose.yml").write_text("services: {}\n")
    host_dir = repo / "hosts" / "piha"
    host_dir.mkdir(parents=True)

    bindir = tmp_path / "bin"
    bindir.mkdir()
    argv_log = tmp_path / "docker-argv.txt"
    stub = bindir / "docker"
    stub.write_text(f'#!/usr/bin/env bash\nprintf "%s\\n" "$@" > {argv_log}\nexit 0\n')
    stub.chmod(0o755)

    return {"repo": repo, "host_dir": host_dir, "bindir": bindir, "argv_log": argv_log}


def run(env, *args, service="vikunja"):
    proc_env = dict(os.environ, PATH=f"{env['bindir']}:{os.environ['PATH']}")
    return subprocess.run(
        [str(SCRIPT), "--repo", str(env["repo"]), "--host-dir", str(env["host_dir"]),
         "--service", service, *args],
        capture_output=True, text=True, env=proc_env,
    )


def argv(env) -> list[str]:
    return env["argv_log"].read_text().splitlines()


def test_base_invocation(env):
    result = run(env)
    assert result.returncode == 0, result.stderr
    assert argv(env) == [
        "compose", "-f", str(env["repo"] / "services" / "vikunja" / "docker-compose.yml"),
        "up", "-d",
    ]


def test_project_name_invariant(env):
    """No -p and no --project-directory: the project name must come from the
    compose file's own directory, exactly as it does for a human deploy."""
    run(env, "--force-recreate")
    assert "-p" not in argv(env)
    assert "--project-directory" not in argv(env)
    assert argv(env)[1] == "-f"  # first -f is the service's own compose file


def test_override_is_second_f(env):
    override = env["host_dir"] / "runtime" / "vikunja"
    override.mkdir(parents=True)
    (override / "docker-compose.override.yml").write_text("services: {}\n")

    run(env)
    args = argv(env)
    assert args[1:5] == [
        "-f", str(env["repo"] / "services" / "vikunja" / "docker-compose.yml"),
        "-f", str(override / "docker-compose.override.yml"),
    ]


def test_env_file_is_passed_when_present(env):
    (env["repo"] / "services" / "vikunja" / ".env").write_text("FOO=bar\n")
    run(env)
    args = argv(env)
    assert "--env-file" in args
    assert args[args.index("--env-file") + 1] == str(env["repo"] / "services" / "vikunja" / ".env")


def test_force_recreate_flag(env):
    """The remediation path needs it: plain `up -d` is a no-op when config is
    unchanged, i.e. exactly the unhealthy-container case a redeploy targets."""
    run(env, "--force-recreate")
    assert "--force-recreate" in argv(env)


def test_no_remove_orphans_by_default(env):
    """The agent path must never be able to delete containers on a project-name
    mismatch — only the human whole-node deploy opts in."""
    run(env, "--force-recreate")
    assert "--remove-orphans" not in argv(env)


def test_remove_orphans_when_requested(env):
    run(env, "--remove-orphans")
    assert "--remove-orphans" in argv(env)


def test_build_if_needed_only_builds_with_dockerfile(env):
    run(env, "--build-if-needed")
    assert "--build" not in argv(env)

    (env["repo"] / "services" / "vikunja" / "Dockerfile").write_text("FROM scratch\n")
    run(env, "--build-if-needed")
    assert "--build" in argv(env)


def test_redeploy_path_never_builds(env):
    """The runner does not pass --build-if-needed: building on the 4 GiB VPS
    mid-incident is an OOM risk, and a redeploy is a reconcile, not a code ship."""
    (env["repo"] / "services" / "vikunja" / "Dockerfile").write_text("FROM scratch\n")
    run(env, "--force-recreate")
    assert "--build" not in argv(env)


def test_service_with_own_deploy_path_exits_3(env):
    (env["repo"] / "services" / "vikunja" / "deploy-local.sh").write_text("#!/bin/sh\n")
    result = run(env)
    assert result.returncode == 3
    assert not env["argv_log"].exists()  # docker never invoked


@pytest.mark.parametrize("bad", ["../../etc", "/abs", "Vikunja", "a b"])
def test_invalid_service_name_exits_2(env, bad):
    result = run(env, service=bad)
    assert result.returncode == 2
    assert not env["argv_log"].exists()


def test_missing_compose_file_exits_2(env):
    (env["repo"] / "services" / "vikunja" / "docker-compose.yml").unlink()
    result = run(env)
    assert result.returncode == 2
    assert not env["argv_log"].exists()
