"""`deploy/production.sh` refuses before it touches anything (Stream K7; plan W17). What it cost before:
`--seed-db` ran `pg_restore --clean --if-exists ... || true`, so a restore that failed halfway reported
success and left production half-replaced; nothing compared what arrived with what left. These run the
script's argument handling and the refusals that fire before any ssh, docker or database call, in
seconds, with no network and no container: the database half is proven by the rehearsal on dfp-devdb
(`.logs/runs/rehearsal-2026-09-17-k7-replace.md`).

The launch marker tests carry rian's rule: a push to live never syncs collection data again. The
replace moves the database once; after it, live is the machine that collects and the machine where
the catalogue is decided, so a second `--replace-db` would throw all of that away in the name of a
routine deploy. The marker makes that a typed flag rather than a habit, and the refusal fires before
step 1 and before any ssh."""
from __future__ import annotations

import os
import pathlib
import subprocess

import pytest

ROOT = pathlib.Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "deploy" / "production.sh"
DUMP_SCRIPT = ROOT / "deploy" / "launch-dump.sh"


def run(*args: str, script: pathlib.Path = SCRIPT) -> subprocess.CompletedProcess:
    # A host that cannot exist: were a refusal ever to fall through to ssh, it fails fast, never deploys.
    env = {**os.environ, "DFP_PROD_HOST": "nobody@invalid.invalid", "DFP_PG_READER": "no-such-container"}
    return subprocess.run(["bash", str(script), *args], capture_output=True, text=True, timeout=20, env=env, stdin=subprocess.DEVNULL)


@pytest.fixture
def dump(tmp_path) -> pathlib.Path:
    path = tmp_path / "staging.dump"
    path.write_bytes(b"not a real dump")
    for side, text in (("counts", "brands\t3\n"), ("head", "d2e3f4a5b6c7\n"), ("sources", "shopify-pty\tt\t1.00\tdeclared\t\n")):
        (tmp_path / f"staging.dump.{side}").write_text(text)
    return path


@pytest.fixture
def fake_root(tmp_path) -> pathlib.Path:
    """The script in a throwaway root. It does `cd "$(dirname "$0")/.."`, so `backups/production/`
    is this tmp tree's, never the repository's: a test can leave a LAUNCHED marker lying about
    without ever standing between rian and a real launch."""
    root = tmp_path / "root"
    (root / "deploy").mkdir(parents=True)
    (root / "deploy" / "production.sh").symlink_to(SCRIPT)
    return root


def launched(root: pathlib.Path, text: str = "launched 2026-09-21-030201 from backups/launch/staging-2026-09-21.dump\n") -> pathlib.Path:
    marker = root / "backups" / "production" / "LAUNCHED"
    marker.parent.mkdir(parents=True, exist_ok=True)
    marker.write_text(text)
    return marker


def test_the_scripts_parse():
    for script in (SCRIPT, DUMP_SCRIPT):
        assert subprocess.run(["bash", "-n", str(script)], capture_output=True).returncode == 0, script


def test_seed_db_is_gone_and_says_what_replaced_it():
    r = run("--seed-db", "backups/x.dump")
    assert r.returncode == 2 and "--seed-db is removed" in r.stderr and "--replace-db" in r.stderr


def test_an_unknown_argument_and_a_missing_value_are_refused():
    assert run("--frobnicate").returncode == 2
    r = run("--replace-db")
    assert r.returncode == 2 and "needs a dump file" in r.stderr


def test_local_is_only_a_rehearsal_of_the_replace_and_never_against_staging(dump):
    r = run("--local")
    assert r.returncode == 2 and "rehearses --replace-db only" in r.stderr
    r = run("--replace-db", str(dump), "--local")
    assert r.returncode == 2 and "would replace staging" in r.stderr, "the default container is staging's"
    r = run("--replace-db", str(dump), "--local", "--db-container", "dutyfreeprofessor-db", "--db", "dfp")
    assert r.returncode == 2 and "would replace staging" in r.stderr


def test_the_replace_and_caddy_are_separate_steps(dump):
    r = run("--replace-db", str(dump), "--caddy")
    assert r.returncode == 2 and "separate steps" in r.stderr


def test_a_missing_dump_or_sidecar_is_refused_before_anything_runs(dump, tmp_path):
    r = run("--replace-db", str(tmp_path / "nope.dump"), "--local", "--db-container", "dfp-devdb")
    assert r.returncode == 1 and "no dump at" in r.stderr
    for side in ("counts", "head", "sources"):
        (tmp_path / f"staging.dump.{side}").rename(tmp_path / f"hidden.{side}")
        r = run("--replace-db", str(dump), "--local", "--db-container", "dfp-devdb", "--app-exec", "true")
        assert r.returncode == 1 and f"staging.dump.{side} is missing" in r.stderr and "launch-dump.sh" in r.stderr
        (tmp_path / f"hidden.{side}").rename(tmp_path / f"staging.dump.{side}")


def test_a_rehearsal_needs_the_app_cli_on_its_own_database(dump):
    r = run("--replace-db", str(dump), "--local", "--db-container", "dfp-devdb", "--db", "dfp_prod_like")
    assert r.returncode == 1 and "--local needs --app-exec" in r.stderr


def test_a_dump_that_cannot_be_read_is_refused_and_nothing_is_dumped_or_dropped(dump):
    r = run("--replace-db", str(dump), "--local", "--db-container", "dfp-devdb", "--db", "dfp_prod_like", "--app-exec", "true", "--dry-run")
    assert r.returncode == 1 and "could not read" in r.stderr
    assert "pg_dump" not in r.stdout and "drop schema" not in r.stdout


def test_launch_dump_refuses_an_unknown_argument_and_documents_itself():
    assert run("--nope", script=DUMP_SCRIPT).returncode == 2
    r = run("--help", script=DUMP_SCRIPT)
    assert r.returncode == 0 and ".counts" in r.stdout and "production.sh --replace-db" in r.stdout


def test_the_launch_happens_once_and_a_second_replace_is_refused_before_step_1(fake_root, dump):
    marker = launched(fake_root)
    r = run("--replace-db", str(dump), script=fake_root / "deploy" / "production.sh")
    assert r.returncode == 1, r.stderr
    assert "backups/production/LAUNCHED" in r.stderr, "the refusal names the file rian has to move or force past"
    assert "--force-replace-db" in r.stderr and "discards every collection" in r.stderr
    assert "2026-09-21-030201" in r.stderr, "it prints the marker it found, so the date of the launch is on screen"
    assert "1. the dump" not in r.stdout, "before step 1: nothing is read, nothing is dumped, no ssh"
    assert marker.read_text().startswith("launched 2026-09-21-030201")


def test_a_dry_run_reports_the_marker_the_same_way_and_writes_none(fake_root, dump):
    script = fake_root / "deploy" / "production.sh"
    marker = launched(fake_root)
    r = run("--replace-db", str(dump), "--dry-run", script=script)
    assert r.returncode == 1 and "--force-replace-db" in r.stderr, "a dry run reports the marker, it does not rehearse past it"
    marker.unlink()
    r = run("--replace-db", str(dump), "--dry-run", script=script)
    assert r.returncode == 1 and "could not read" in r.stderr, "past the marker gate, into step 1"
    assert not marker.exists(), "a dry run never writes the marker; only a finished replace does"


def test_force_replace_db_passes_the_marker_and_leaves_it_as_it_was(fake_root, dump):
    marker = launched(fake_root)
    before = marker.read_text()
    r = run("--replace-db", str(dump), "--force-replace-db", script=fake_root / "deploy" / "production.sh")
    assert r.returncode == 1 and "could not read" in r.stderr, "it reached step 1, which is the point"
    assert "LAUNCHED" not in r.stderr and "1. the dump" in r.stdout
    assert marker.read_text() == before


def test_force_replace_db_on_its_own_does_nothing_and_says_so():
    r = run("--force-replace-db")
    assert r.returncode == 2 and "modifies --replace-db" in r.stderr


def test_a_rehearsal_is_never_blocked_by_the_marker(fake_root, dump):
    # --local replaces a scratch database on this machine, not live; the launch is irrelevant to it.
    launched(fake_root)
    r = run("--replace-db", str(dump), "--local", "--db-container", "dfp-devdb", "--db", "dfp_prod_like",
            "--app-exec", "true", script=fake_root / "deploy" / "production.sh")
    assert r.returncode == 1 and "could not read" in r.stderr and "LAUNCHED" not in r.stderr


def test_the_help_names_the_marker_and_the_force_flag():
    # The help is a sed line range over the header comment: adding to the comment without moving the
    # range silently truncates it, which is how a flag ends up documented nowhere.
    r = run("--help")
    assert r.returncode == 0
    assert "--force-replace-db" in r.stdout and "backups/production/LAUNCHED" in r.stdout
    assert "never syncs collection data again" in r.stdout
    assert "set -euo pipefail" not in r.stdout, "the range stops at the end of the header comment"
