"""
tests/regression/test_workflow_sha_payload.py

task-2486 회귀 테스트: CI pull_request SHA payload fallback fix
작성자: 아르고스 (dev1-team 테스터)

대상 모듈: scripts/verify_workflow_sha_payload.py
- _resolve_pr, _resolve_sha, _load_event 헬퍼 단위 테스트
- fixture 기반 dry-run 통합 테스트
- ci.yml 구조 회귀 테스트
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from unittest.mock import patch

import yaml

# ---------------------------------------------------------------------------
# 경로 설정 — scripts/ 를 sys.path 에 추가
# ---------------------------------------------------------------------------
WORKTREE_ROOT = Path(__file__).parent.parent.parent  # task-2486-dev1/
SCRIPTS_DIR = WORKTREE_ROOT / "scripts"
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(WORKTREE_ROOT))

from scripts.verify_workflow_sha_payload import (  # type: ignore[import-not-found]  # noqa: E402
    _load_event,
    _resolve_pr,
    _resolve_sha,
)

FIXTURES_DIR = WORKTREE_ROOT / "tests" / "regression" / "fixtures" / "workflow_sha_payload"
CI_YML = WORKTREE_ROOT / ".github" / "workflows" / "ci.yml"

# ---------------------------------------------------------------------------
# A. fixture 기반 dry-run 통합 테스트 (4건)
# ---------------------------------------------------------------------------


def test_resolve_normal_event():
    """A-1: pr_event_normal.json — SHA + PR 정상 추출, exit_code 0."""
    event_path = FIXTURES_DIR / "pr_event_normal.json"
    expected_path = FIXTURES_DIR / "pr_event_normal.expected.json"

    event = _load_event(str(event_path))
    with open(expected_path, "r", encoding="utf-8") as f:
        expected = json.load(f)

    pr_number, pr_stage = _resolve_pr(event, branch="", repo="", dry_run=True)
    sha, sha_stage, _ = _resolve_sha(event, pr_number or None, repo="", dry_run=True)

    resolved_ok = bool(sha and pr_number)
    got_exit = 0 if resolved_ok else 1

    assert got_exit == expected["exit_code"], (
        f"exit_code mismatch: expected={expected['exit_code']}, got={got_exit}"
    )
    assert sha == expected["sha"], f"SHA mismatch: expected={expected['sha']!r}, got={sha!r}"
    assert pr_number == str(expected["pr"]), (
        f"PR mismatch: expected={expected['pr']!r}, got={pr_number!r}"
    )
    # 1단계 직접 채택 검증
    assert sha_stage == "1", f"SHA stage should be '1' (primary), got={sha_stage!r}"
    assert pr_stage == "1", f"PR stage should be '1' (primary), got={pr_stage!r}"


def test_resolve_empty_sha_falls_back():
    """A-2: pr_event_empty_sha.json + GITHUB_SHA env_override. fallback SHA = 'fallback_sha_value'."""
    event_path = FIXTURES_DIR / "pr_event_empty_sha.json"
    expected_path = FIXTURES_DIR / "pr_event_empty_sha.expected.json"

    event = _load_event(str(event_path))
    with open(expected_path, "r", encoding="utf-8") as f:
        expected = json.load(f)

    # env_overrides 적용
    env_overrides = expected.get("env_overrides", {})
    with patch.dict(os.environ, env_overrides):
        pr_number, _ = _resolve_pr(event, branch="", repo="", dry_run=True)
        sha, sha_stage, _ = _resolve_sha(
            event, pr_number or None, repo="", dry_run=True
        )

    resolved_ok = bool(sha and pr_number)
    got_exit = 0 if resolved_ok else 1

    assert got_exit == expected["exit_code"], (
        f"exit_code mismatch: expected={expected['exit_code']}, got={got_exit}"
    )
    assert sha == expected["sha"], (
        f"fallback SHA mismatch: expected={expected['sha']!r}, got={sha!r}"
    )
    assert sha == "fallback_sha_value", f"SHA must be 'fallback_sha_value', got={sha!r}"
    # 3단계(GITHUB_SHA env) 에서 채택됐는지 검증
    assert sha_stage == "3", f"SHA fallback should reach stage '3', got={sha_stage!r}"


def test_resolve_empty_pr_fails():
    """A-3: pr_event_empty_pr.json — PR 추출 실패, exit_code 1."""
    event_path = FIXTURES_DIR / "pr_event_empty_pr.json"
    expected_path = FIXTURES_DIR / "pr_event_empty_pr.expected.json"

    event = _load_event(str(event_path))
    with open(expected_path, "r", encoding="utf-8") as f:
        expected = json.load(f)

    pr_number, pr_stage = _resolve_pr(event, branch="", repo="", dry_run=True)
    sha, _, _ = _resolve_sha(event, pr_number or None, repo="", dry_run=True)

    resolved_ok = bool(sha and pr_number)
    got_exit = 0 if resolved_ok else 1

    assert got_exit == 1, f"empty PR should yield exit_code=1, got={got_exit}"
    assert got_exit == expected["exit_code"]
    assert not pr_number, f"PR should be empty, got={pr_number!r}"
    assert pr_stage == "none", f"PR stage should be 'none', got={pr_stage!r}"


def test_resolve_empty_both_fails():
    """A-4: pr_event_empty_both.json — SHA + PR 모두 추출 실패, exit_code 1."""
    event_path = FIXTURES_DIR / "pr_event_empty_both.json"
    expected_path = FIXTURES_DIR / "pr_event_empty_both.expected.json"

    event = _load_event(str(event_path))
    with open(expected_path, "r", encoding="utf-8") as f:
        expected = json.load(f)

    # GITHUB_SHA 미설정 상태 보장
    env_without_sha = {k: v for k, v in os.environ.items() if k != "GITHUB_SHA"}
    with patch.dict(os.environ, env_without_sha, clear=True):
        pr_number, _ = _resolve_pr(event, branch="", repo="", dry_run=True)
        sha, _, _ = _resolve_sha(event, pr_number or None, repo="", dry_run=True)

    resolved_ok = bool(sha and pr_number)
    got_exit = 0 if resolved_ok else 1

    assert got_exit == 1, f"empty both should yield exit_code=1, got={got_exit}"
    assert got_exit == expected["exit_code"]
    assert not sha, f"SHA should be empty, got={sha!r}"
    assert not pr_number, f"PR should be empty, got={pr_number!r}"


# ---------------------------------------------------------------------------
# B. canonical 검증 단위 테스트 (2건)
# ---------------------------------------------------------------------------


def test_canonical_skip_when_primary_used():
    """B-5: SHA 1단계(primary) 채택 시 gh pr view 호출 0건 확인."""
    event = {
        "pull_request": {
            "number": 47,
            "head": {
                "sha": "deadbeef1234567890abcdef1234567890abcdef",
                "ref": "task-2486-dev1",
            },
        }
    }

    with patch("scripts.verify_workflow_sha_payload._run_gh") as mock_gh:
        sha, sha_stage, sha_is_canonical = _resolve_sha(
            event, pr_number="47", repo="owner/repo", dry_run=True
        )

    # dry_run=True → 4단계 gh 호출 스킵, 1단계 is_canonical=True → canonical 검증도 불필요
    assert sha == "deadbeef1234567890abcdef1234567890abcdef"
    assert sha_stage == "1"
    assert sha_is_canonical is True
    # gh 호출이 발생하지 않았음
    mock_gh.assert_not_called()


def test_canonical_used_when_fallback():
    """B-6: fallback SHA(3단계) 사용 시 canonical 비교 강제. mismatch → canonical 교체."""
    from scripts.verify_workflow_sha_payload import _verify_canonical  # type: ignore[import-not-found]

    fallback_sha = "fallback_sha_value"
    canonical_sha = "canonical_sha_value_abcdef1234567890"
    pr_number = "47"
    repo = "owner/repo"

    # gh pr view 가 canonical_sha를 반환하는 상황 모킹
    mock_output = json.dumps({"headRefOid": canonical_sha})
    with patch("scripts.verify_workflow_sha_payload._run_gh", return_value=mock_output):
        final_sha, was_mismatch = _verify_canonical(fallback_sha, pr_number, repo)

    # mismatch → canonical로 교체됐는지 확인
    assert was_mismatch is True, "canonical mismatch should be detected"
    assert final_sha == canonical_sha, (
        f"final SHA should be canonical={canonical_sha!r}, got={final_sha!r}"
    )
    assert final_sha != fallback_sha, "fallback SHA should have been replaced by canonical"


# ---------------------------------------------------------------------------
# C. PR fallback 안전성 (2건)
# ---------------------------------------------------------------------------


def test_pr_list_multiple_aborts():
    """C-7: gh pr list 결과 2건 이상이면 PR fallback 실패 (모호함 방지)."""
    # event 에 pull_request.number 와 event.number 모두 없는 상태
    event: dict = {
        "pull_request": {
            "number": None,
            "head": {"sha": "some_sha", "ref": "feature-branch"},
        },
        "number": None,
    }

    # gh pr list 가 2건 반환하는 상황 모킹
    multi_result = json.dumps([
        {"number": 10, "headRefName": "feature-branch", "headRepositoryOwner": {"login": "owner"}},
        {"number": 11, "headRefName": "feature-branch", "headRepositoryOwner": {"login": "owner"}},
    ])

    with patch("scripts.verify_workflow_sha_payload._run_gh", return_value=multi_result):
        pr_number, pr_stage = _resolve_pr(
            event, branch="feature-branch", repo="owner/repo", dry_run=False
        )

    # 2건 이상 → 모호함으로 fallback 실패
    assert pr_number == "", (
        f"PR should be empty when gh pr list returns multiple results, got={pr_number!r}"
    )
    assert pr_stage == "none", (
        f"PR stage should be 'none' on ambiguous result, got={pr_stage!r}"
    )


def test_pr_event_number_fallback():
    """C-8: event.pull_request.number 없고 event.number 만 있는 경우 event.number 사용."""
    # pull_request.number = None 이지만 event.number = 99 존재
    event = {
        "number": 99,
        "pull_request": {
            "number": None,
            "head": {
                "sha": "some_sha_value_here",
                "ref": "feature-branch",
            },
        },
    }

    pr_number, pr_stage = _resolve_pr(event, branch="", repo="", dry_run=True)

    assert pr_number == "99", (
        f"should fall back to event.number=99, got={pr_number!r}"
    )
    assert pr_stage == "2", (
        f"should use stage '2' (event.number fallback), got={pr_stage!r}"
    )


# ---------------------------------------------------------------------------
# D. ci.yml 구조 회귀 (2건)
# ---------------------------------------------------------------------------

REQUIRED_JOBS = [
    "cancel-kill-switch",
    "qc-check",
    "hidden-path-audit",
    "lock-in-check",
    "merge-safety-check",
    "gemini-review-gate",
    "phase3-merge-gate",
    "ci-guard",
    "guard",
]


def _load_ci_yml() -> dict:
    with open(CI_YML, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)


def test_ci_yml_required_jobs_intact():
    """D-9: ci.yml 에 필수 job 9개 모두 존재하는지 확인."""
    ci = _load_ci_yml()
    jobs: dict = ci.get("jobs", {})

    missing = [job for job in REQUIRED_JOBS if job not in jobs]
    assert not missing, (
        f"ci.yml 에서 다음 job 이 누락됨: {missing}\n"
        f"존재하는 job: {list(jobs.keys())}"
    )


def test_ci_yml_uses_resolve_step():
    """D-10: gemini-review-gate 와 phase3-merge-gate 의 steps 에 verify_workflow_sha_payload.py --mode resolve 포함 확인."""
    ci = _load_ci_yml()
    jobs: dict = ci.get("jobs", {})

    target_jobs = ["gemini-review-gate", "phase3-merge-gate"]
    EXPECTED_FRAGMENT = "verify_workflow_sha_payload.py --mode resolve"

    for job_name in target_jobs:
        assert job_name in jobs, f"job '{job_name}' not found in ci.yml"

        steps = jobs[job_name].get("steps", [])
        found = False
        for step in steps:
            # run 필드가 문자열인 경우
            run_cmd = step.get("run", "") or ""
            if EXPECTED_FRAGMENT in run_cmd:
                found = True
                break
            # uses/with 중 env 또는 args에도 있을 수 있음
            with_val = step.get("with", {}) or {}
            for v in with_val.values():
                if isinstance(v, str) and EXPECTED_FRAGMENT in v:
                    found = True
                    break

        assert found, (
            f"job '{job_name}' steps 에서 '{EXPECTED_FRAGMENT}' 를 찾지 못함.\n"
            f"steps: {[s.get('name', '(no name)') for s in steps]}"
        )
