"""
task-2576 TODO-3 신규 회귀: stash_audit.py invocation failure 시 fail-stop (sys.exit(1)) 보장.

배경:
- PR #125 (attempt-3) 에서 Gemini HIGH `:1196` finding 발견:
  `if res.returncode != 0 or not res.stdout.strip(): sys.exit(0)`
  → audit 실패를 success로 swallow하여 spec §3.2.2.3 fail-stop 위반.
- task-2576 TODO-3: sys.exit(0) → sys.exit(1) + `if debug:` 가드 제거 + FAIL-STOP 로그.

검증:
1. 정적: sys.exit(0) 회귀 0건, sys.exit(1) 존재, FAIL-STOP 로그 존재.
2. 동적: heredoc 추출 + audit 실패 시뮬레이션 후 non-zero exit + 메시지 확인.
"""
from __future__ import annotations

import re
import subprocess
import sys
import textwrap
from pathlib import Path

import pytest

WORKSPACE = Path(__file__).resolve().parents[2]
FINISH_TASK_SH = WORKSPACE / "scripts" / "finish-task.sh"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _read_sh() -> str:
    return FINISH_TASK_SH.read_text(encoding="utf-8")


def _extract_stash_lifecycle_heredoc(content: str) -> str:
    """
    finish-task.sh 의 stash-lifecycle dispatch 블록 내 PYEOF heredoc 본문을 추출한다.
    (stash-lifecycle dispatch 헤레독: python3 - ... <<'PYEOF' … PYEOF 블록)

    dispatch heredoc 는 'task_id = sys.argv[1]' 로 시작하고 'PYEOF' 로 끝나는
    두 번째 대형 Python heredoc 블록이다. (첫 번째는 cleanup-audit.jsonl 블록)
    """
    # stash-lifecycle dispatch heredoc 식별자: audit_timeout 라인 포함하는 블록
    # 'task_id = sys.argv[1]' 이 있는 블록 중 'audit_timeout' 도 있는 것을 선택
    marker = "audit_timeout = int(sys.argv[7])"
    idx = content.find(marker)
    if idx == -1:
        return ""
    # marker 전후로 블록의 시작(task_id = sys.argv[1])을 찾음
    block_start_marker = "task_id = sys.argv[1]"
    # marker 이전에서 가장 가까운 block_start_marker 를 찾음
    start_idx = content.rfind(block_start_marker, 0, idx)
    if start_idx == -1:
        # fallback: marker 위치에서 시작
        start_idx = idx
    # 해당 위치 이후의 PYEOF 종료 태그까지 추출
    end_marker = "\nPYEOF"
    end_idx = content.find(end_marker, start_idx)
    if end_idx == -1:
        return ""
    return content[start_idx : end_idx]


# ---------------------------------------------------------------------------
# Test 1: stash-lifecycle 블록 내 sys.exit(0) 회귀 0건
# ---------------------------------------------------------------------------

class TestNoSysExit0InAuditFailurePath:
    """TODO-3 회귀 — audit failure 처리부에 sys.exit(0) 가 없어야 한다."""

    def test_sys_exit_0_absent_in_audit_failure_block(self):
        """
        finish-task.sh 의 stash-lifecycle dispatch python heredoc 내부에
        'if res.returncode != 0 or not res.stdout.strip(): ... sys.exit(0)' 패턴이
        0건이어야 한다. (PR #125 이전 회귀 탐지)
        """
        content = _read_sh()

        # 전체 파일에서 audit failure + sys.exit(0) 의 복합 패턴 검색
        # returncode 체크 블록 내에서 sys.exit(0) 를 포함하는 경우를 탐지
        pattern = re.compile(
            r'if\s+res\.returncode\s*!=\s*0.*?sys\.exit\(0\)',
            re.DOTALL,
        )
        match = pattern.search(content)
        assert match is None, (
            f"TODO-3 회귀 감지: audit failure 처리부에 sys.exit(0) 발견.\n"
            f"매칭 위치: {match.start()} — {match.group()!r}\n"
            "fix: sys.exit(0) → sys.exit(1) (spec §3.2.2.3 fail-stop 요건)"
        )

    def test_sys_exit_0_not_in_lifecycle_heredoc(self):
        """
        stash-lifecycle heredoc 본문에 sys.exit(0) 가 없어야 한다.
        (heredoc 추출 후 isolated 검증)
        """
        content = _read_sh()
        heredoc = _extract_stash_lifecycle_heredoc(content)
        assert heredoc, "stash-lifecycle dispatch heredoc 를 추출할 수 없음 — 구조 변경 확인 필요"

        assert "sys.exit(0)" not in heredoc, (
            "TODO-3 회귀: stash-lifecycle heredoc 내 sys.exit(0) 발견.\n"
            "fix: sys.exit(1) 로 교체 필수 (fail-stop 보장)"
        )


# ---------------------------------------------------------------------------
# Test 2: audit failure 처리부에 sys.exit(1) 1건 확인
# ---------------------------------------------------------------------------

class TestSysExit1PresentInAuditFailurePath:
    """TODO-3 fix 박제 — audit failure 처리부에 sys.exit(1) 가 있어야 한다."""

    def test_fail_stop_log_message_present(self):
        """
        'FAIL-STOP — stash_audit.py invocation failed' 문구가 1건 이상 존재해야 한다.
        """
        content = _read_sh()
        matches = re.findall(
            r'FAIL-STOP\s+[—\-]+\s+stash_audit\.py invocation failed',
            content,
        )
        assert len(matches) >= 1, (
            "TODO-3 fix 실종: 'FAIL-STOP — stash_audit.py invocation failed' 메시지 없음.\n"
            "finish-task.sh stash-lifecycle heredoc 에 FAIL-STOP 로그 추가 필요."
        )

    def test_sys_exit_1_adjacent_to_fail_stop_log(self):
        """
        FAIL-STOP 로그 직후에 sys.exit(1) 이 있어야 한다.
        """
        content = _read_sh()
        # FAIL-STOP 로그 이후 2줄 이내에 sys.exit(1) 존재 여부 확인
        pattern = re.compile(
            r'FAIL-STOP\s+[—\-]+\s+stash_audit\.py invocation failed.*?sys\.exit\(1\)',
            re.DOTALL,
        )
        match = pattern.search(content)
        assert match is not None, (
            "TODO-3 fix 실종: FAIL-STOP 로그 후 sys.exit(1) 없음.\n"
            "audit 실패 시 sys.exit(1) 로 fail-stop 보장 필요."
        )

    def test_sys_exit_1_in_lifecycle_heredoc(self):
        """
        stash-lifecycle heredoc 본문에 sys.exit(1) 가 포함되어야 한다.
        """
        content = _read_sh()
        heredoc = _extract_stash_lifecycle_heredoc(content)
        assert heredoc, "stash-lifecycle dispatch heredoc 추출 실패"

        assert "sys.exit(1)" in heredoc, (
            "TODO-3 fix 실종: stash-lifecycle heredoc 내 sys.exit(1) 없음."
        )

    def test_returncode_check_pattern_present(self):
        """
        'if res.returncode != 0 or not res.stdout.strip():' 패턴이 stash-lifecycle
        heredoc 내에 있어야 한다 (audit 결과 체크 로직 존재 확인).
        """
        content = _read_sh()
        heredoc = _extract_stash_lifecycle_heredoc(content)
        assert heredoc, "stash-lifecycle dispatch heredoc 추출 실패"

        pattern = re.compile(r'if\s+res\.returncode\s*!=\s*0\s+or\s+not\s+res\.stdout\.strip\(\)\s*:')
        assert pattern.search(heredoc) is not None, (
            "stash-lifecycle heredoc 내 returncode 체크 로직 없음 — 구조 변경 확인 필요."
        )


# ---------------------------------------------------------------------------
# Test 3: audit failure 처리부 직전에 `if debug:` 가드 부재 확인
# ---------------------------------------------------------------------------

class TestNoDebugGuardBeforeAuditFailure:
    """TODO-3 fix 박제 — audit failure 처리부에 if debug: 가드가 없어야 한다."""

    def test_audit_failed_skipping_absent(self):
        """
        'audit failed, skipping' 문구가 0건이어야 한다.
        (이전 버전에서 if debug: 가드 하에 audit 실패를 skip 했던 패턴)
        """
        content = _read_sh()
        count = content.count("audit failed, skipping")
        assert count == 0, (
            f"TODO-3 회귀: 'audit failed, skipping' 문구 {count}건 발견.\n"
            "audit 실패는 반드시 fail-stop 처리되어야 함 (spec §3.2.2.3)."
        )

    def test_no_debug_guard_wrapping_audit_failure_exit(self):
        """
        stash-lifecycle heredoc 내에서 returncode 체크 → sys.exit(1) 블록이
        `if debug:` 가드 안에 있지 않아야 한다.

        허용 패턴: 직접 if res.returncode != 0: ... sys.exit(1)
        금지 패턴: if debug: ... if res.returncode != 0: ... sys.exit(1)
        """
        content = _read_sh()
        heredoc = _extract_stash_lifecycle_heredoc(content)
        assert heredoc, "stash-lifecycle dispatch heredoc 추출 실패"

        # returncode 체크 블록의 위치 확인
        rc_match = re.search(
            r'if\s+res\.returncode\s*!=\s*0\s+or\s+not\s+res\.stdout\.strip\(\)\s*:',
            heredoc,
        )
        assert rc_match is not None, "returncode 체크 로직 없음"

        # returncode 체크 직전 50자 이내에 `if debug:` 가 없어야 함
        preceding = heredoc[max(0, rc_match.start() - 200) : rc_match.start()]
        # 'if debug:' 와 returncode 체크 사이에 다른 블록이 있는지 확인
        # (마지막 if debug: 이후 returncode 체크까지의 거리를 측정)
        debug_matches = list(re.finditer(r'if\s+debug\s*:', preceding))
        if debug_matches:
            last_debug = debug_matches[-1]
            gap = preceding[last_debug.end():]
            # gap 내에 다른 블록 시작(if/for/with/def 등)이 없다면 debug 가드 내부
            other_blocks = re.findall(r'\n\s*(if|for|with|def|try|else|elif)\b', gap)
            assert len(other_blocks) > 0, (
                "TODO-3 회귀 의심: audit failure 처리부가 `if debug:` 가드 내부에 있을 수 있음.\n"
                f"preceding context: {preceding[-100:]!r}"
            )


# ---------------------------------------------------------------------------
# Test 4: 동적 검증 — 정적 AST 기반 sys.exit(1) 확인
# ---------------------------------------------------------------------------

class TestDynamicAuditFailureSimulation:
    """
    TODO-3 동적 회귀: stash-lifecycle heredoc 를 AST 파싱하여
    audit failure 경로에서 sys.exit(1) 호출 보장을 검증한다.
    """

    def test_ast_sys_exit_1_in_audit_failure_branch(self):
        """
        stash-lifecycle heredoc Python 코드를 AST 파싱하여
        returncode 체크 → sys.exit(1) 호출 구조를 확인한다.
        """
        import ast

        content = _read_sh()
        heredoc = _extract_stash_lifecycle_heredoc(content)
        assert heredoc, "stash-lifecycle dispatch heredoc 추출 실패"

        # AST 파싱 가능 여부 확인
        try:
            tree = ast.parse(heredoc)
        except SyntaxError as e:
            pytest.skip(f"heredoc Python 코드 파싱 불가 (의존성 코드일 수 있음): {e}")

        # ast.walk 로 sys.exit 호출 수집
        exit_calls = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                func = node.func
                # sys.exit(...) 패턴
                if (
                    isinstance(func, ast.Attribute)
                    and func.attr == "exit"
                    and isinstance(func.value, ast.Name)
                    and func.value.id == "sys"
                ):
                    if node.args and isinstance(node.args[0], ast.Constant):
                        exit_calls.append(node.args[0].value)

        assert 1 in exit_calls, (
            f"AST 검증 실패: sys.exit(1) 호출 없음. 발견된 sys.exit 인수: {exit_calls}\n"
            "TODO-3 fix: audit failure 경로에 sys.exit(1) 필수."
        )

    def test_mock_subprocess_heredoc_exits_nonzero(self, tmp_path):
        """
        stash-lifecycle heredoc 에서 stash_audit.py 를 모킹하여
        audit 실패(returncode=1) 시 python3 프로세스가 exit code 1 로 종료되는지 확인.

        전략: heredoc 본문을 임시 .py 파일에 쓰되, subprocess.run 을 monkeypatch 하는
        헤더를 prepend 하여 stash_audit.py 호출 시 rc=1 반환.
        """
        content = _read_sh()
        heredoc = _extract_stash_lifecycle_heredoc(content)
        assert heredoc, "stash-lifecycle dispatch heredoc 추출 실패"

        # monkeypatch 헤더: subprocess.run 을 오버라이드
        mock_header = textwrap.dedent("""\
            import subprocess as _real_subprocess
            import sys as _sys_orig
            _original_subprocess_run = _real_subprocess.run

            class _MockResult:
                def __init__(self):
                    self.returncode = 1
                    self.stdout = ""
                    self.stderr = "mocked stash_audit.py failure"

            def _patched_run(cmd, **kwargs):
                if isinstance(cmd, (list, tuple)) and any("stash_audit.py" in str(c) for c in cmd):
                    return _MockResult()
                return _original_subprocess_run(cmd, **kwargs)

            import subprocess
            subprocess.run = _patched_run
        """)

        test_script = tmp_path / "test_heredoc_failstop.py"
        # heredoc 코드에 필요한 sys.argv 설정 prepend
        # heredoc 내 코드: task_id=argv[1], approve=argv[2], indices=argv[3],
        #                   debug=argv[4], workspace=argv[5], done_file=argv[6], audit_timeout=argv[7]
        fake_ws = str(tmp_path)
        argv_setup = textwrap.dedent(f"""\
            import sys
            # argv: script_name, task_id, approve, indices, debug, workspace, done_file, audit_timeout
            sys.argv = ["test", "test-task-2576", "0", "", "0", {fake_ws!r}, "/tmp/fake.done", "30"]
        """)

        test_script.write_text(mock_header + argv_setup + heredoc, encoding="utf-8")

        result = subprocess.run(
            [sys.executable, str(test_script)],
            capture_output=True,
            text=True,
            timeout=30,
        )

        assert result.returncode != 0, (
            "TODO-3 동적 회귀: audit 실패 시 sys.exit(1) 이 호출되지 않음.\n"
            f"returncode={result.returncode}, stderr={result.stderr!r}"
        )

        assert "FAIL-STOP" in result.stderr, (
            "TODO-3 동적 회귀: FAIL-STOP 메시지가 stderr 에 없음.\n"
            f"stderr={result.stderr!r}"
        )

        assert "stash_audit.py invocation failed" in result.stderr, (
            "TODO-3 동적 회귀: 'stash_audit.py invocation failed' 메시지가 stderr 에 없음.\n"
            f"stderr={result.stderr!r}"
        )
