# -*- coding: utf-8 -*-
"""task-2735 — finish-task terminal callback PATH fix 회귀 테스트.

문제(진단 확정):
  finish-task.sh cleanup_timer 의 terminal_state_callback emit 호출이
  ① PYTHONPATH 미설정 → 모듈 top-level `from dispatch... import` 가
     ModuleNotFoundError → emit 미작동.
  ② `2>/dev/null || true` 로 실패를 완전 은폐(observability 0).

fix 계약(회장 허용 범위 §3):
  (A) 호출 시 PYTHONPATH="$WORKSPACE" 보장.
  (B) stderr 를 silent 폐기하지 않고 events 로그로 보존 + 실패 marker 박제.
  (C) terminal callback 실패가 finish-task 전체 실패를 유발하지 않음(비차단).
  (D) 기존 성공 경로 무손상(envelope 정상 생성).

본 테스트는 (1) finish-task.sh 소스 정적 계약, (2) 실제
terminal_state_callback.py 의 PYTHONPATH 유무별 동작, (3) fix 블록의
관측성+비차단 동작(충실 재현) 을 검증한다.
"""
from __future__ import annotations

import os
import re
import subprocess
import sys
from pathlib import Path

import pytest

_ROOT = Path(__file__).resolve().parent.parent.parent
_FINISH_TASK = _ROOT / "scripts" / "finish-task.sh"
_TSC = _ROOT / "scripts" / "harness" / "v36" / "terminal_state_callback.py"


# ── 소스 추출 헬퍼: cleanup_timer 의 terminal callback 블록 ─────────────────────
def _finish_task_src() -> str:
    return _FINISH_TASK.read_text(encoding="utf-8")


def _cleanup_timer_block() -> str:
    """cleanup_timer() 함수 본문 텍스트를 반환."""
    src = _finish_task_src()
    start = src.index("cleanup_timer() {")
    # 함수 끝: 다음 최상위 '}\n' 까지 (단순하지만 충분 — 본 블록은 단일 함수)
    end = src.index("\n}", start)
    return src[start:end]


# ──────────────────────────────────────────────────────────────────────────────
# 1. 정적 계약 — finish-task.sh 소스
# ──────────────────────────────────────────────────────────────────────────────

def test_finish_task_exists():
    assert _FINISH_TASK.exists(), "scripts/finish-task.sh 존재해야 함"


def _emit_invocation_lines(block: str) -> list:
    """실제 python3 ... terminal_state_callback.py emit 호출 라인만 추출
    (JSON reason 문자열을 담은 printf 라인은 제외)."""
    return [
        ln for ln in block.splitlines()
        if "python3" in ln and "terminal_state_callback.py" in ln and "emit --task-id" in ln
    ]


def test_terminal_callback_invocation_sets_pythonpath():
    """(A) terminal_state_callback emit 호출이 PYTHONPATH="$WORKSPACE" 를 보장."""
    block = _cleanup_timer_block()
    emit_lines = _emit_invocation_lines(block)
    assert emit_lines, "terminal_state_callback emit 호출 라인이 있어야 함"
    for ln in emit_lines:
        assert 'PYTHONPATH="$WORKSPACE"' in ln, (
            f"emit 호출 라인에 PYTHONPATH=\"$WORKSPACE\" 보장 필요: {ln!r}"
        )


def test_terminal_callback_stderr_not_silently_discarded():
    """(B) emit 호출이 stderr 를 /dev/null 로 silent 폐기하지 않음."""
    block = _cleanup_timer_block()
    emit_lines = _emit_invocation_lines(block)
    assert emit_lines
    for ln in emit_lines:
        assert "2>/dev/null" not in ln, (
            f"terminal callback emit 의 stderr 를 /dev/null 로 폐기하면 안 됨: {ln!r}"
        )
        # stderr 가 관측 가능한 로그 파일 변수($_tcb_err)로 라우팅되어야 함
        assert '2>"$_tcb_err"' in ln, (
            f"stderr 를 관측 가능한 로그 파일($_tcb_err)로 보존해야 함: {ln!r}"
        )
    # _tcb_err 가 .log 경로로 정의되어 있어야 함
    assert re.search(r'_tcb_err="[^"]*\.log"', block), "stderr 로그 변수는 .log 경로여야 함"


def test_terminal_callback_failure_marker_written():
    """(B) 실패 시 ${TASK_ID}.terminal-callback-failed.json marker 박제."""
    block = _cleanup_timer_block()
    assert "terminal-callback-failed.json" in block, (
        "실패를 관측 가능한 marker(${TASK_ID}.terminal-callback-failed.json)로 기록해야 함"
    )
    assert "TERMINAL_CALLBACK_FAILED" in block


def test_terminal_callback_is_non_blocking():
    """(C) 블록이 finish-task 를 실패시키지 않음(비차단): exit 비-0 도입 없음."""
    block = _cleanup_timer_block()
    # cleanup_timer 의 terminal callback 처리에서 'exit 1' 등 강제 종료가 없어야 함
    assert not re.search(r"\bexit\s+[1-9]", block), (
        "terminal callback 처리는 비차단이어야 하며 finish-task 를 강제 종료하면 안 됨"
    )
    # 실패 marker write 는 || true 로 비차단 보장
    assert "terminal-callback-failed.json\" 2>/dev/null || true" in block or \
        re.search(r"terminal-callback-failed\.json.*\|\|\s*true", block), (
        "실패 marker write 가 비차단(|| true)이어야 함"
    )


# ──────────────────────────────────────────────────────────────────────────────
# 2. 실제 terminal_state_callback.py — PYTHONPATH 유무별 동작
# ──────────────────────────────────────────────────────────────────────────────

def test_emit_with_pythonpath_produces_envelope(tmp_path):
    """(D) PYTHONPATH 보장 시 import 성공 → terminal-state envelope 생성, exit 0.

    --dry-run 으로 실제 cron 등록 backend 를 우회(실 cron 발사 0).
    """
    events = tmp_path / "events"
    events.mkdir()
    done = tmp_path / "none.done"  # 부재 → UNKNOWN_FINISH_FAILURE
    proc = subprocess.run(
        [sys.executable, str(_TSC), "emit",
         "--task-id", "pp-1", "--events-dir", str(events),
         "--workspace", str(_ROOT), "--done-file", str(done), "--dry-run"],
        cwd=str(_ROOT),
        env={**os.environ, "PYTHONPATH": str(_ROOT)},
        capture_output=True, text=True, timeout=60,
    )
    assert proc.returncode == 0, f"PYTHONPATH 보장 시 exit 0 이어야 함. stderr={proc.stderr}"
    envelope = events / "pp-1.terminal-state.json"
    assert envelope.exists(), "import 성공 시 terminal-state envelope 가 생성되어야 함"


def test_emit_without_pythonpath_reproduces_bug(tmp_path):
    """(진단 박제) PYTHONPATH 미설정 → ModuleNotFoundError, envelope 미생성.

    이것이 fix 가 해소하는 결함이다(현행 L284 의 silent 은폐 대상).
    """
    events = tmp_path / "events"
    events.mkdir()
    done = tmp_path / "none.done"
    clean_env = {k: v for k, v in os.environ.items() if k != "PYTHONPATH"}
    proc = subprocess.run(
        [sys.executable, str(_TSC), "emit",
         "--task-id", "np-1", "--events-dir", str(events),
         "--workspace", str(_ROOT), "--done-file", str(done)],
        cwd=str(tmp_path),  # WS 가 아닌 곳 → dispatch 미해결
        env=clean_env,
        capture_output=True, text=True, timeout=60,
    )
    assert proc.returncode != 0, "PYTHONPATH 미설정 시 import 실패로 비-0 종료여야 함"
    assert "dispatch" in proc.stderr, f"ModuleNotFoundError(dispatch) 가 stderr 에 나와야 함: {proc.stderr}"
    assert not (events / "np-1.terminal-state.json").exists(), "버그 조건에서는 envelope 미생성"


# ──────────────────────────────────────────────────────────────────────────────
# 3. fix 블록 관측성 + 비차단 (충실 재현)
# ──────────────────────────────────────────────────────────────────────────────

# finish-task.sh cleanup_timer 의 terminal callback 블록을 그대로 재현한 셸 스니펫.
# (실제 finish-task.sh 와 동일 의미 — PYTHONPATH 보장 / stderr 로그 보존 /
#  실패 marker 박제 / 비차단). 본 스니펫의 비차단·관측성을 직접 검증한다.
_FIX_SNIPPET = r'''
set -u
cleanup_terminal_callback() {
    if [ "${TERMINAL_CALLBACK_ENABLED:-0}" = "1" ]; then
        _tcb_err="$EVENTS_DIR/${TASK_ID}.terminal-callback-stderr.log"
        if ( cd "$WORKSPACE" && PYTHONPATH="$WORKSPACE" python3 "$TSC_PATH" emit --task-id "$TASK_ID" --events-dir "$EVENTS_DIR" --workspace "$WORKSPACE" --done-file "$DONE_FILE" ${TSC_EXTRA:-} 2>"$_tcb_err" ); then
            rm -f "$_tcb_err" "$EVENTS_DIR/${TASK_ID}.terminal-callback-failed.json" 2>/dev/null || true
        else
            printf '{"status":"TERMINAL_CALLBACK_FAILED","task_id":"%s","reason":"terminal_state_callback.py emit returned non-zero (import/interpreter failure) — non-blocking","stderr_log":"%s","ts":%s}\n' \
                "$TASK_ID" "$_tcb_err" "$(date +%s)" > "$EVENTS_DIR/${TASK_ID}.terminal-callback-failed.json" 2>/dev/null || true
        fi
    fi
}
cleanup_terminal_callback
echo "POST_CALLBACK_REACHED rc=$?"
'''


def _run_snippet(env_overrides: dict) -> subprocess.CompletedProcess:
    env = {**os.environ, **env_overrides}
    return subprocess.run(["bash", "-c", _FIX_SNIPPET], env=env,
                          capture_output=True, text=True, timeout=60)


def test_failure_is_observable_and_non_blocking(tmp_path):
    """실패 시: (B) marker 박제 관측 가능 + (C) 비차단(후속 실행 도달, rc 0)."""
    events = tmp_path / "events"
    events.mkdir()
    proc = _run_snippet({
        "TERMINAL_CALLBACK_ENABLED": "1",
        "WORKSPACE": str(tmp_path),
        "TASK_ID": "fail-1",
        "EVENTS_DIR": str(events),
        "DONE_FILE": str(tmp_path / "none.done"),
        # 존재하지 않는 스크립트 경로 → python3 비-0 종료 강제(=import/interpreter 실패 등가)
        "TSC_PATH": str(tmp_path / "does_not_exist.py"),
    })
    # (C) 비차단: 콜백 실패에도 후속 라인 도달 + cleanup 함수가 비-0 전파 안 함
    assert "POST_CALLBACK_REACHED rc=0" in proc.stdout, (
        f"terminal callback 실패가 비차단이어야 함(후속 도달·rc 0). stdout={proc.stdout} stderr={proc.stderr}"
    )
    # (B) 관측 가능: 실패 marker 박제
    marker = events / "fail-1.terminal-callback-failed.json"
    assert marker.exists(), "실패 시 terminal-callback-failed.json marker 가 생성되어야 함"
    assert "TERMINAL_CALLBACK_FAILED" in marker.read_text(encoding="utf-8")
    # stderr 로그도 보존(관측 가능)
    assert (events / "fail-1.terminal-callback-stderr.log").exists()


def test_success_path_clean_and_no_failure_marker(tmp_path):
    """(D) 성공 경로: envelope 생성 + 실패 marker 미생성 + stale marker 정리."""
    events = tmp_path / "events"
    events.mkdir()
    # stale 실패 marker 선존치 — 성공 시 제거되어야 함
    stale = events / "ok-1.terminal-callback-failed.json"
    stale.write_text("{}", encoding="utf-8")
    proc = _run_snippet({
        "TERMINAL_CALLBACK_ENABLED": "1",
        "WORKSPACE": str(_ROOT),
        "TASK_ID": "ok-1",
        "EVENTS_DIR": str(events),
        "DONE_FILE": str(tmp_path / "none.done"),
        "TSC_PATH": str(_TSC),
        # 테스트 안전: --dry-run 으로 실제 cron 등록 backend 우회(실 cron 발사 0).
        # import 성공·envelope 생성·exit 0 만 검증(성공 경로 무손상).
        "TSC_EXTRA": "--dry-run",
        "PYTHONPATH": str(_ROOT),
    })
    assert "POST_CALLBACK_REACHED rc=0" in proc.stdout, f"성공 경로 rc 0. {proc.stdout} {proc.stderr}"
    assert (events / "ok-1.terminal-state.json").exists(), "성공 시 envelope 생성"
    assert not stale.exists(), "성공 시 stale 실패 marker 가 정리되어야 함"
    assert not (events / "ok-1.terminal-callback-failed.json").exists(), "성공 시 실패 marker 미생성"


if __name__ == "__main__":
    raise SystemExit(pytest.main([__file__, "-v"]))
