"""
tests/regression/test_delegation_watcher_2773.py — task-2773 회귀 테스트 4종

아르고스(테스터) 작성. 헤르메스 산하 task-2773 위임.
대상: utils/delegation_watcher.py (불칸 구현 중)
격리: tmp_path fixture (pytest built-in) — 각 테스트마다 독립 ledger/산출물 디렉토리.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

# ── 경로 보정 (regression/conftest.py 가 우선 처리하지만, 안전망으로 중복 보장) ──
_WORKSPACE = Path(__file__).resolve().parents[2]
if str(_WORKSPACE) not in sys.path:
    sys.path.insert(0, str(_WORKSPACE))

# ── 모듈 import — 불칸 구현 완료 전에는 ImportError 가 발생할 수 있음 ──
try:
    from utils.delegation_watcher import (
        record_delegation,
        check_pending_delegations,
        close_delegation,
        render_pending_briefing,
    )
    _MODULE_AVAILABLE = True
except ImportError as _e:
    _MODULE_AVAILABLE = False
    _IMPORT_ERROR = str(_e)

import pytest

# 모듈 미존재 시 전체 파일 스킵 (계약 기준 테스트는 완성 상태 유지)
pytestmark = pytest.mark.skipif(
    not _MODULE_AVAILABLE,
    reason=f"utils.delegation_watcher 미구현(불칸 담당): {_IMPORT_ERROR if not _MODULE_AVAILABLE else ''}",
)


# ── 공통 헬퍼 ──

def _make_ledger_path(tmp_path: Path) -> Path:
    """격리된 ledger 경로 반환 (부모 디렉토리까지 생성)."""
    ledger = tmp_path / "pending_delegations.jsonl"
    return ledger


def _parse_jsonl(path: Path) -> list[dict]:
    """jsonl 파일을 파싱하여 dict 리스트로 반환."""
    records = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if line:
            records.append(json.loads(line))
    return records


# ── 테스트 1: ledger 생성 및 레코드 검증 ──

def test_ledger_creation(tmp_path: Path) -> None:
    """record_delegation 호출 후 ledger(jsonl) 파일 존재 및 레코드 내용 검증.

    검증 항목:
    - pending_delegations.jsonl 파일이 존재해야 한다.
    - 파일을 직접 파싱하면 해당 task_id 레코드가 1줄 이상 존재해야 한다.
    - 레코드의 status == 'pending', record_type == 'record' 이어야 한다.
    """
    ledger_path = _make_ledger_path(tmp_path)
    task_id = "task-2773-ledger"
    result_dir = tmp_path / "results" / task_id
    result_dir.mkdir(parents=True, exist_ok=True)

    expected_paths = [
        str(result_dir / f"{task_id}.done"),
        str(result_dir / f"{task_id}.failure-envelope.json"),
        str(result_dir / "result.json"),
    ]

    rec = record_delegation(
        task_id=task_id,
        bot="불칸",
        team="dev1",
        branch="feat/task-2773",
        expected_result_paths=expected_paths,
        dispatch_time="2026-06-24T00:00:00Z",
        ledger_path=str(ledger_path),
    )

    # (1) ledger 파일 존재 확인
    assert ledger_path.exists(), "ledger(jsonl) 파일이 생성되지 않았다."

    # (2) 파일 직접 파싱 — task_id 레코드 존재 확인
    records = _parse_jsonl(ledger_path)
    matching = [r for r in records if r.get("task_id") == task_id]
    assert len(matching) >= 1, f"ledger 에 task_id={task_id} 레코드가 없다. 전체: {records}"

    # (3) 레코드 필드 검증
    latest = matching[-1]
    assert latest.get("status") == "pending", f"status 기대값 'pending', 실제: {latest.get('status')}"
    assert latest.get("record_type") == "record", f"record_type 기대값 'record', 실제: {latest.get('record_type')}"

    # (4) 반환 dict 도 동일 계약 충족 확인
    assert rec.get("task_id") == task_id
    assert rec.get("status") == "pending"
    assert rec.get("record_type") == "record"


# ── 테스트 2: result.json 존재 + .done 부재 → ready_for_adjudication ──

def test_result_json_separated_from_done(tmp_path: Path) -> None:
    """result.json 만 생성(.done 부재) 시 status_detected=='ready_for_adjudication'.

    .done 이 없다고 해서 'failure' 판정을 내려서는 안 된다.
    ready_for_adjudication 이면 needs_adjudication==True 이어야 한다.
    """
    ledger_path = _make_ledger_path(tmp_path)
    task_id = "task-2773-rfa"
    result_dir = tmp_path / "results" / task_id
    result_dir.mkdir(parents=True, exist_ok=True)

    done_path = result_dir / f"{task_id}.done"
    failure_path = result_dir / f"{task_id}.failure-envelope.json"
    result_json_path = result_dir / "result.json"

    expected_paths = [
        str(done_path),
        str(failure_path),
        str(result_json_path),
    ]

    record_delegation(
        task_id=task_id,
        bot="불칸",
        team="dev1",
        branch="feat/task-2773",
        expected_result_paths=expected_paths,
        dispatch_time="2026-06-24T00:01:00Z",
        ledger_path=str(ledger_path),
    )

    # result.json 만 생성 — .done, .failure-envelope.json 은 생성하지 않음
    result_json_path.write_text(json.dumps({"result": "partial"}), encoding="utf-8")
    # done_path 및 failure_path 는 의도적으로 생성 안 함

    items = check_pending_delegations(
        workspace=str(tmp_path),
        ledger_path=str(ledger_path),
    )

    matching = [i for i in items if i.get("task_id") == task_id]
    assert len(matching) == 1, f"check_pending_delegations 결과에 task_id={task_id} 없음: {items}"

    item = matching[0]
    assert item.get("status_detected") == "ready_for_adjudication", (
        f".done 부재 + result.json 존재 시 status_detected 기대값 'ready_for_adjudication', "
        f"실제: {item.get('status_detected')}"
    )
    assert item.get("needs_adjudication") is True, (
        f"ready_for_adjudication 상태에서 needs_adjudication 기대값 True, 실제: {item.get('needs_adjudication')}"
    )
    assert item.get("status_detected") != "failure", (
        ".done 부재만으로 'failure' 판정 금지 — 계약 위반"
    )


# ── 테스트 3: supervisor-crash + result.json 공존 → crash 분리 ──

def test_supervisor_crash_separated(tmp_path: Path) -> None:
    """supervisor-crash-marker + result.json 동시 존재 시 crash/deliverable_ok 분리.

    crash==True 라도 result.json 이 있으면 deliverable_ok==True 로 분리.
    crash 만으로 'failure' 단정 금지.
    """
    ledger_path = _make_ledger_path(tmp_path)
    task_id = "task-2773-crash"
    result_dir = tmp_path / "results" / task_id
    result_dir.mkdir(parents=True, exist_ok=True)

    done_path = result_dir / f"{task_id}.done"
    failure_path = result_dir / f"{task_id}.failure-envelope.json"
    result_json_path = result_dir / "result.json"
    crash_marker_path = result_dir / f"{task_id}.supervisor-crash-marker.json"

    expected_paths = [
        str(done_path),
        str(failure_path),
        str(result_json_path),
    ]

    record_delegation(
        task_id=task_id,
        bot="불칸",
        team="dev1",
        branch="feat/task-2773",
        expected_result_paths=expected_paths,
        dispatch_time="2026-06-24T00:02:00Z",
        ledger_path=str(ledger_path),
    )

    # crash marker 와 result.json 동시 생성 (.done 부재)
    crash_marker_path.write_text(
        json.dumps({"reason": "supervisor OOM", "task_id": task_id}),
        encoding="utf-8",
    )
    result_json_path.write_text(
        json.dumps({"result": "recovered_output"}),
        encoding="utf-8",
    )
    # .done 과 .failure-envelope.json 은 생성 안 함

    items = check_pending_delegations(
        workspace=str(tmp_path),
        ledger_path=str(ledger_path),
    )

    matching = [i for i in items if i.get("task_id") == task_id]
    assert len(matching) == 1, f"check_pending_delegations 결과에 task_id={task_id} 없음: {items}"

    item = matching[0]
    assert item.get("crash") is True, (
        f"crash-marker 존재 시 crash 기대값 True, 실제: {item.get('crash')}"
    )
    assert item.get("deliverable_ok") is True, (
        f"crash==True 라도 result.json 존재 시 deliverable_ok 기대값 True, "
        f"실제: {item.get('deliverable_ok')}"
    )
    assert item.get("status_detected") != "failure", (
        "crash 만으로 'failure' 단정 금지 — 계약 위반"
    )


# ── 테스트 4: prelude 차단 의미론 (pending 존재 / 미존재 양방향) ──

def test_prelude_blocking_semantics(tmp_path: Path) -> None:
    """pending 존재/미존재 양방향 시나리오 검증.

    (a) pending 있음: items 비어있지 않고, render_pending_briefing(items) 가
        'PENDING' 또는 task_id 를 포함한 비어있지 않은 문자열 반환.
    (b) pending 없음: items==[], render_pending_briefing([]) == '' (무영향).
    """
    task_id = "task-2773-prelude"

    # ── (a) pending 존재 시나리오 ──
    ledger_a = tmp_path / "ledger_a.jsonl"
    result_dir_a = tmp_path / "results_a" / task_id
    result_dir_a.mkdir(parents=True, exist_ok=True)

    expected_paths_a = [
        str(result_dir_a / f"{task_id}.done"),
        str(result_dir_a / f"{task_id}.failure-envelope.json"),
        str(result_dir_a / "result.json"),
    ]

    record_delegation(
        task_id=task_id,
        bot="불칸",
        team="dev1",
        branch="feat/task-2773",
        expected_result_paths=expected_paths_a,
        dispatch_time="2026-06-24T00:03:00Z",
        ledger_path=str(ledger_a),
    )
    # 산출물 파일 생성 안 함 → 상태: pending

    items_a = check_pending_delegations(
        workspace=str(tmp_path),
        ledger_path=str(ledger_a),
    )
    assert len(items_a) > 0, "pending 레코드가 있는데 check_pending_delegations 가 빈 리스트 반환"

    briefing_a = render_pending_briefing(items_a)
    assert isinstance(briefing_a, str), "render_pending_briefing 반환값이 str 이어야 한다"
    assert len(briefing_a) > 0, "pending 항목 있을 때 render_pending_briefing 가 빈 문자열 반환 — hook 주입 불가"
    assert ("PENDING" in briefing_a or task_id in briefing_a), (
        f"briefing 에 'PENDING' 또는 '{task_id}' 가 포함되어야 한다. 실제: {briefing_a!r}"
    )

    # ── (b) pending 없음 시나리오 ──
    ledger_b = tmp_path / "ledger_b_empty.jsonl"
    # ledger_b 는 생성하지 않음 (파일 없음 = pending 0)

    items_b = check_pending_delegations(
        workspace=str(tmp_path),
        ledger_path=str(ledger_b),
    )
    assert items_b == [], (
        f"ledger 없을 때 check_pending_delegations 기대값 [], 실제: {items_b}"
    )

    briefing_b = render_pending_briefing([])
    assert briefing_b == "", (
        f"빈 items 일 때 render_pending_briefing 기대값 '', 실제: {briefing_b!r}"
    )
