"""anu_v2.tests.test_owner_trigger_security_boundaries_2554 — 보안 경계 회귀 (task-2554).

system spec §10 #10 + 회장 §8 추가 5건 (#15~#19) 1:1 박제:
  #10 token value가 log에 출력되면 fail
  #15 single allowed endpoint 검증 (POST issues/{n}/comments only)
  #16 forbidden API list hard-block 정적/동적 검증
  #17 OWNER_GEMINI_TRIGGER_TOKEN 외 token 사용 시 fail
  #18 GH_TOKEN fallback fail
  #19 BOT_GITHUB_TOKEN 사용 시 fail
"""

from __future__ import annotations

import json
import logging
import re
import sys
from pathlib import Path

import pytest

WORKSPACE_ROOT = Path(__file__).resolve().parents[2]
if str(WORKSPACE_ROOT) not in sys.path:
    sys.path.insert(0, str(WORKSPACE_ROOT))

from anu_v2.owner_trigger_only import (  # noqa: E402
    FORBIDDEN_ENDPOINT_PATTERNS,
    FORBIDDEN_TOKEN_ENV_NAMES,
    ForbiddenEndpointError,
    OwnerTriggerOnly,
    TOKEN_ENV_NAME,
    TokenBoundaryViolation,
    assert_endpoint_allowed,
    assert_token_boundary,
)
from anu_v2.owner_trigger_audit import OwnerTriggerAudit  # noqa: E402

_HEAD_A = "a" * 40

_SECRET_SENTINEL = "ghp_REAL_OWNER_TOKEN_VALUE_MUST_NEVER_BE_LOGGED_zzzz"


def _write_decision(tmp_path: Path, *, pr: int = 103) -> Path:
    decision = {
        "schema": "anu_v2.owner_trigger_decision.v1",
        "task_id": "task-2554",
        "pr": pr,
        "current_head": _HEAD_A,
        "queue_head": True,
        "current_head_confirmed": True,
        "gemini_evidence_fresh": False,
        "nudge_count_for_pr_head": 0,
        "allowed_action": "POST_GEMINI_REVIEW_TRIGGER_COMMENT",
        "comment_body": "/gemini review",
        "allowed": True,
    }
    p = tmp_path / "decision.json"
    p.write_text(json.dumps(decision), encoding="utf-8")
    return p


def _build_module(tmp_path: Path, *, token: str = _SECRET_SENTINEL):
    posts: list[dict] = []

    def http_post(method: str, path: str, body: dict, headers: dict) -> dict:
        posts.append({"method": method, "path": path, "body": body, "headers": headers})
        return {"status": 201}

    def token_provider() -> str:
        return token

    audit = OwnerTriggerAudit(tmp_path)
    mod = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=token_provider,
        audit=audit,
    )
    return mod, posts, audit


# ─── §10 #10: token value가 log/audit에 출력되면 fail ────────────────────────

def test_token_raw_value_not_in_audit_jsonl(tmp_path):
    decision_path = _write_decision(tmp_path)
    mod, _, audit = _build_module(tmp_path)

    mod.trigger_gemini_review(
        decision_path=decision_path,
        owner="o",
        repo="r",
        current_head_actual=_HEAD_A,
    )
    raw = audit.path.read_text(encoding="utf-8")
    # raw token value 절대 미포함
    assert _SECRET_SENTINEL not in raw
    # token sentinel prefix들 미포함
    for sent in ("ghp_", "github_pat_", "Bearer ", "ghu_", "ghs_", "ghr_"):
        assert sent not in raw, f"audit contains token sentinel {sent!r}"


def test_token_raw_value_not_in_capsys_or_caplog(tmp_path, capsys, caplog):
    decision_path = _write_decision(tmp_path)
    mod, _, _ = _build_module(tmp_path)

    caplog.set_level(logging.DEBUG)
    mod.trigger_gemini_review(
        decision_path=decision_path,
        owner="o",
        repo="r",
        current_head_actual=_HEAD_A,
    )
    captured = capsys.readouterr()
    # stdout / stderr / log records 모두 raw value 미출력
    assert _SECRET_SENTINEL not in captured.out
    assert _SECRET_SENTINEL not in captured.err
    for record in caplog.records:
        assert _SECRET_SENTINEL not in str(record.getMessage())
    # 보너스: hash_prefix 만 audit에 남는다
    # (audit 검증은 위 테스트에서 이미 함)


def test_module_source_has_no_hardcoded_token_value():
    """모듈 source 정적 검사: 'ghp_' / 'github_pat_' / 'Bearer ' 패턴이 실제 token처럼 하드코딩 X."""
    src = (WORKSPACE_ROOT / "anu_v2" / "owner_trigger_only.py").read_text(encoding="utf-8")
    # 'Bearer ' 는 헤더 빌드용 prefix로만 등장 (literal "Bearer {token}") — 실 값 0
    # 'ghp_' / 'github_pat_' 는 등장하면 안 됨
    assert "ghp_" not in src
    assert "github_pat_" not in src


# ─── §8 추가 #15: single allowed endpoint 검증 ────────────────────────────────

def test_only_one_allowed_endpoint_path_regex_static():
    """static check: _ALLOWED_PATH_RE 은 POST issues/{n}/comments 만 매치."""
    from anu_v2.owner_trigger_only import _ALLOWED_PATH_RE  # type: ignore[attr-defined]

    # match
    assert _ALLOWED_PATH_RE.match("/repos/o/r/issues/103/comments")
    # non-match (수많은 후보)
    for path in [
        "/repos/o/r/issues/103",
        "/repos/o/r/issues/103/labels",
        "/repos/o/r/pulls/103/comments",
        "/repos/o/r/pulls/103/reviews",
        "/repos/o/r/pulls/103/merge",
        "/repos/o/r/issues/comments",
        "/repos/o/r/issues/103/comments/1",
    ]:
        assert not _ALLOWED_PATH_RE.match(path), f"path should NOT match: {path}"


def test_single_endpoint_runtime_one_call_per_trigger(tmp_path):
    decision_path = _write_decision(tmp_path)
    mod, posts, _ = _build_module(tmp_path)
    mod.trigger_gemini_review(
        decision_path=decision_path,
        owner="o",
        repo="r",
        current_head_actual=_HEAD_A,
    )
    assert len(posts) == 1
    call = posts[0]
    assert call["method"] == "POST"
    assert re.match(r"^/repos/[^/]+/[^/]+/issues/\d+/comments$", call["path"])


# ─── §8 추가 #16: forbidden API list hard-block 정적/동적 검증 ───────────────

def test_forbidden_11_endpoints_static_pattern_completeness():
    """11 패턴 모두 정의됨을 정적 검증."""
    # 회장 §7 명시 11 endpoint — 본 모듈 FORBIDDEN_ENDPOINT_PATTERNS 와 1:1 매핑.
    # 본 테스트는 패턴 수만 확인 (구체 케이스는 별 test에서 실측).
    assert len(FORBIDDEN_ENDPOINT_PATTERNS) >= 10  # close/reopen은 patch on pulls/{n}과 통합


def test_forbidden_endpoints_dynamic_block_each_case():
    """동적 호출 시 11 endpoint 케이스 모두 ForbiddenEndpointError raise."""
    forbidden_cases = [
        ("POST", "/repos/o/r/pulls/103/merge"),
        ("POST", "/repos/o/r/pulls/103/reviews"),
        ("PATCH", "/repos/o/r/git/refs/heads/main"),
        ("POST", "/repos/o/r/git/refs"),
        ("PUT", "/repos/o/r/contents/file.py"),
        ("PATCH", "/repos/o/r/issues/103"),
        ("POST", "/repos/o/r/issues/103/labels"),
        ("DELETE", "/repos/o/r/branches/main"),
        ("POST", "/repos/o/r/actions/runs/1/rerun"),
        ("POST", "/repos/o/r/check-runs/1/rerequest"),
        ("PATCH", "/repos/o/r/pulls/103"),  # close/reopen via PATCH
    ]
    for method, path in forbidden_cases:
        with pytest.raises(ForbiddenEndpointError):
            assert_endpoint_allowed(method, path)


def test_non_post_method_blocked():
    with pytest.raises(ForbiddenEndpointError):
        assert_endpoint_allowed("GET", "/repos/o/r/issues/103/comments")
    with pytest.raises(ForbiddenEndpointError):
        assert_endpoint_allowed("PATCH", "/repos/o/r/issues/103/comments")


def test_module_source_has_no_call_to_forbidden_endpoints():
    """정적 grep: owner_trigger_only.py 본문에 'pulls/.../merge' 등 금지 string literal 없음.

    유일한 endpoint string 는 ``_build_post_comment_path`` 의 f-string이며
    ``/repos/{owner}/{repo}/issues/{pr_number}/comments`` 만 사용.
    """
    src = (WORKSPACE_ROOT / "anu_v2" / "owner_trigger_only.py").read_text(encoding="utf-8")
    # 본 모듈 source는 endpoint URL을 1 곳에서만 빌드한다.
    # FORBIDDEN_ENDPOINT_PATTERNS 정의용 regex literal 외에 실제 호출 endpoint 빌드 0건.
    # 단순 grep: f"/repos/{owner}/{repo}/pulls/" 같은 빌더가 없는지.
    forbidden_url_builders = [
        '/pulls/{pr',
        '/pulls/{number',
        'pulls/{n}/merge',
        '/git/refs/heads',
        '/contents/',
        '/labels"',
        '/labels{',
        '/actions/runs/',
    ]
    # 본 source 내에서 endpoint를 빌드하는 f-string 은 _build_post_comment_path 한 군데
    assert src.count('def _build_post_comment_path(') == 1
    # /issues/{pr_number}/comments 등장 위치: docstring 1 + builder f-string 1 == 정확히 2
    assert src.count('/issues/{pr_number}/comments') == 2
    # return f"..." 위치 1건만
    assert src.count('return f"/repos/') == 1
    # forbidden URL builder string 명시 차단 (regex 패턴 정의는 별 영역)
    forbidden_url_builders = [
        '/pulls/{pr',
        'pulls/{n}/merge',
        '/git/refs/heads/{',
        '/contents/{',
    ]
    for f in forbidden_url_builders:
        # regex 정의 영역에는 등장하지 않으며 builder 코드에도 없음
        assert f not in src, f"forbidden URL builder pattern found: {f}"


# ─── §8 추가 #17: OWNER_GEMINI_TRIGGER_TOKEN 외 token 사용 시 fail ──────────

def test_token_env_name_constant_is_owner_gemini_trigger_token():
    assert TOKEN_ENV_NAME == "OWNER_GEMINI_TRIGGER_TOKEN"


def test_forbidden_token_env_list_includes_bot_and_gh_token():
    assert "BOT_GITHUB_TOKEN" in FORBIDDEN_TOKEN_ENV_NAMES
    assert "GH_TOKEN" in FORBIDDEN_TOKEN_ENV_NAMES
    assert "GITHUB_TOKEN" in FORBIDDEN_TOKEN_ENV_NAMES
    assert "OWNER_PAT" in FORBIDDEN_TOKEN_ENV_NAMES


def test_assert_token_boundary_blocks_owner_pat_injection():
    with pytest.raises(TokenBoundaryViolation):
        assert_token_boundary({"OWNER_PAT": "x"})


# ─── §8 추가 #18: GH_TOKEN fallback fail ──────────────────────────────────────

def test_gh_token_injection_fails(tmp_path):
    decision_path = _write_decision(tmp_path)
    mod, posts, _ = _build_module(tmp_path)
    with pytest.raises(TokenBoundaryViolation):
        mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual=_HEAD_A,
            env_override={"GH_TOKEN": "fallback-x"},
        )
    assert posts == []


def test_github_token_injection_fails(tmp_path):
    decision_path = _write_decision(tmp_path)
    mod, posts, _ = _build_module(tmp_path)
    with pytest.raises(TokenBoundaryViolation):
        mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual=_HEAD_A,
            env_override={"GITHUB_TOKEN": "x"},
        )
    assert posts == []


# ─── §8 추가 #19: BOT_GITHUB_TOKEN 사용 시 fail (owner trigger path에서) ────

def test_bot_github_token_injection_fails(tmp_path):
    decision_path = _write_decision(tmp_path)
    mod, posts, _ = _build_module(tmp_path)
    with pytest.raises(TokenBoundaryViolation):
        mod.trigger_gemini_review(
            decision_path=decision_path,
            owner="o",
            repo="r",
            current_head_actual=_HEAD_A,
            env_override={"BOT_GITHUB_TOKEN": "bot-merge-token"},
        )
    assert posts == []


def test_module_source_does_not_use_bot_github_token():
    """정적 grep: BOT_GITHUB_TOKEN 환경변수를 owner_trigger_only.py가 사용하지 않음.

    유일한 등장 위치는 FORBIDDEN_TOKEN_ENV_NAMES tuple 와 docstring/주석 (boundary 설명용).
    실제 ``os.environ['BOT_GITHUB_TOKEN']`` / ``os.getenv('BOT_GITHUB_TOKEN')`` 호출 0건.
    """
    src = (WORKSPACE_ROOT / "anu_v2" / "owner_trigger_only.py").read_text(encoding="utf-8")
    assert 'os.environ["BOT_GITHUB_TOKEN"]' not in src
    assert "os.environ['BOT_GITHUB_TOKEN']" not in src
    assert 'os.getenv("BOT_GITHUB_TOKEN")' not in src
    assert "os.getenv('BOT_GITHUB_TOKEN')" not in src


def test_module_source_does_not_read_gh_token_or_github_token():
    src = (WORKSPACE_ROOT / "anu_v2" / "owner_trigger_only.py").read_text(encoding="utf-8")
    for name in ("GH_TOKEN", "GITHUB_TOKEN", "OWNER_PAT"):
        # 단순 string 등장은 FORBIDDEN_TOKEN_ENV_NAMES 정의/주석에서만 허용
        # os.environ[name] / os.getenv(name) 호출 0건
        assert f'os.environ["{name}"]' not in src
        assert f"os.environ['{name}']" not in src
        assert f'os.getenv("{name}")' not in src
        assert f"os.getenv('{name}')" not in src
