# -*- coding: utf-8 -*-
"""tests.regression.test_ci_watch_handoff_schema — task-2642.

회장 verbatim (2026-05-23 19:38 KST) 12 필수 필드 + 5 terminal_states enum 정합 회귀.
Layer A / NO-CRON: subprocess / cokacdir / merge / cron / live gh 호출 0.
"""
from __future__ import annotations

import copy

import pytest

from utils.ci_watch_handoff_schema import (
    ALL_TERMINAL_STATES,
    ALLOWED_AUTO_REMEDIATION_SEVERITIES,
    DEFAULT_MAX_WATCH_MINUTES,
    DEFAULT_POLL_INTERVAL_SECONDS,
    REQUIRED_FIELDS,
    SCHEMA,
    SchemaError,
    TERMINAL_CHAIR_REQUIRED,
    TERMINAL_CI_FAILED_NON_REMEDIABLE,
    TERMINAL_GEMINI_EXTERNAL_TRIGGER_STALE,
    TERMINAL_LOOP_BOUNDARY,
    TERMINAL_MERGE_READY,
    validate_handoff,
)


# ── canonical handoff (12 필드 모두 정상) ─────────────────────────────────────
def _canonical_handoff() -> dict:
    return {
        "pr_number": 146,
        "head_sha": "a" * 40,
        "branch": "task/task-2642-runner",
        "expected_files": [
            "utils/ci_watch_handoff_runner.py",
            "tests/regression/test_ci_watch_handoff_runner.py",
        ],
        "forbidden_paths": [
            "scripts/finish-task.sh",
            "cokacdir/**",
        ],
        "watcher_owner": "dev6-cron-watcher",
        "max_watch_minutes": 120,
        "poll_interval_seconds": 60,
        "gemini_nudge_policy": {
            "enabled": True,
            "max_nudges_per_pr_head": 1,
            "on_403": "report",
        },
        "auto_remediation_policy": {
            "enabled": True,
            "allow_severities": ["medium", "style", "quality", "non-critical-high"],
        },
        "callback_on_terminal_state": True,
        "terminal_states": [
            "MERGE_READY",
            "CHAIR_REQUIRED",
            "GEMINI_EXTERNAL_TRIGGER_STALE",
            "CI_FAILED_NON_REMEDIABLE",
            "LOOP_BOUNDARY",
        ],
    }


def test_schema_constant_is_v1():
    assert SCHEMA == "utils.ci_watch_handoff_schema.v1"


# ── ANCHOR-1: 12 필수 필드 1:1 박제 ───────────────────────────────────────────


def test_required_fields_exact_12():
    assert len(REQUIRED_FIELDS) == 12
    assert set(REQUIRED_FIELDS) == {
        "pr_number",
        "head_sha",
        "branch",
        "expected_files",
        "forbidden_paths",
        "watcher_owner",
        "max_watch_minutes",
        "poll_interval_seconds",
        "gemini_nudge_policy",
        "auto_remediation_policy",
        "callback_on_terminal_state",
        "terminal_states",
    }


@pytest.mark.parametrize("missing_field", REQUIRED_FIELDS)
def test_validate_handoff_rejects_missing_required_field(missing_field):
    handoff = _canonical_handoff()
    handoff.pop(missing_field)
    with pytest.raises(SchemaError, match="missing required fields"):
        validate_handoff(handoff)


# ── ANCHOR-2: 5 terminal_states enum ──────────────────────────────────────────


def test_terminal_states_enum_exact_5():
    assert ALL_TERMINAL_STATES == frozenset(
        {
            TERMINAL_MERGE_READY,
            TERMINAL_CHAIR_REQUIRED,
            TERMINAL_GEMINI_EXTERNAL_TRIGGER_STALE,
            TERMINAL_CI_FAILED_NON_REMEDIABLE,
            TERMINAL_LOOP_BOUNDARY,
        }
    )
    assert len(ALL_TERMINAL_STATES) == 5


def test_validate_handoff_rejects_invalid_terminal_state():
    handoff = _canonical_handoff()
    handoff["terminal_states"] = ["MERGE_READY", "UNKNOWN_TERMINAL"]
    with pytest.raises(SchemaError, match="terminal_states contains invalid enum"):
        validate_handoff(handoff)


def test_validate_handoff_rejects_empty_terminal_states():
    handoff = _canonical_handoff()
    handoff["terminal_states"] = []
    with pytest.raises(SchemaError, match="terminal_states must be non-empty list"):
        validate_handoff(handoff)


# ── ANCHOR-3: gemini_nudge_policy.max_nudges_per_pr_head <= 1 hard limit ─────


def test_validate_handoff_rejects_nudge_limit_above_one():
    handoff = _canonical_handoff()
    handoff["gemini_nudge_policy"]["max_nudges_per_pr_head"] = 2
    with pytest.raises(SchemaError, match="must be <= 1"):
        validate_handoff(handoff)


def test_validate_handoff_accepts_nudge_limit_zero():
    handoff = _canonical_handoff()
    handoff["gemini_nudge_policy"]["max_nudges_per_pr_head"] = 0
    normalized = validate_handoff(handoff)
    assert normalized["gemini_nudge_policy"]["max_nudges_per_pr_head"] == 0


def test_validate_handoff_rejects_invalid_on_403():
    handoff = _canonical_handoff()
    handoff["gemini_nudge_policy"]["on_403"] = "retry"
    with pytest.raises(SchemaError, match="on_403 must be 'report'"):
        validate_handoff(handoff)


# ── ANCHOR-4: auto_remediation severity 화이트리스트 ─────────────────────────


def test_validate_handoff_rejects_invalid_severity():
    handoff = _canonical_handoff()
    handoff["auto_remediation_policy"]["allow_severities"] = ["medium", "critical"]
    with pytest.raises(SchemaError, match="contains invalid severity"):
        validate_handoff(handoff)


def test_allowed_severities_are_exact_4():
    assert ALLOWED_AUTO_REMEDIATION_SEVERITIES == frozenset(
        {"medium", "style", "quality", "non-critical-high"}
    )


# ── 타입 검증 ────────────────────────────────────────────────────────────────


def test_validate_handoff_rejects_non_positive_pr_number():
    for bad in (0, -1, "146", 1.5, True, False):
        handoff = _canonical_handoff()
        handoff["pr_number"] = bad
        with pytest.raises(SchemaError, match="pr_number must be positive int"):
            validate_handoff(handoff)


def test_validate_handoff_rejects_invalid_head_sha():
    for bad in ("short", "Z" * 40, "g" * 40, 123, None):
        handoff = _canonical_handoff()
        handoff["head_sha"] = bad
        with pytest.raises(SchemaError, match="head_sha"):
            validate_handoff(handoff)


def test_validate_handoff_normalizes_head_sha_to_lower():
    handoff = _canonical_handoff()
    handoff["head_sha"] = "ABCDEF" + "0" * 34
    normalized = validate_handoff(handoff)
    assert normalized["head_sha"] == "abcdef" + "0" * 34


def test_validate_handoff_rejects_empty_branch():
    handoff = _canonical_handoff()
    handoff["branch"] = "   "
    with pytest.raises(SchemaError, match="branch must be non-empty"):
        validate_handoff(handoff)


def test_validate_handoff_rejects_empty_expected_files():
    handoff = _canonical_handoff()
    handoff["expected_files"] = []
    with pytest.raises(SchemaError, match="expected_files must be non-empty"):
        validate_handoff(handoff)


def test_validate_handoff_accepts_empty_forbidden_paths():
    handoff = _canonical_handoff()
    handoff["forbidden_paths"] = []
    normalized = validate_handoff(handoff)
    assert normalized["forbidden_paths"] == []


def test_validate_handoff_rejects_non_positive_minutes():
    handoff = _canonical_handoff()
    handoff["max_watch_minutes"] = 0
    with pytest.raises(SchemaError, match="max_watch_minutes"):
        validate_handoff(handoff)


def test_validate_handoff_rejects_non_positive_poll_interval():
    handoff = _canonical_handoff()
    handoff["poll_interval_seconds"] = -10
    with pytest.raises(SchemaError, match="poll_interval_seconds"):
        validate_handoff(handoff)


def test_validate_handoff_rejects_non_bool_callback():
    handoff = _canonical_handoff()
    handoff["callback_on_terminal_state"] = "true"
    with pytest.raises(SchemaError, match="callback_on_terminal_state must be bool"):
        validate_handoff(handoff)


def test_defaults_are_spec_120_and_60():
    assert DEFAULT_MAX_WATCH_MINUTES == 120
    assert DEFAULT_POLL_INTERVAL_SECONDS == 60


def test_validate_handoff_returns_normalized_copy_does_not_mutate_input():
    handoff = _canonical_handoff()
    handoff["head_sha"] = "ABCDEF" + "0" * 34
    snapshot = copy.deepcopy(handoff)
    normalized = validate_handoff(handoff)
    assert handoff == snapshot, "input must not be mutated"
    assert normalized is not handoff


def test_validate_handoff_rejects_non_dict_input():
    for bad in (None, "string", [1, 2, 3], 123):
        with pytest.raises(SchemaError, match="handoff must be dict"):
            validate_handoff(bad)


def test_validate_handoff_canonical_passes():
    """smoke — 정상 12 필드 dict 은 통과."""
    normalized = validate_handoff(_canonical_handoff())
    assert normalized["pr_number"] == 146
    assert normalized["head_sha"] == "a" * 40
    assert len(normalized["terminal_states"]) == 5
