"""task-2503 회귀 테스트 파일 2 — 회장 §6 10건 + 자기참조 1건.

회장 §6 회귀 테스트 (파일 2/3):
  - classify 9 룰 검증 (10건)
  - 자기참조 검증 (1건)
  - AUDIT_LOG_PATH monkeypatch로 실제 audit 파일 오염 방지
"""
import json
import sys
from pathlib import Path
from typing import Any

# workspace root를 sys.path에 추가
WORKSPACE = Path(__file__).resolve().parent.parent.parent
if str(WORKSPACE) not in sys.path:
    sys.path.insert(0, str(WORKSPACE))

from utils.merge_topology_gate import (  # type: ignore[import]
    ALLOW,
    BLOCK,
    LIMITED_PARALLEL,
    REQUIRE_CHAIR_OVERRIDE,
    REASON_CHERRY_PICK_REQUESTED,
    REASON_DUPLICATE_FILE,
    REASON_DUPLICATE_VERIFIER,
    REASON_METADATA_MISSING,
    REASON_MISSING_DEPENDENCY,
    REASON_PARALLEL_SAFE_FALSE_DECLARATION,
    REASON_QUEUE_POSITION_MISSING,
    REASON_STALE_RECHECK_REQUIRED,
    classify,
    parse_topology_metadata,
    run_gate,
    validate_metadata,
)

# ── 완전한 task spec 템플릿 (parallel_safe, ALLOW 기대) ──────────────────
TASK_DESC_TEMPLATE = '''# Test task

```yaml
expected_files:
  - "foo/bar.py"
risk_area: "verifier_layer"
dependency: "none"
parallel_policy: "serial_only"
merge_queue_position: 1
stale_recheck_required: true
cherry_pick_allowed: false
```
'''

TASK_DESC_PARALLEL_SAFE = '''# Parallel safe task

```yaml
expected_files:
  - "foo/unique_file.py"
risk_area: "content"
dependency: "none"
parallel_policy: "parallel_safe"
merge_queue_position: "n/a"
stale_recheck_required: false
cherry_pick_allowed: false
```
'''


def _make_metadata(
    files: Any = None,
    risk_area: str = "content",
    dependency: Any = "none",
    parallel_policy: str = "serial_only",
    merge_queue_position: Any = 1,
    stale_recheck_required: bool = False,
    cherry_pick_allowed: Any = False,
) -> dict:
    """테스트용 완전한 metadata dict 생성 헬퍼.

    각 필드는 metadata schema가 union 타입(int|str, bool|str, list|str)을 허용하므로
    `Any`로 어노테이션. 실제 검증은 validate_metadata가 수행한다.
    """
    return {
        "expected_files": files or ["foo/bar.py"],
        "risk_area": risk_area,
        "dependency": dependency,
        "parallel_policy": parallel_policy,
        "merge_queue_position": merge_queue_position,
        "stale_recheck_required": stale_recheck_required,
        "cherry_pick_allowed": cherry_pick_allowed,
    }


# ═══════════════════════════════════════════════════════════════════════════
# TC-1: 빈 metadata → BLOCK + METADATA_MISSING
# ═══════════════════════════════════════════════════════════════════════════
def test_metadata_missing_blocks():
    """룰 1: 빈 metadata → decision=BLOCK + METADATA_MISSING in reason_codes."""
    decision = classify({}, active_tasks=[])
    assert decision.decision == BLOCK, f"expected BLOCK, got {decision.decision}"
    assert REASON_METADATA_MISSING in decision.reason_codes, (
        f"expected METADATA_MISSING in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-2: 다른 active task와 expected_files 교집합 → BLOCK + DUPLICATE_FILE
# ═══════════════════════════════════════════════════════════════════════════
def test_duplicate_file_overlap_blocks():
    """룰 2: 다른 active task와 expected_files 교집합 → BLOCK + DUPLICATE_FILE."""
    metadata = _make_metadata(files=["utils/shared.py", "tests/test_foo.py"])

    active_task = {
        "task_id": "task-9001",
        "expected_files": ["utils/shared.py", "other/file.py"],
        "risk_area": "other",
        "parallel_policy": "serial_only",
        "merge_queue_position": 2,
    }

    decision = classify(metadata, active_tasks=[active_task])
    assert decision.decision == BLOCK, f"expected BLOCK, got {decision.decision}"
    assert REASON_DUPLICATE_FILE in decision.reason_codes, (
        f"expected DUPLICATE_FILE in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-3: verifier_layer risk_area 중복 → BLOCK + DUPLICATE_VERIFIER
# ═══════════════════════════════════════════════════════════════════════════
def test_duplicate_verifier_risk_area_blocks_or_limited():
    """룰 4: 두 task 모두 risk_area=verifier_layer → BLOCK + DUPLICATE_VERIFIER."""
    metadata = _make_metadata(
        files=["verifier/new_check.py"],
        risk_area="verifier_layer",
    )

    active_task = {
        "task_id": "task-9002",
        "expected_files": ["verifier/other_check.py"],
        "risk_area": "verifier_layer",
        "parallel_policy": "serial_only",
        "merge_queue_position": 2,
    }

    decision = classify(metadata, active_tasks=[active_task])
    # 현재 구현은 BLOCK
    assert decision.decision == BLOCK, f"expected BLOCK, got {decision.decision}"
    assert REASON_DUPLICATE_VERIFIER in decision.reason_codes, (
        f"expected DUPLICATE_VERIFIER in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-4: dependency 미머지 → BLOCK + MISSING_DEPENDENCY (mock dependency_check)
# ═══════════════════════════════════════════════════════════════════════════
def test_dependency_unmerged_blocks():
    """룰 6: dependency=['task-9999.merged'] 미머지 → BLOCK + MISSING_DEPENDENCY."""
    metadata = _make_metadata(
        files=["utils/new_feature.py"],
        dependency=["task-9999.merged"],
    )

    # dependency_check mock: 항상 (False, ['task-9999.merged']) 반환
    def _dep_unmerged(*_args):
        del _args  # parameter present for signature compatibility only
        return (False, ["task-9999.merged"])

    decision = classify(
        metadata,
        active_tasks=[],
        dependency_check=_dep_unmerged,
    )

    assert decision.decision == BLOCK, f"expected BLOCK, got {decision.decision}"
    assert REASON_MISSING_DEPENDENCY in decision.reason_codes, (
        f"expected MISSING_DEPENDENCY in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-5: parallel_safe 허위 선언 (active task와 파일 교집합) → BLOCK + PARALLEL_SAFE_FALSE_DECLARATION
# ═══════════════════════════════════════════════════════════════════════════
def test_parallel_safe_false_declaration_blocks():
    """룰 9: parallel_policy=parallel_safe인데 active task와 expected_files 교집합 → BLOCK + PARALLEL_SAFE_FALSE_DECLARATION."""
    metadata = _make_metadata(
        files=["utils/shared_module.py"],
        parallel_policy="parallel_safe",
        merge_queue_position="n/a",
    )

    active_task = {
        "task_id": "task-9003",
        "expected_files": ["utils/shared_module.py"],  # 교집합 발생!
        "risk_area": "other",
        "parallel_policy": "serial_only",
        "merge_queue_position": 1,
    }

    decision = classify(metadata, active_tasks=[active_task])
    assert decision.decision == BLOCK, f"expected BLOCK, got {decision.decision}"
    assert REASON_PARALLEL_SAFE_FALSE_DECLARATION in decision.reason_codes, (
        f"expected PARALLEL_SAFE_FALSE_DECLARATION in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-6: cherry_pick_allowed='recovery_only' → REQUIRE_CHAIR_OVERRIDE + CHERRY_PICK_REQUESTED
# ═══════════════════════════════════════════════════════════════════════════
def test_cherry_pick_allowed_recovery_only_requires_chair_override():
    """룰 7: cherry_pick_allowed='recovery_only' → REQUIRE_CHAIR_OVERRIDE + CHERRY_PICK_REQUESTED."""
    metadata = _make_metadata(
        files=["hotfix/recovery.py"],
        cherry_pick_allowed="recovery_only",
    )

    decision = classify(metadata, active_tasks=[])
    assert decision.decision == REQUIRE_CHAIR_OVERRIDE, (
        f"expected REQUIRE_CHAIR_OVERRIDE, got {decision.decision}"
    )
    assert REASON_CHERRY_PICK_REQUESTED in decision.reason_codes, (
        f"expected CHERRY_PICK_REQUESTED in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-7: run_gate(override=True) → audit jsonl에 override_used=true 기록
# ═══════════════════════════════════════════════════════════════════════════
def test_override_used_audit_record_generated(tmp_path, monkeypatch):
    """run_gate(task_id, desc, override=True) 시 audit jsonl에 override_used=true 기록."""
    audit_file = tmp_path / "merge-topology-gate.jsonl"

    # AUDIT_LOG_PATH를 tmp_path로 redirect (실제 audit 파일 오염 방지)
    monkeypatch.setattr(
        "utils.merge_topology_gate.AUDIT_LOG_PATH",
        audit_file,
    )

    # cherry_pick_allowed=recovery_only → REQUIRE_CHAIR_OVERRIDE → override=True로 통과
    task_desc = '''# Override test task

```yaml
expected_files:
  - "hotfix/override_test.py"
risk_area: "governance"
dependency: "none"
parallel_policy: "serial_only"
merge_queue_position: 1
stale_recheck_required: false
cherry_pick_allowed: recovery_only
```
'''

    decision, allowed = run_gate(
        task_id="test-override-001",
        task_desc=task_desc,
        override=True,
    )

    # override=True + REQUIRE_CHAIR_OVERRIDE → allowed=True
    assert allowed is True, f"expected allowed=True, got {allowed}"
    assert decision.decision == REQUIRE_CHAIR_OVERRIDE, (
        f"expected REQUIRE_CHAIR_OVERRIDE, got {decision.decision}"
    )
    assert audit_file.exists(), "audit jsonl must be created"

    # jsonl 레코드 검증
    records = [json.loads(line) for line in audit_file.read_text().splitlines() if line.strip()]
    assert records, "audit file must contain at least one record"

    last_record = records[-1]
    assert last_record["task_id"] == "test-override-001"
    assert last_record["override_used"] is True, (
        f"expected override_used=true but got {last_record.get('override_used')}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-8: 정상 parallel_safe + active task 없음 + dependency='none' → ALLOW
# ═══════════════════════════════════════════════════════════════════════════
def test_normal_parallel_safe_allows(monkeypatch, tmp_path):
    """metadata 정상 + active task 없음 + parallel_safe → ALLOW."""
    # AUDIT_LOG_PATH도 오염 방지용으로 redirect
    audit_file = tmp_path / "merge-topology-gate.jsonl"
    monkeypatch.setattr(
        "utils.merge_topology_gate.AUDIT_LOG_PATH",
        audit_file,
    )

    metadata = _make_metadata(
        files=["utils/isolated_new_module.py"],
        risk_area="content",
        dependency="none",
        parallel_policy="parallel_safe",
        merge_queue_position="n/a",
    )

    decision = classify(metadata, active_tasks=[])
    assert decision.decision == ALLOW, (
        f"expected ALLOW for clean parallel_safe task, got {decision.decision} "
        f"reason_codes={decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-9: parallel_policy=limited_parallel + merge_queue_position='n/a' → BLOCK + QUEUE_POSITION_MISSING
# ═══════════════════════════════════════════════════════════════════════════
def test_limited_parallel_with_missing_queue_position_blocks():
    """룰 8: parallel_policy=limited_parallel + merge_queue_position='n/a' → BLOCK + QUEUE_POSITION_MISSING."""
    # validate_metadata가 QUEUE_POSITION_MISSING만 반환하는 경우를 classify가 처리
    metadata = {
        "expected_files": ["utils/limited_task.py"],
        "risk_area": "dispatch_layer",
        "dependency": "none",
        "parallel_policy": "limited_parallel",
        "merge_queue_position": "n/a",  # limited_parallel은 양의 정수 강제
        "stale_recheck_required": False,
        "cherry_pick_allowed": False,
    }

    decision = classify(metadata, active_tasks=[])
    assert decision.decision == BLOCK, (
        f"expected BLOCK for limited_parallel with n/a queue_position, got {decision.decision}"
    )
    assert REASON_QUEUE_POSITION_MISSING in decision.reason_codes, (
        f"expected QUEUE_POSITION_MISSING in {decision.reason_codes}"
    )


# ═══════════════════════════════════════════════════════════════════════════
# TC-10: stale_recheck_required=True → metadata에 보존 검증
# ═══════════════════════════════════════════════════════════════════════════
def test_stale_recheck_required_invocation():
    """stale_recheck_required=True인 metadata로 classify 호출 시 metadata에 보존됨."""
    metadata = _make_metadata(
        files=["utils/stale_check.py"],
        stale_recheck_required=True,
    )

    decision = classify(metadata, active_tasks=[])

    # stale_recheck_required 자체는 BLOCK 사유가 아님 — metadata에 보존되는지 검증
    assert decision.metadata.get("stale_recheck_required") is True, (
        f"stale_recheck_required=True must be preserved in decision.metadata, "
        f"got: {decision.metadata.get('stale_recheck_required')}"
    )
    # 단독으로는 BLOCK 아님 (다른 위반 없을 경우)
    assert REASON_STALE_RECHECK_REQUIRED not in decision.reason_codes or decision.decision in (
        ALLOW, LIMITED_PARALLEL, REQUIRE_CHAIR_OVERRIDE, BLOCK
    ), "stale_recheck_required alone should not introduce unexpected decision"


# ═══════════════════════════════════════════════════════════════════════════
# TC-11 (자기참조): task-2503.md 파일 파싱 → validate → classify (active_tasks=[])
#                   errors=[] AND decision != BLOCK
# ═══════════════════════════════════════════════════════════════════════════
def test_self_reference_task_2503_passes_metadata_extraction():
    """자기참조: task-2503.md 파일 자체를 파싱→validate→classify (active_tasks=[]) 했을 때
    errors=[] AND decision != BLOCK."""
    task_md_path = WORKSPACE / "memory" / "tasks" / "task-2503.md"
    assert task_md_path.exists(), f"task-2503.md not found: {task_md_path}"

    task_desc = task_md_path.read_text(encoding="utf-8")

    # parse
    metadata = parse_topology_metadata(task_desc)
    assert metadata, "parse_topology_metadata must extract metadata from task-2503.md"

    # validate
    errors = validate_metadata(metadata)
    assert errors == [], (
        f"task-2503.md metadata must pass validate_metadata, got errors: {errors}"
    )

    # classify (active_tasks=[] — 자기참조만 검증)
    def _dep_merged(*_args):
        del _args  # parameter present for signature compatibility only
        return (True, [])

    decision = classify(
        metadata,
        active_tasks=[],
        dependency_check=_dep_merged,  # dependency mock (task-2502.merged 스킵)
    )

    assert decision.decision != BLOCK, (
        f"task-2503.md self-reference must NOT result in BLOCK, "
        f"got {decision.decision} reason_codes={decision.reason_codes}"
    )
