"""anu_v2.tests.test_owner_trigger_http_post_wiring_2699 — 검증 8 시나리오 (task-2699).

아르고스(QA 테스터) 작성. 불칸 구현:
  - anu_v2/owner_trigger_http_post.py  (make_owner_trigger_token_provider, make_production_http_post)
  - anu_v2/owner_trigger_entry.py      (build_runner, run_single, build_scheduler, run_scheduler_cycle)

★ 실제 OWNER PAT 로 live POST 절대 금지 — mock/dry-run/주입 callable 만 사용.
"""

from __future__ import annotations

import json
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Callable

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_http_post import (  # noqa: E402
    GITHUB_API_BASE,
    OwnerTriggerHttpError,
    make_owner_trigger_token_provider,
    make_production_http_post,
)
from anu_v2.owner_trigger_only import (  # noqa: E402
    COMMENT_BODY,
    TOKEN_ENV_NAME,
    ForbiddenEndpointError,
    OwnerTriggerOnly,
    TokenBoundaryViolation,
    invoke_from_scheduler,
)
from anu_v2.owner_trigger_audit import OwnerTriggerAudit  # noqa: E402
from anu_v2 import owner_trigger_entry  # noqa: E402


# ─── 공용 헬퍼 ─────────────────────────────────────────────────────────────────

_HEAD_A = "a" * 40
_HEAD_B = "b" * 40

_OWNER = "Jeon-Jonghyuk"
_REPO = "dev_workspace"
_PR = 42
_TASK_ID = "task-2699"
_SECRET_TOKEN = "ghp_TESTONLY_ARGOS_MUST_NOT_LEAK_2699abc"


def _write_decision(
    tmp_path: Path,
    *,
    pr: int = _PR,
    head: str = _HEAD_A,
    queue_head: bool = True,
    current_head_confirmed: bool = True,
    gemini_evidence_fresh: bool = False,
    nudge_count_for_pr_head: int = 0,
    allowed_action: str = "POST_GEMINI_REVIEW_TRIGGER_COMMENT",
    comment_body: str = "/gemini review",
    allowed: bool = True,
    task_id: str = _TASK_ID,
    filename: str = "decision.json",
) -> Path:
    """유효 decision JSON v1 파일 생성 (validate_decision PASS 조건 충족)."""
    decision = {
        "schema": "anu_v2.owner_trigger_decision.v1",
        "task_id": task_id,
        "pr": pr,
        "current_head": head,
        "queue_head": queue_head,
        "current_head_confirmed": current_head_confirmed,
        "gemini_evidence_fresh": gemini_evidence_fresh,
        "nudge_count_for_pr_head": nudge_count_for_pr_head,
        "allowed_action": allowed_action,
        "comment_body": comment_body,
        "allowed": allowed,
    }
    path = tmp_path / filename
    path.write_text(json.dumps(decision), encoding="utf-8")
    return path


def _make_mock_http_post(posts: list[dict] | None = None) -> tuple[list[dict], Callable]:
    """호출 기록 mock http_post. (posts, callable) 반환."""
    if posts is None:
        posts = []

    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, "id": 9999}

    return posts, http_post


def _make_fixed_token_provider(token: str = _SECRET_TOKEN) -> Callable[[], str]:
    def _provider() -> str:
        return token
    return _provider


def _build_runner(
    tmp_path: Path,
    *,
    posts: list[dict] | None = None,
    token: str = _SECRET_TOKEN,
) -> tuple[OwnerTriggerOnly, list[dict]]:
    """OwnerTriggerOnly + posts 리스트 반환 (기존 테스트 패턴 동일 스타일)."""
    if posts is None:
        posts = []
    _, http_post = _make_mock_http_post(posts)
    audit = OwnerTriggerAudit(workspace_root=tmp_path)
    runner = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=_make_fixed_token_provider(token),
        audit=audit,
    )
    return runner, posts


# ─── 시나리오 1: mock http_post POST 정확히 1회 ───────────────────────────────

def test_mock_http_post_posts_once(tmp_path):
    """decision 8조건 충족 → mock http_post로 trigger → POST 정확히 1회, 인자 검증, status=="POSTED"."""
    decision_path = _write_decision(tmp_path)
    posts: list[dict] = []
    _, http_post = _make_mock_http_post(posts)
    audit = OwnerTriggerAudit(workspace_root=tmp_path)
    runner = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=_make_fixed_token_provider(),
        audit=audit,
    )

    result = runner.trigger_gemini_review(
        decision_path=decision_path,
        owner=_OWNER,
        repo=_REPO,
        current_head_actual=_HEAD_A,
    )

    # POST 정확히 1회
    assert len(posts) == 1, f"POST call count should be 1, got {len(posts)}"

    call = posts[0]
    # method == "POST"
    assert call["method"] == "POST"
    # body == {"body": "/gemini review"}
    assert call["body"] == {"body": "/gemini review"}
    # endpoint == /repos/{owner}/{repo}/issues/{pr}/comments
    expected_endpoint = f"/repos/{_OWNER}/{_REPO}/issues/{_PR}/comments"
    assert call["path"] == expected_endpoint, f"endpoint mismatch: {call['path']!r}"

    # status == "POSTED"
    assert result.status == "POSTED"
    assert result.pr == _PR
    assert result.head == _HEAD_A


# ─── 시나리오 2: dry_run — 실제 네트워크 0 ────────────────────────────────────

def test_dry_run_no_actual_post(tmp_path, monkeypatch):
    """make_production_http_post(dry_run=True) → dry_run dict, status==0, urlopen 호출 0."""
    # urlopen 을 예외 raise 하도록 패치 — dry_run 이면 이 호출이 발생하면 안 됨
    def _forbidden_urlopen(*args, **kwargs):
        raise AssertionError("urlopen must NOT be called in dry_run mode")

    monkeypatch.setattr(urllib.request, "urlopen", _forbidden_urlopen)

    http_post = make_production_http_post(dry_run=True)
    result = http_post(
        "POST",
        "/repos/o/r/issues/1/comments",
        {"body": "/gemini review"},
        {},
    )

    assert result.get("dry_run") is True, "dry_run flag should be True"
    assert result.get("status") == 0, f"dry_run status should be 0, got {result.get('status')}"
    # monkeypatch 로 urlopen 을 예외로 치환했지만 dry_run 경로는 urlopen 우회 → 예외 없이 통과


# ─── 시나리오 3: 동일 (pr, head) dedupe — 2회차 DEDUPED ───────────────────────

def test_dedupe_same_pr_head_second_deduped(tmp_path):
    """동일 (pr, head) decision 2회 trigger → 1회차 POSTED, 2회차 DEDUPED, mock POST 총 1회."""
    decision_path = _write_decision(tmp_path)
    posts: list[dict] = []
    runner, posts = _build_runner(tmp_path, posts=posts)

    # 1회차
    r1 = runner.trigger_gemini_review(
        decision_path=decision_path,
        owner=_OWNER,
        repo=_REPO,
        current_head_actual=_HEAD_A,
    )
    assert r1.status == "POSTED"
    assert len(posts) == 1, "1st trigger: 1 POST expected"

    # 2회차 — 동일 pr, head → DEDUPED
    r2 = runner.trigger_gemini_review(
        decision_path=decision_path,
        owner=_OWNER,
        repo=_REPO,
        current_head_actual=_HEAD_A,
    )
    assert r2.status == "DEDUPED"
    # mock POST 여전히 1회 (2회차 POST 0)
    assert len(posts) == 1, f"2nd trigger must NOT call POST again, total calls: {len(posts)}"


# ─── 시나리오 4: forbidden endpoint/method raise ForbiddenEndpointError ────────

def test_forbidden_endpoint_raises(tmp_path, monkeypatch):
    """make_production_http_post() callable에 merge/pulls endpoint/GET method → ForbiddenEndpointError."""
    # network 호출 도달 전에 assert_endpoint_allowed 가 차단 — urlopen 패치 불필요하지만 보험
    def _forbidden_urlopen(*args, **kwargs):
        raise AssertionError("urlopen should NOT be reached after ForbiddenEndpointError")

    monkeypatch.setattr(urllib.request, "urlopen", _forbidden_urlopen)

    http_post = make_production_http_post()

    # merge endpoint
    with pytest.raises(ForbiddenEndpointError):
        http_post("POST", "/repos/o/r/pulls/1/merge", {}, {})

    # reviews endpoint
    with pytest.raises(ForbiddenEndpointError):
        http_post("POST", "/repos/o/r/pulls/1/reviews", {}, {})

    # method != POST (예: GET)
    with pytest.raises(ForbiddenEndpointError):
        http_post("GET", "/repos/o/r/issues/1/comments", {}, {})

    # method DELETE
    with pytest.raises(ForbiddenEndpointError):
        http_post("DELETE", "/repos/o/r/issues/1/comments", {}, {})


# ─── 시나리오 5: token 부재 → fail-closed, BOT_GITHUB_TOKEN fallback 0 ─────────

def test_token_unavailable_fail_closed(tmp_path):
    """make_owner_trigger_token_provider(env={}) → TokenBoundaryViolation.
    OwnerTriggerOnly 에 주입 시 trigger → POST 0 (fail-closed).
    BOT_GITHUB_TOKEN 이 env 에 있어도 fallback 안 함.
    """
    # 1) 빈 env → TokenBoundaryViolation
    provider_empty = make_owner_trigger_token_provider(env={})
    with pytest.raises(TokenBoundaryViolation):
        provider_empty()

    # 2) BOT_GITHUB_TOKEN 만 있어도 fallback 안 함 — 여전히 TokenBoundaryViolation
    provider_bot_only = make_owner_trigger_token_provider(env={"BOT_GITHUB_TOKEN": "x"})
    with pytest.raises(TokenBoundaryViolation):
        provider_bot_only()

    # 3) GH_TOKEN fallback 0
    provider_gh_only = make_owner_trigger_token_provider(env={"GH_TOKEN": "y"})
    with pytest.raises(TokenBoundaryViolation):
        provider_gh_only()

    # 4) OwnerTriggerOnly 에 fail-closed token_provider 주입 → POST 0
    decision_path = _write_decision(tmp_path)
    posts: list[dict] = []
    _, http_post = _make_mock_http_post(posts)
    audit = OwnerTriggerAudit(workspace_root=tmp_path)
    runner = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=http_post,
        token_provider=provider_empty,
        audit=audit,
    )
    with pytest.raises(TokenBoundaryViolation):
        runner.trigger_gemini_review(
            decision_path=decision_path,
            owner=_OWNER,
            repo=_REPO,
            current_head_actual=_HEAD_A,
        )
    # POST 0 (fail-closed)
    assert len(posts) == 0, f"POST must not be called when token unavailable, got {len(posts)}"


# ─── 시나리오 6: token 누출 금지 (audit + 예외 메시지) ────────────────────────

def test_no_token_leak_in_audit_and_errors(tmp_path, monkeypatch):
    """mock http_post 예외 시 audit JSONL + 예외 메시지에 token raw value 0건.
    make_production_http_post 로 HTTPError → OwnerTriggerHttpError 메시지에 token/Auth/Bearer raw value 미포함.
    """
    raw_token = _SECRET_TOKEN  # 이 값이 audit/예외 메시지에 노출되면 FAIL

    # --- Part A: mock http_post 예외 시 audit에 token raw value 0 ---
    exc_msg_container: list[str] = []

    def raising_http_post(method, path, body, headers):
        raise RuntimeError("network error (simulated)")

    audit = OwnerTriggerAudit(workspace_root=tmp_path)
    runner = OwnerTriggerOnly(
        workspace_root=tmp_path,
        http_post=raising_http_post,
        token_provider=_make_fixed_token_provider(raw_token),
        audit=audit,
    )
    decision_path = _write_decision(tmp_path)

    raised_exc = None
    try:
        runner.trigger_gemini_review(
            decision_path=decision_path,
            owner=_OWNER,
            repo=_REPO,
            current_head_actual=_HEAD_A,
        )
    except Exception as exc:
        raised_exc = exc

    # 예외가 발생해야 함 (http_post 실패)
    assert raised_exc is not None, "Exception expected when http_post raises"

    # 예외 메시지에 raw token 값 0
    exc_message = str(raised_exc)
    assert raw_token not in exc_message, (
        f"Token leaked in exception message: {exc_message!r}"
    )

    # audit JSONL 에 raw token 값 0
    if audit.path.exists():
        audit_content = audit.path.read_text(encoding="utf-8")
        assert raw_token not in audit_content, (
            f"Token leaked in audit JSONL: {audit_content[:200]!r}"
        )
        # "ghp_" prefix 0 (raw token 포함 여부)
        # note: 실제 ghp_ prefix 토큰이면 체크, 여기서는 테스트 토큰 prefix 그대로 검색
        assert "MUST_NOT_LEAK" not in audit_content, (
            "Token sentinel leaked in audit JSONL"
        )

    # --- Part B: make_production_http_post + HTTPError → OwnerTriggerHttpError ---
    def _mock_opener_that_raises(req, timeout=None):
        raise urllib.error.HTTPError(
            url=req.full_url,
            code=403,
            msg="Forbidden",
            hdrs={},
            fp=None,
        )

    class _MockOpener:
        def open(self, req, timeout=None):
            return _mock_opener_that_raises(req, timeout=timeout)

    http_post_prod = make_production_http_post(opener=_MockOpener())

    owner_trigger_http_error = None
    try:
        http_post_prod(
            "POST",
            "/repos/o/r/issues/1/comments",
            {"body": "/gemini review"},
            {"Authorization": f"Bearer {raw_token}"},  # headers 에 token 포함 — 하지만 예외메시지엔 미포함
        )
    except OwnerTriggerHttpError as exc:
        owner_trigger_http_error = exc

    assert owner_trigger_http_error is not None, "OwnerTriggerHttpError expected on HTTPError"

    http_err_msg = str(owner_trigger_http_error)

    # 예외 메시지에 token raw value 0
    assert raw_token not in http_err_msg, (
        f"Token leaked in OwnerTriggerHttpError message: {http_err_msg!r}"
    )
    # "Authorization" / "Bearer" 문자열 0 (헤더 값 노출 금지)
    assert "Authorization" not in http_err_msg, (
        f"'Authorization' leaked in OwnerTriggerHttpError: {http_err_msg!r}"
    )
    # "Bearer " 뒤 token raw value — raw_token 체크로 이미 커버되지만 명시
    # "Bearer" 단어 자체가 포함되면 token value 누출 우려 (f-string 잔재 차단)
    assert "Bearer" not in http_err_msg, (
        f"'Bearer' leaked in OwnerTriggerHttpError: {http_err_msg!r}"
    )

    # 메시지에 status code + endpoint path 만 포함 (보안 원칙 §3)
    assert "403" in http_err_msg or "status=" in http_err_msg, (
        f"Expected status code in error message: {http_err_msg!r}"
    )


# ─── 시나리오 7: entry point run_single injection ────────────────────────────

def test_entry_point_run_single_with_injection(tmp_path):
    """owner_trigger_entry.run_single(…) — mock http_post + token_provider 주입 → POSTED, POST 1회."""
    decision_path = _write_decision(tmp_path)
    posts: list[dict] = []
    _, http_post = _make_mock_http_post(posts)

    status = owner_trigger_entry.run_single(
        workspace_root=tmp_path,
        decision_path=decision_path,
        owner=_OWNER,
        repo=_REPO,
        current_head_actual=_HEAD_A,
        http_post=http_post,
        token_provider=_make_fixed_token_provider(),
    )

    assert status == "POSTED", f"Expected POSTED, got {status!r}"
    assert len(posts) == 1, f"Expected 1 POST, got {len(posts)}"
    call = posts[0]
    assert call["method"] == "POST"
    assert call["body"] == {"body": "/gemini review"}
    assert f"/issues/{_PR}/comments" in call["path"]


def test_entry_point_build_scheduler_smoke(tmp_path):
    """build_scheduler(…, snapshot_provider, merge_executor) — 인스턴스 생성 smoke 검증."""
    from anu_v2.executor_scheduler import ExecutorScheduler
    from anu_v2.idle_pr_diagnoser import IdlePRSnapshot
    from anu_v2.merge_queue_executor import MergeQueueExecutor

    def _snapshot_provider():
        return []  # 빈 리스트 — PR 없음

    # merge_executor mock (build_scheduler에 주입하면 production subprocess 실행 안 함)
    posts: list[dict] = []
    _, http_post = _make_mock_http_post(posts)

    # MergeQueueExecutor 를 None 으로 두면 production subprocess 빌더가 실행됨 →
    # 테스트에서는 주입 가능한 stub 사용
    def _noop_gh(*args, **kwargs):
        import subprocess
        return subprocess.CompletedProcess(args=[], returncode=0, stdout="[]", stderr="")

    def _noop_git(*args, **kwargs):
        import subprocess
        return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")

    def _noop_pytest(*args, **kwargs):
        return 0

    def _noop_audit(record):
        pass

    merge_exec = MergeQueueExecutor(
        gh_runner=_noop_gh,
        git_runner=_noop_git,
        pytest_runner=_noop_pytest,
        audit_writer=_noop_audit,
        task_md_root=tmp_path / "tasks",
    )

    scheduler = owner_trigger_entry.build_scheduler(
        workspace_root=tmp_path,
        decision_dir=tmp_path / "decisions",
        owner=_OWNER,
        repo=_REPO,
        snapshot_provider=_snapshot_provider,
        http_post=http_post,
        token_provider=_make_fixed_token_provider(),
        merge_executor=merge_exec,
    )

    assert isinstance(scheduler, ExecutorScheduler), (
        f"build_scheduler should return ExecutorScheduler, got {type(scheduler).__name__}"
    )


# ─── 시나리오 8: 기존 owner_trigger 회귀 — 별도 test 함수 없음 (아래 참고) ─────
# 기존 테스트 회귀는 검증 명령으로 확인:
#   python3 -m pytest anu_v2/tests/ -k "owner_trigger" -q
# 본 파일에 명시 기록 (시나리오 8).

def test_existing_owner_trigger_regression_placeholder():
    """시나리오 8: 기존 owner_trigger 테스트 회귀 확인용 placeholder.

    실제 회귀 검증은 pytest 명령으로 수행:
      python3 -m pytest anu_v2/tests/ -k "owner_trigger" -q

    본 함수는 이 사실을 기록/문서화하며, PASS 만 한다.
    신규 시나리오 1~7 PASS + 기존 owner_trigger 전체 PASS 확인 필요.
    """
    # 기존 테스트가 깨지지 않음을 보고서에 명시 — PASS
    assert True
