"""autodrive.sh 런타임 smoke — Codex 3차 리뷰 합의.

근거: `declare -A`(bash3 비호환)는 `bash -n` 을 통과했고, 카운터
fail-open 도 happy-path 수동 smoke 를 통과했다 — 둘 다 Python 단위
시험 밖 **런타임** 결함이라, 실제 `/bin/bash` + 스텁 curl/psql 로
비용 안전 계약 4가지를 자동으로 잠근다.

  (1) 카운터 저장 실패 = fail-closed 즉시 정지 (상한 소멸 금지)
  (2) 401 응답은 유료 시도로 세지 않는다
  (3) HTTP 2xx + status=started 만 계수, 3회 상한 후 정지
  (4) 복수 failed/partial 은 파이프라인 순서의 첫 하나만 POST
"""
from __future__ import annotations

import json
import os
import stat
import subprocess
from pathlib import Path

BACKEND = Path(__file__).resolve().parents[2]
SCRIPT = BACKEND / "autodrive.sh"


def _steps_json(steps):
    return json.dumps({"steps": steps})


def _make_stub_bin(tmp_path: Path, steps_payload: str, resume_body: str,
                   resume_code: str) -> Path:
    """PATH 앞에 둘 스텁 bin — curl 은 URL 로 분기해 기록·응답한다."""
    bin_dir = tmp_path / "stubbin"
    bin_dir.mkdir()
    post_log = tmp_path / "posts.log"
    curl = bin_dir / "curl"
    curl.write_text(f"""#!/bin/bash
args="$*"
case "$args" in
  *auth/login*) exit 0;;
  *mode=resume*)
    echo "$args" >> "{post_log}"
    printf '%s\\n%s' '{resume_body}' '{resume_code}'
    exit 0;;
  *steps*)
    printf '%s' '{steps_payload}'
    exit 0;;
  *) exit 0;;
esac
""")
    curl.chmod(curl.stat().st_mode | stat.S_IEXEC)
    psql = bin_dir / "psql"
    psql.write_text("#!/bin/bash\nexit 0\n")
    psql.chmod(psql.stat().st_mode | stat.S_IEXEC)
    return bin_dir


def _run(tmp_path: Path, bin_dir: Path, cnt_dir: str, timeout: float):
    env = dict(os.environ)
    env.update({
        "PATH": f"{bin_dir}:{env['PATH']}",
        "P": "smokeP", "E": "smokeE", "CAP": "1",
        "SLEEP": "0.05", "CNT_DIR": cnt_dir,
    })
    try:
        proc = subprocess.run(
            ["/bin/bash", str(SCRIPT)], env=env, cwd=str(BACKEND),
            capture_output=True, text=True, timeout=timeout)
        return proc.returncode, proc.stdout + proc.stderr, False
    except subprocess.TimeoutExpired as exc:
        out = (exc.stdout or b"")
        err = (exc.stderr or b"")
        if isinstance(out, bytes):
            out = out.decode("utf-8", "ignore")
        if isinstance(err, bytes):
            err = err.decode("utf-8", "ignore")
        return None, out + err, True


FAILED_ONE = _steps_json([
    {"step_id": "stepA", "status": "failed", "order": 1},
    {"step_id": "done1", "status": "completed", "order": 0},
])
FAILED_TWO = _steps_json([
    {"step_id": "stepB", "status": "partial", "order": 2},
    {"step_id": "stepA", "status": "failed", "order": 1},
    {"step_id": "done1", "status": "completed", "order": 0},
])
STARTED = '{"ok":true,"status":"started"}'


def test_1_counter_dir_failure_is_fail_closed(tmp_path):
    bin_dir = _make_stub_bin(tmp_path, FAILED_ONE, STARTED, "200")
    rc, out, timed_out = _run(
        tmp_path, bin_dir, cnt_dir="/dev/null/nope", timeout=10)
    assert not timed_out and rc == 1, f"즉시 정지해야 함: rc={rc}\n{out}"
    assert "카운터 디렉토리 생성 실패" in out


def test_2_http_401_is_not_counted(tmp_path):
    bin_dir = _make_stub_bin(tmp_path, FAILED_ONE, '{"detail":"auth"}', "401")
    cnt_dir = tmp_path / "cnt"
    rc, out, timed_out = _run(tmp_path, bin_dir, str(cnt_dir), timeout=2)
    assert timed_out, "401 은 상한을 소모하지 않아 계속 재시도해야 함"
    assert "카운트 제외" in out or "재로그인" in out
    cnts = list(cnt_dir.glob("*.cnt"))
    assert cnts == [], f"401 에서 카운터가 생기면 안 됨: {cnts}"


def test_3_started_counts_and_cap_stops_at_three(tmp_path):
    bin_dir = _make_stub_bin(tmp_path, FAILED_ONE, STARTED, "200")
    cnt_dir = tmp_path / "cnt"
    rc, out, timed_out = _run(tmp_path, bin_dir, str(cnt_dir), timeout=15)
    assert not timed_out and rc == 1, f"3회 후 정지해야 함: rc={rc}\n{out}"
    assert "회수 불가" in out and "3회" in out
    cnt = (cnt_dir / "smokeP.smokeE.stepA.cnt").read_text().strip()
    assert cnt == "3", f"started 3회만 계수돼야 함: {cnt}"
    posts = (tmp_path / "posts.log").read_text().splitlines()
    assert len(posts) == 3, f"POST 도 3회여야 함: {len(posts)}"


def test_4_multiple_fp_resumes_first_in_pipeline_order_only(tmp_path):
    bin_dir = _make_stub_bin(tmp_path, FAILED_TWO, STARTED, "200")
    cnt_dir = tmp_path / "cnt"
    rc, out, timed_out = _run(tmp_path, bin_dir, str(cnt_dir), timeout=15)
    assert not timed_out and rc == 1
    posts = (tmp_path / "posts.log").read_text()
    assert "stepA" in posts and "stepB" not in posts, (
        f"order 첫 스텝(stepA)만 POST 돼야 함:\n{posts}"
    )


def test_1b_existing_cnt_unreadable_or_empty_is_fail_closed(tmp_path):
    """Codex 4차: 부재만 0 — **존재하는** cnt 파일이 디렉터리/빈 파일이면
    0 으로 세탁하지 않고 POST 0회 + 즉시 정지해야 한다(상한 리셋 금지)."""
    for kind in ("directory", "empty"):
        sub = tmp_path / kind
        sub.mkdir()
        bin_dir = _make_stub_bin(sub, FAILED_ONE, STARTED, "200")
        cnt_dir = sub / "cnt"
        cnt_dir.mkdir()
        bad = cnt_dir / "smokeP.smokeE.stepA.cnt"
        if kind == "directory":
            bad.mkdir()
        else:
            bad.write_text("")
        rc, out, timed_out = _run(sub, bin_dir, str(cnt_dir), timeout=10)
        assert not timed_out and rc == 1, (
            f"[{kind}] 즉시 정지해야 함: rc={rc}\n{out}")
        assert "상한을 보장할 수 없어" in out, f"[{kind}]\n{out}"
        posts = sub / "posts.log"
        assert not posts.exists() or posts.read_text() == "", (
            f"[{kind}] 유료 POST 가 없어야 함")
