"""anu_v2.tests.test_owner_trigger_403_token_scope_mismatch_classification — task-2557 §Test-1.

회장 §명시 2026-05-12 Track C Rank 1 1:1 박제:
  PR #107 attempt-4 raw 403 evidence (``owner_trigger_403_dual_scope_missing.json``)
  fixture 1 건을 로드해 4 가지 회귀를 영구 catch.

회귀 catch 4 종:
  1. evidence schema = ``anu_v2.owner_trigger_evidence.owner-trigger-failed.v2``
  2. ``response_headers["x-accepted-github-permissions"]`` 파싱 (issues=write; pull_requests=write)
  3. ``missing_permission`` diff 자동 계산 = ``pull_requests=write`` (1 건)
  4. classification = ``TOKEN_SCOPE_MISMATCH_DIAGNOSED`` (Critical 7 #6)

본 test 는 fixture 기반 evidence 회귀만 수행 (owner_trigger_only / executor_scheduler 모듈 코드
변경 0 — 회장 §명시 task-2557 금지 12 #12 1:1).
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest


FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures"
FIXTURE_PATH = FIXTURES_DIR / "owner_trigger_403_dual_scope_missing.json"

EXPECTED_SCHEMA = "anu_v2.owner_trigger_evidence.owner-trigger-failed.v2"
EXPECTED_CLASSIFICATION = "TOKEN_SCOPE_MISMATCH_DIAGNOSED"
EXPECTED_MISSING_PERMISSION = "pull_requests=write"
EXPECTED_ENDPOINT = "/repos/Jeon-Jonghyuk/dev_workspace/issues/107/comments"
EXPECTED_TOKEN_HASH_PREFIX = "a9e05574"
EXPECTED_HEAD = "e03f536ad7ad626f1a2afca080d1952588f53a71"


# ─── helpers ──────────────────────────────────────────────────────────────────


def _load_fixture() -> dict:
    return json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))


def _parse_accepted_permissions(header_value: str) -> list[str]:
    """``X-Accepted-GitHub-Permissions`` 헤더 파싱.

    GitHub 응답에서 본 헤더는 ``issues=write; pull_requests=write`` 형식.
    세미콜론 구분, 각 토큰은 ``<resource>=<level>``.
    """
    if not isinstance(header_value, str) or not header_value:
        return []
    return [tok.strip() for tok in header_value.split(";") if tok.strip()]


def _compute_missing_permission_diff(
    required: list[str], granted: list[str]
) -> list[str]:
    """granted 에 없는 required 권한만 반환. 입력 순서 보존."""
    granted_set = set(granted)
    return [perm for perm in required if perm not in granted_set]


# ─── tests ────────────────────────────────────────────────────────────────────


def test_fixture_loads_and_schema_matches_owner_trigger_failed_v2():
    """fixture 1 의 schema 가 owner-trigger-failed.v2 임을 박제."""
    fx = _load_fixture()
    assert fx["schema"] == EXPECTED_SCHEMA
    assert fx["http_status"] == 403
    assert fx["pr"] == 107
    assert fx["attempt"] == 4
    assert fx["head"] == EXPECTED_HEAD
    assert fx["endpoint"] == EXPECTED_ENDPOINT
    assert fx["method"] == "POST"
    # token redaction 정책 — raw token value 미포함, hash prefix + present 플래그만.
    assert fx["token_value_logged"] is False
    assert fx["token_present"] is True
    assert fx["token_hash_prefix"] == EXPECTED_TOKEN_HASH_PREFIX
    assert len(fx["token_hash_prefix"]) == 8


def test_response_headers_x_accepted_github_permissions_parses_dual_scope():
    """response_headers 에 x-accepted-github-permissions 가 dual scope (issues + pull_requests) 로 박제."""
    fx = _load_fixture()
    headers = fx["response_headers"]
    assert isinstance(headers, dict)
    raw = headers.get("x-accepted-github-permissions")
    assert raw == "issues=write; pull_requests=write"

    parsed = _parse_accepted_permissions(raw)
    assert parsed == ["issues=write", "pull_requests=write"]
    # top-level mirror — runner 가 별도 접근하는 표준 필드.
    assert fx["x_accepted_github_permissions"] == raw


def test_missing_permission_diff_auto_computed_pull_requests_write_only():
    """missing_permission diff 자동 계산 — issues=write 는 granted, pull_requests=write 만 missing."""
    fx = _load_fixture()
    required = fx["permissions_required_set"]
    granted = fx["permissions_currently_granted_set"]
    assert "issues=write" in granted
    assert "pull_requests=write" not in granted
    assert "issues=write" in required
    assert "pull_requests=write" in required

    diff = _compute_missing_permission_diff(required, granted)
    assert diff == ["pull_requests=write"]
    assert len(diff) == 1
    # fixture 의 미리 계산된 결과와 1:1 일치.
    assert fx["missing_permission"] == EXPECTED_MISSING_PERMISSION
    assert fx["missing_permissions_list"] == diff


def test_classification_token_scope_mismatch_diagnosed_critical_seven_6():
    """classification = TOKEN_SCOPE_MISMATCH_DIAGNOSED (Critical 7 #6) 박제."""
    fx = _load_fixture()
    assert fx["classification"] == EXPECTED_CLASSIFICATION
    assert fx["critical_seven_code"] == EXPECTED_CLASSIFICATION
    # 진단 조건: 403 + x-accepted-github-permissions 헤더 존재 + missing diff != 0.
    assert fx["http_status"] == 403
    assert fx.get("x_accepted_github_permissions")
    missing = _compute_missing_permission_diff(
        fx["permissions_required_set"],
        fx["permissions_currently_granted_set"],
    )
    assert len(missing) >= 1
    # http_response_message 가 GitHub PAT 권한 부족 패턴 1:1.
    assert "Resource not accessible by personal access token" in fx["http_response_message"]


def test_documentation_url_points_to_github_rest_issue_comments():
    """documentation_url 박제 — GitHub 응답 본체 그대로 보존 (회귀 시 endpoint URL 변경 catch)."""
    fx = _load_fixture()
    assert fx["documentation_url"].startswith("https://docs.github.com/rest/issues/comments")


def test_expected_resolution_describes_owner_ui_permission_add_only():
    """expected_resolution 박제 — token regenerate 불필요, owner UI 권한 추가만."""
    fx = _load_fixture()
    resolution = fx["expected_resolution"]
    assert "Pull requests" in resolution
    assert "Read and write" in resolution
    assert "token regenerate 불필요" in resolution


@pytest.mark.parametrize(
    "field",
    [
        "schema",
        "task_id",
        "attempt",
        "pr",
        "head",
        "ts",
        "endpoint",
        "method",
        "http_status",
        "http_response_message",
        "response_headers",
        "documentation_url",
        "x_accepted_github_permissions",
        "currently_set_per_owner_ui",
        "token_present",
        "token_hash_prefix",
        "token_value_logged",
        "missing_permission",
        "missing_permissions_list",
        "permissions_required_set",
        "permissions_currently_granted_set",
        "classification",
        "critical_seven_code",
        "comment_body_attempted",
        "expected_resolution",
        "lesson_pinned",
    ],
)
def test_owner_trigger_failed_v2_required_fields_present(field):
    """schema owner-trigger-failed.v2 필수 필드 누락 회귀 catch."""
    fx = _load_fixture()
    assert field in fx, f"required field missing in owner-trigger-failed.v2 fixture: {field}"
