"""tests/regression/test_repository_policy_adapter_2519.py

회귀 테스트 14건 — repository_policy_adapter.py
작성자: 헤임달(테스터), 개발2팀 QA 엔지니어
task-2519 회귀 테스트 고정 fixture

모듈 본체 수정 금지. gh api 실호출 금지. 모두 runner mock.
"""
from __future__ import annotations

import json
import subprocess
import sys
from dataclasses import fields
from pathlib import Path
from typing import Any, Dict

import pytest

# ---------------------------------------------------------------------------
# Import path 설정
# ---------------------------------------------------------------------------
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from utils.repository_policy_adapter import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    BlockedReason,
    MergePathPlan,
    RepositoryCapability,
    assert_no_admin_override,
    classify_blocked_reason,
    invoke_triage_hook,
    probe_capability,
    select_merge_path,
)


# ---------------------------------------------------------------------------
# Runner factory helper (회장 §명시 패턴 그대로)
# ---------------------------------------------------------------------------

def make_mock_runner(responses: Dict[str, Any]):
    """gh api endpoint별로 mock 응답을 반환하는 runner factory."""

    def runner(cmd: list[str], *, cwd: Any = None) -> subprocess.CompletedProcess[str]:
        del cwd
        endpoint = cmd[2] if len(cmd) > 2 else ""
        if endpoint in responses:
            stdout = json.dumps(responses[endpoint])
            return subprocess.CompletedProcess(cmd, 0, stdout, "")
        return subprocess.CompletedProcess(cmd, 1, "", "Not Found")

    return runner


# ---------------------------------------------------------------------------
# 공통 capability 헬퍼
# ---------------------------------------------------------------------------

def _cap(
    *,
    can_squash_merge: bool = True,
    requires_approval: bool = False,
    requires_thread_resolution: bool = False,
    auto_merge_enabled: bool = True,
    bot_can_merge: bool = True,
    admin_override_required: bool = False,
) -> RepositoryCapability:
    return RepositoryCapability(
        can_squash_merge=can_squash_merge,
        requires_approval=requires_approval,
        requires_thread_resolution=requires_thread_resolution,
        auto_merge_enabled=auto_merge_enabled,
        bot_can_merge=bot_can_merge,
        admin_override_required=admin_override_required,
    )


# ===========================================================================
# capability probe 4건
# ===========================================================================


def test_repository_capability_six_field_probe() -> None:
    """(1) probe_capability에 mock runner 주입 → 6 field 모두 정확 매칭."""
    owner, repo, branch = "test-owner", "test-repo", "main"

    ruleset_endpoint = f"repos/{owner}/{repo}/rules/branches/{branch}"
    protection_endpoint = f"repos/{owner}/{repo}/branches/{branch}/protection"
    repo_endpoint = f"repos/{owner}/{repo}"
    perm_endpoint = f"repos/{owner}/{repo}/collaborators/github-actions[bot]/permission"

    # protection_endpoint는 dict에서 빼면 make_mock_runner가 returncode=1로 처리
    _ = protection_endpoint
    mock_responses: Dict[str, Any] = {
        ruleset_endpoint: [
            {
                "type": "pull_request",
                "parameters": {
                    "required_approving_review_count": 2,
                    "required_review_thread_resolution": True,
                },
                "bypass_actors": [],
            }
        ],
        repo_endpoint: {
            "allow_squash_merge": True,
            "allow_auto_merge": True,
        },
        perm_endpoint: {"permission": "write"},
    }

    cap = probe_capability(owner, repo, branch, runner=make_mock_runner(mock_responses))

    # 6 field 모두 dataclass에 존재 확인
    field_names = {f.name for f in fields(cap)}
    assert "can_squash_merge" in field_names
    assert "requires_approval" in field_names
    assert "requires_thread_resolution" in field_names
    assert "auto_merge_enabled" in field_names
    assert "bot_can_merge" in field_names
    assert "admin_override_required" in field_names
    assert len(field_names) == 6

    # 값 검증
    assert cap.can_squash_merge is True
    assert cap.requires_approval is True          # count=2 > 0
    assert cap.requires_thread_resolution is True
    assert cap.auto_merge_enabled is True
    assert cap.bot_can_merge is True              # "write"
    assert cap.admin_override_required is False   # bypass_actors=[]


def test_ruleset_required_review_thread_resolution_true() -> None:
    """(2) ruleset 응답 mock에 required_review_thread_resolution=true 포함 시 requires_thread_resolution=True."""
    owner, repo, branch = "org", "myrepo", "main"

    ruleset_endpoint = f"repos/{owner}/{repo}/rules/branches/{branch}"
    repo_endpoint = f"repos/{owner}/{repo}"
    perm_endpoint = f"repos/{owner}/{repo}/collaborators/github-actions[bot]/permission"

    mock_responses: Dict[str, Any] = {
        ruleset_endpoint: [
            {
                "type": "pull_request",
                "parameters": {
                    "required_approving_review_count": 0,
                    "required_review_thread_resolution": True,
                },
                "bypass_actors": [],
            }
        ],
        repo_endpoint: {"allow_squash_merge": True, "allow_auto_merge": False},
        perm_endpoint: {"permission": "write"},
    }
    cap = probe_capability(owner, repo, branch, runner=make_mock_runner(mock_responses))
    assert cap.requires_thread_resolution is True


def test_required_approving_review_count_zero() -> None:
    """(3) ruleset의 required_approving_review_count=0 → requires_approval=False."""
    owner, repo, branch = "org", "myrepo", "main"

    ruleset_endpoint = f"repos/{owner}/{repo}/rules/branches/{branch}"
    repo_endpoint = f"repos/{owner}/{repo}"
    perm_endpoint = f"repos/{owner}/{repo}/collaborators/github-actions[bot]/permission"

    mock_responses: Dict[str, Any] = {
        ruleset_endpoint: [
            {
                "type": "pull_request",
                "parameters": {
                    "required_approving_review_count": 0,
                    "required_review_thread_resolution": False,
                },
                "bypass_actors": [],
            }
        ],
        repo_endpoint: {"allow_squash_merge": True, "allow_auto_merge": True},
        perm_endpoint: {"permission": "write"},
    }
    cap = probe_capability(owner, repo, branch, runner=make_mock_runner(mock_responses))
    assert cap.requires_approval is False


def test_bot_permission_probe() -> None:
    """(4) collaborators permission mock 응답이 'write' → bot_can_merge=True. 'read' → False."""
    owner, repo, branch = "org", "myrepo", "main"
    ruleset_endpoint = f"repos/{owner}/{repo}/rules/branches/{branch}"
    repo_endpoint = f"repos/{owner}/{repo}"
    perm_endpoint = f"repos/{owner}/{repo}/collaborators/github-actions[bot]/permission"

    def responses_for(perm_value: str) -> Dict[str, Any]:
        return {
            ruleset_endpoint: [],
            repo_endpoint: {"allow_squash_merge": True, "allow_auto_merge": True},
            perm_endpoint: {"permission": perm_value},
        }

    cap_write = probe_capability(owner, repo, branch, runner=make_mock_runner(responses_for("write")))
    assert cap_write.bot_can_merge is True

    cap_read = probe_capability(owner, repo, branch, runner=make_mock_runner(responses_for("read")))
    assert cap_read.bot_can_merge is False


# ===========================================================================
# BLOCKED 원인 분류 7종 (7건)
# ===========================================================================


def test_classify_unresolved_review_thread_pr61_fixture() -> None:
    """(5) PR #61 fixture (5 unresolved threads + ruleset requires_thread_resolution) → UNRESOLVED_REVIEW_THREAD."""
    pr61: Dict[str, Any] = {
        "number": 61,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [
            {"isResolved": False},
            {"isResolved": False},
            {"isResolved": False},
            {"isResolved": False},
            {"isResolved": False},
        ],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(requires_thread_resolution=True, bot_can_merge=True, auto_merge_enabled=True)
    result = classify_blocked_reason(pr61, cap)
    assert result == BlockedReason.UNRESOLVED_REVIEW_THREAD


def test_classify_required_approval() -> None:
    """(6) 합성 fixture (capability.requires_approval=True, reviewDecision='REVIEW_REQUIRED') → REQUIRED_APPROVAL."""
    pr: Dict[str, Any] = {
        "number": 100,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "REVIEW_REQUIRED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(requires_approval=True, requires_thread_resolution=False, bot_can_merge=True, auto_merge_enabled=True)
    result = classify_blocked_reason(pr, cap)
    assert result == BlockedReason.REQUIRED_APPROVAL


def test_classify_stale_base_pr67_fixture() -> None:
    """(7) PR #67 fixture (mergeStateStatus='BEHIND') → STALE_BASE."""
    pr67: Dict[str, Any] = {
        "number": 67,
        "mergeStateStatus": "BEHIND",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(requires_thread_resolution=False, requires_approval=False, bot_can_merge=True, auto_merge_enabled=True)
    result = classify_blocked_reason(pr67, cap)
    assert result == BlockedReason.STALE_BASE


def test_classify_missing_ci_check() -> None:
    """(8) 합성 (statusCheckRollup state='PENDING') → MISSING_CI_CHECK."""
    pr: Dict[str, Any] = {
        "number": 200,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "PENDING"},
    }
    cap = _cap(
        requires_thread_resolution=False,
        requires_approval=False,
        bot_can_merge=True,
        auto_merge_enabled=True,
    )
    result = classify_blocked_reason(pr, cap)
    assert result == BlockedReason.MISSING_CI_CHECK


def test_classify_branch_protection() -> None:
    """(9) 합성 (mergeStateStatus='BLOCKED', capability에 추가 차단요소 없음, bot_can_merge=True, auto_merge_enabled=True) → BRANCH_PROTECTION."""
    pr: Dict[str, Any] = {
        "number": 300,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(
        requires_thread_resolution=False,
        requires_approval=False,
        bot_can_merge=True,
        auto_merge_enabled=True,
    )
    result = classify_blocked_reason(pr, cap)
    assert result == BlockedReason.BRANCH_PROTECTION


def test_classify_permission_issue() -> None:
    """(10) 합성 (capability.bot_can_merge=False) → PERMISSION_ISSUE."""
    pr: Dict[str, Any] = {
        "number": 400,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(
        requires_thread_resolution=False,
        requires_approval=False,
        bot_can_merge=False,
        auto_merge_enabled=True,
    )
    result = classify_blocked_reason(pr, cap)
    assert result == BlockedReason.PERMISSION_ISSUE


def test_classify_auto_merge_unsupported() -> None:
    """(11) 합성 (capability.auto_merge_enabled=False, bot_can_merge=True) → AUTO_MERGE_UNSUPPORTED."""
    pr: Dict[str, Any] = {
        "number": 500,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(
        requires_thread_resolution=False,
        requires_approval=False,
        bot_can_merge=True,
        auto_merge_enabled=False,
    )
    result = classify_blocked_reason(pr, cap)
    assert result == BlockedReason.AUTO_MERGE_UNSUPPORTED


# ===========================================================================
# merge path + replay 3건
# ===========================================================================


def test_pr61_replay_unresolved_to_triage() -> None:
    """(12) PR #61 replay → UNRESOLVED_REVIEW_THREAD → MergePathPlan(action='auto_gemini_triage', triage_hook='auto_gemini_triage.triage_pr').

    추가 검증:
    - REQUIRED_APPROVAL 분류 시 plan.requires_chair=False (회장 직접 머지 fallback 없음)
    - assert_no_admin_override(['gh', 'pr', 'merge', '--admin']) → RuntimeError
    - assert_no_admin_override(['gh', 'pr', 'merge', '--squash']) → no error
    - BRANCH_PROTECTION → capability_gap=True
    - PERMISSION_ISSUE → capability_gap=True
    """
    pr61: Dict[str, Any] = {
        "number": 61,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [
            {"isResolved": False},
            {"isResolved": False},
            {"isResolved": False},
            {"isResolved": False},
            {"isResolved": False},
        ],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(requires_thread_resolution=True, bot_can_merge=True, auto_merge_enabled=True)

    blocked = classify_blocked_reason(pr61, cap)
    assert blocked == BlockedReason.UNRESOLVED_REVIEW_THREAD

    plan = select_merge_path(pr61, cap, blocked)
    assert isinstance(plan, MergePathPlan)
    assert plan.action == "auto_gemini_triage"
    assert plan.triage_hook == "auto_gemini_triage.triage_pr"

    # invoke_triage_hook 인터페이스 검증 (lazy import — callable 반환만)
    hook = invoke_triage_hook(61)
    assert hook["hook"] == "auto_gemini_triage.triage_pr"
    assert hook["pr_number"] == 61
    assert callable(hook["callable"])

    # REQUIRED_APPROVAL → requires_chair=False (회장 직접 머지 X)
    pr_req: Dict[str, Any] = {
        "number": 61,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "REVIEW_REQUIRED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap_req = _cap(requires_approval=True, bot_can_merge=True, auto_merge_enabled=True)
    blocked_req = classify_blocked_reason(pr_req, cap_req)
    assert blocked_req == BlockedReason.REQUIRED_APPROVAL
    plan_req = select_merge_path(pr_req, cap_req, blocked_req)
    assert plan_req.requires_chair is False, "REQUIRED_APPROVAL: 회장 직접 머지 fallback 없음"

    # BRANCH_PROTECTION → capability_gap=True
    pr_bp: Dict[str, Any] = {
        "number": 61,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap_bp = _cap(requires_thread_resolution=False, requires_approval=False, bot_can_merge=True, auto_merge_enabled=True)
    blocked_bp = classify_blocked_reason(pr_bp, cap_bp)
    assert blocked_bp == BlockedReason.BRANCH_PROTECTION
    plan_bp = select_merge_path(pr_bp, cap_bp, blocked_bp)
    assert plan_bp.capability_gap is True

    # PERMISSION_ISSUE → capability_gap=True
    pr_pi: Dict[str, Any] = {
        "number": 61,
        "mergeStateStatus": "BLOCKED",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap_pi = _cap(bot_can_merge=False, auto_merge_enabled=True)
    blocked_pi = classify_blocked_reason(pr_pi, cap_pi)
    assert blocked_pi == BlockedReason.PERMISSION_ISSUE
    plan_pi = select_merge_path(pr_pi, cap_pi, blocked_pi)
    assert plan_pi.capability_gap is True

    # admin override 차단 검증
    with pytest.raises(RuntimeError):
        assert_no_admin_override(["gh", "pr", "merge", "--admin"])

    # 정상 args는 통과
    assert_no_admin_override(["gh", "pr", "merge", "--squash"])  # no error


def test_pr67_replay_stale_to_base_sync() -> None:
    """(13) PR #67 replay → STALE_BASE → MergePathPlan(action='base_sync', base_sync_command='git merge origin/main'). force push 명령 미포함 검증."""
    pr67: Dict[str, Any] = {
        "number": 67,
        "mergeStateStatus": "BEHIND",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    cap = _cap(requires_thread_resolution=False, requires_approval=False, bot_can_merge=True, auto_merge_enabled=True)

    blocked = classify_blocked_reason(pr67, cap)
    assert blocked == BlockedReason.STALE_BASE

    plan = select_merge_path(pr67, cap, blocked)
    assert plan.action == "base_sync"
    assert plan.base_sync_command == "git merge origin/main"

    # force push 명령 미포함 검증
    assert plan.base_sync_command is not None
    assert "force" not in plan.base_sync_command.lower()
    assert "--force" not in plan.base_sync_command
    assert "-f" not in plan.base_sync_command.split()
    assert "push" not in plan.base_sync_command.lower()


def test_pr68_replay_normal_squash_merge() -> None:
    """(14) PR #68 fixture (task-2517 정상 머지 capability — 모든 필드 충족) → classify=None → MergePathPlan(action='squash_merge', capability_gap=False)."""
    pr68: Dict[str, Any] = {
        "number": 68,
        "mergeStateStatus": "CLEAN",
        "reviewThreads": [],
        "reviewDecision": "APPROVED",
        "statusCheckRollup": {"state": "SUCCESS"},
    }
    # 모든 필드 충족 (task-2517 정상 머지 capability)
    cap = _cap(
        can_squash_merge=True,
        requires_approval=False,
        requires_thread_resolution=False,
        auto_merge_enabled=True,
        bot_can_merge=True,
        admin_override_required=False,
    )

    blocked = classify_blocked_reason(pr68, cap)
    assert blocked is None

    plan = select_merge_path(pr68, cap, blocked)
    assert plan.action == "squash_merge"
    assert plan.capability_gap is False
    assert plan.requires_chair is False
