# -*- coding: utf-8 -*-
"""task-2729+2 — normal callback registration reconciliation 회귀 테스트.

근본 원인 (audit): production 의 ANU normal callback envelope 는 registrar
(utils/anu_callback_registrar + dispatch/finalize_hooks) 가 real schedule_id 를
``cron_schedule_id`` 필드로, ANU key 를 ``anu_key`` 필드로 기록한다. 그러나
validator(utils/normal_callback_registration_validator) 는 canonical
``schedule_id`` / ``owner_key`` 필드명으로만 읽어 필드명 불일치로 FAIL 했다.

fix: validator 가 canonical 부재 시에만 registrar alias(cron_schedule_id /
anu_key) 를 회수하도록 input mapping 을 정합(canonical 우선 — 검증 약화 0).

테스트 케이스 (5개, validator alias 정합 검증):
  1. test_validator_pass_via_registrar_aliases — alias 만으로 4-source PASS
  2. test_sid_history_match_and_mismatch       — sid ↔ schedule_history 매칭
  3. test_self_key_non_authoritative_preserved — alias 경로 self-key 차단 유지
  4. test_canonical_precedence_no_weakening     — canonical 우선(검증 약화 0)
  5. test_existing_canonical_dogfood_pass       — 기존 canonical 동작 무손상
"""
from __future__ import annotations

import json
import pathlib
import sys

# --------------------------------------------------------------------------- #
# validator import
# --------------------------------------------------------------------------- #

_REPO = pathlib.Path(__file__).resolve().parents[2]  # repo root
if str(_REPO) not in sys.path:
    sys.path.insert(0, str(_REPO))

from utils.normal_callback_registration_validator import (  # noqa: E402
    ANU_KEY,
    FAIL,
    NON_AUTHORITATIVE,
    PASS,
    validate_callback_registration,
)

# --------------------------------------------------------------------------- #
# Helpers
# --------------------------------------------------------------------------- #


def _write_envelope(path: pathlib.Path, data: dict) -> str:
    """tmp 경로에 envelope JSON 작성 후 경로 문자열 반환."""
    path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
    return str(path)


def _write_schedule_history(history_dir: pathlib.Path, sid: str, status: str = "ok") -> None:
    """schedule_history/<sid>.log 작성."""
    log = history_dir / f"{sid}.log"
    log.write_text(
        json.dumps({"status": status, "schedule_id": sid}) + "\n",
        encoding="utf-8",
    )


def _write_inbound(inbound_dir: pathlib.Path, task_id: str) -> pathlib.Path:
    """inbound/<task_id>-normal-completion.json 작성."""
    f = inbound_dir / f"{task_id}-normal-completion.json"
    f.write_text(json.dumps({"task_id": task_id, "status": "ok"}), encoding="utf-8")
    return f


# --------------------------------------------------------------------------- #
# 1. test_validator_pass_via_registrar_aliases
# --------------------------------------------------------------------------- #

def test_validator_pass_via_registrar_aliases(tmp_path):
    """registrar 스타일 envelope (cron_schedule_id + anu_key alias) + history + inbound
    → validator가 alias로 Source 1,3을 회수하여 verdict==PASS.
    """
    task_id = "task-reg-2729"
    cron_sid = "SID-REG-1"

    # tmp dirs
    history_dir = tmp_path / "schedule_history"
    history_dir.mkdir()
    inbound_dir = tmp_path / "inbound"
    inbound_dir.mkdir()

    # envelope: schedule_id 없음, owner_key 없음, alias만 존재
    envelope_data = {
        "task_id": task_id,
        "cron_schedule_id": cron_sid,
        "anu_key": ANU_KEY,
        "schedule_type": "cron_once",
    }
    env_path = _write_envelope(tmp_path / f"{task_id}.envelope.json", envelope_data)

    # Source 2: schedule_history
    _write_schedule_history(history_dir, cron_sid, status="ok")

    # Source 4: inbound
    _write_inbound(inbound_dir, task_id)

    result = validate_callback_registration(
        task_id=task_id,
        envelope_path=env_path,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )

    assert result.verdict == PASS, (
        f"Expected PASS via alias, got {result.verdict}. "
        f"sources={result.sources_checked}, reasons={result.reasons}"
    )
    # 모든 sources PASS
    for src, v in result.sources_checked.items():
        assert v == PASS, f"source '{src}' not PASS: {v}. reasons={result.reasons}"


# --------------------------------------------------------------------------- #
# 3. test_sid_history_match_and_mismatch
# --------------------------------------------------------------------------- #

def test_sid_history_match_and_mismatch(tmp_path):
    """
    (a) cron_schedule_id alias + matching history → schedule_history source PASS, verdict PASS
    (b) cron_schedule_id alias + missing history → schedule_history source FAIL, verdict FAIL
    """
    # --- (a) match ---
    task_a = "task-match-2729"
    sid_a = "SID-MATCH"

    history_dir = tmp_path / "schedule_history"
    history_dir.mkdir()
    inbound_dir = tmp_path / "inbound"
    inbound_dir.mkdir()

    _write_schedule_history(history_dir, sid_a, status="ok")
    _write_inbound(inbound_dir, task_a)

    env_a = _write_envelope(
        tmp_path / f"{task_a}.envelope.json",
        {
            "task_id": task_a,
            "cron_schedule_id": sid_a,
            "anu_key": ANU_KEY,
            "schedule_type": "cron_once",
        },
    )

    result_a = validate_callback_registration(
        task_id=task_a,
        envelope_path=env_a,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )
    assert result_a.sources_checked.get("schedule_history") == PASS, (
        f"(a) schedule_history should PASS, got: {result_a.sources_checked}. "
        f"reasons={result_a.reasons}"
    )
    assert result_a.verdict == PASS, \
        f"(a) verdict should PASS, got {result_a.verdict}"

    # --- (b) mismatch (history file missing) ---
    task_b = "task-nohist-2729"
    sid_b = "SID-NOHIST"
    # history_dir 에 SID-NOHIST.log 없음
    _write_inbound(inbound_dir, task_b)

    env_b = _write_envelope(
        tmp_path / f"{task_b}.envelope.json",
        {
            "task_id": task_b,
            "cron_schedule_id": sid_b,
            "anu_key": ANU_KEY,
            "schedule_type": "cron_once",
        },
    )

    result_b = validate_callback_registration(
        task_id=task_b,
        envelope_path=env_b,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )
    assert result_b.sources_checked.get("schedule_history") == FAIL, (
        f"(b) schedule_history should FAIL (no log), got: {result_b.sources_checked}"
    )
    assert result_b.verdict == FAIL, \
        f"(b) verdict should FAIL, got {result_b.verdict}"


# --------------------------------------------------------------------------- #
# 4. test_self_key_non_authoritative_preserved
# --------------------------------------------------------------------------- #

def test_self_key_non_authoritative_preserved(tmp_path):
    """executor self-key를 anu_key alias 필드에 기록 + owner_key 없음
    → alias 경로로 회수해도 self-key 차단 → NON_AUTHORITATIVE.
    """
    task_id = "task-selfkey-2729"
    self_key = "deadbeef00000000"  # ANU_KEY 아님
    sid = "SID-SELFKEY"

    history_dir = tmp_path / "schedule_history"
    history_dir.mkdir()
    inbound_dir = tmp_path / "inbound"
    inbound_dir.mkdir()

    _write_schedule_history(history_dir, sid, status="ok")
    _write_inbound(inbound_dir, task_id)

    # anu_key 필드에 self-key 기록, canonical owner_key 없음
    env_path = _write_envelope(
        tmp_path / f"{task_id}.envelope.json",
        {
            "task_id": task_id,
            "cron_schedule_id": sid,
            "anu_key": self_key,
            "schedule_type": "cron_once",
        },
    )

    result = validate_callback_registration(
        task_id=task_id,
        envelope_path=env_path,
        executor_key=self_key,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )

    assert result.verdict == NON_AUTHORITATIVE, (
        f"Expected NON_AUTHORITATIVE (self-key alias blocked), got {result.verdict}. "
        f"sources={result.sources_checked}, reasons={result.reasons}"
    )
    assert result.sources_checked.get("owner_key") == NON_AUTHORITATIVE, (
        f"owner_key source should be NON_AUTHORITATIVE, got: {result.sources_checked}"
    )


# --------------------------------------------------------------------------- #
# 5. test_canonical_precedence_no_weakening
# --------------------------------------------------------------------------- #

def test_canonical_precedence_no_weakening(tmp_path):
    """canonical 필드가 존재할 때 alias로 fallthrough하지 않음 (검증 약화 0 증명).

    (a) canonical schedule_id='pending'(placeholder) + cron_schedule_id='SID-VALID'
        → canonical placeholder 채택, alias 무시 → FAIL

    (b) canonical owner_key=wrong_key + anu_key=ANU_KEY
        → canonical 우선이므로 owner_key FAIL, alias 무시 → FAIL
    """
    history_dir = tmp_path / "schedule_history"
    history_dir.mkdir()
    inbound_dir = tmp_path / "inbound"
    inbound_dir.mkdir()

    # --- (a) canonical placeholder schedule_id ---
    task_a = "task-canon-sched-2729"
    sid_valid = "SID-VALID"
    _write_schedule_history(history_dir, sid_valid, status="ok")
    _write_inbound(inbound_dir, task_a)

    env_a = _write_envelope(
        tmp_path / f"{task_a}.envelope.json",
        {
            "task_id": task_a,
            "schedule_id": "pending",          # canonical placeholder
            "cron_schedule_id": sid_valid,     # alias (무시되어야 함)
            "owner_key": ANU_KEY,
            "schedule_type": "cron_once",
        },
    )

    result_a = validate_callback_registration(
        task_id=task_a,
        envelope_path=env_a,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )
    assert result_a.sources_checked.get("schedule_id") == FAIL, (
        "(a) canonical placeholder 'pending' should cause schedule_id FAIL, "
        f"got: {result_a.sources_checked}. reasons={result_a.reasons}"
    )
    assert result_a.verdict == FAIL, \
        f"(a) verdict should FAIL, got {result_a.verdict}"

    # --- (b) canonical wrong owner_key ---
    task_b = "task-canon-owner-2729"
    sid_b = "SID-CANON-B"
    _write_schedule_history(history_dir, sid_b, status="ok")
    _write_inbound(inbound_dir, task_b)

    env_b = _write_envelope(
        tmp_path / f"{task_b}.envelope.json",
        {
            "task_id": task_b,
            "schedule_id": sid_b,
            "owner_key": "deadbeef00000000",  # canonical wrong key
            "anu_key": ANU_KEY,               # alias (무시되어야 함)
            "schedule_type": "cron_once",
        },
    )

    result_b = validate_callback_registration(
        task_id=task_b,
        envelope_path=env_b,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )
    assert result_b.sources_checked.get("owner_key") == FAIL, (
        "(b) canonical wrong owner_key should FAIL, "
        f"got: {result_b.sources_checked}. reasons={result_b.reasons}"
    )
    assert result_b.verdict == FAIL, \
        f"(b) verdict should FAIL, got {result_b.verdict}"


# --------------------------------------------------------------------------- #
# 6. test_existing_canonical_dogfood_pass
# --------------------------------------------------------------------------- #

def test_existing_canonical_dogfood_pass(tmp_path):
    """canonical 필드만 사용 (기존 정상 동작 무손상).

    schedule_id + owner_key 모두 canonical → 4-source AND PASS → verdict PASS.
    """
    task_id = "task-dogfood-2729"
    sid = "SID-CANON"

    history_dir = tmp_path / "schedule_history"
    history_dir.mkdir()
    inbound_dir = tmp_path / "inbound"
    inbound_dir.mkdir()

    _write_schedule_history(history_dir, sid, status="ok")
    _write_inbound(inbound_dir, task_id)

    env_path = _write_envelope(
        tmp_path / f"{task_id}.envelope.json",
        {
            "task_id": task_id,
            "schedule_id": sid,
            "owner_key": ANU_KEY,
            "schedule_type": "cron_once",
        },
    )

    result = validate_callback_registration(
        task_id=task_id,
        envelope_path=env_path,
        schedule_history_dir=str(history_dir),
        inbound_search_dirs=[str(inbound_dir)],
    )

    assert result.verdict == PASS, (
        f"canonical dogfood: expected PASS, got {result.verdict}. "
        f"sources={result.sources_checked}, reasons={result.reasons}"
    )
    for src, v in result.sources_checked.items():
        assert v == PASS, f"source '{src}' not PASS: {v}"
