# -*- coding: utf-8 -*-
"""tests/regression/test_callback_registration_status_consistency.py

task-2635+1 — status schema 5축 분리 + 6 모순 조합 FAIL 단언.

회장 verbatim (task-2635+1 §7):
    `registration_result_status == NOT_REGISTERED` + `cron_schedule_id != None` → FAIL
    `registration_result_status == REGISTERED` + `cron_schedule_id == None` → FAIL
    `registration_attempted == False` + result ∈ (REGISTERED, REGISTER_FAILED) → FAIL
    `registration_result_status == SENDFILE_ONLY` + attempted_callback_registration == True → FAIL
    `callback_delivery_status == DELIVERED` + result != REGISTERED → FAIL
    `collector_receipt_status == RECEIVED` + callback_delivery_status != DELIVERED → FAIL

Also asserts that all 10 fixture scenarios (5 existing + 5 new) carry
contradiction-free envelopes, and that the build-then-register-then-update
pipeline produces axes that pass the contradiction scanner.

Live cokacdir CLI 호출 0 (모든 subprocess 는 mock runner 로 주입).
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

import pytest

# tests/conftest.py adds /home/jay/workspace (live) to sys.path[0]. Pin the
# worktree first so our updated utils/dispatch modules resolve before the
# live shims (mirrors test_callback_vs_sendfile_separation.py pattern).
_WORKTREE_ROOT = str(Path(__file__).resolve().parents[2])
if sys.path and sys.path[0] != _WORKTREE_ROOT:
    if _WORKTREE_ROOT in sys.path:
        sys.path.remove(_WORKTREE_ROOT)
    sys.path.insert(0, _WORKTREE_ROOT)
for _mod in list(sys.modules):
    if _mod == "dispatch" or _mod.startswith("dispatch.") or _mod == "utils" or _mod.startswith("utils."):
        _cached = getattr(sys.modules[_mod], "__file__", "") or ""
        if _WORKTREE_ROOT not in _cached:
            sys.modules.pop(_mod, None)

from utils.anu_callback_registrar import (  # noqa: E402
    build_callback_envelope,
    merge_registrar_result_into_envelope,
    RegistrarResult,
)
from utils.callback_envelope_schema import (  # noqa: E402
    ALL_DELIVERY_STATUSES,
    ALL_RECEIPT_STATUSES,
    ALL_RESULT_STATUSES,
    CallbackDeliveryStatus,
    CollectorReceiptStatus,
    RegistrationResultStatus,
    detect_status_contradictions,
    validate_envelope,
)

FIXTURE_ROOT = (
    Path(__file__).resolve().parents[1]
    / "fixtures"
    / "normal_callback_registration"
)

EXISTING_SCENARIOS = [
    "registered_normal",
    "not_registered_envelope_only",
    "sendfile_only_no_cron",
    "register_failed_cli_error",
    "skipped_explicit_reason_dryrun",
]
NEW_SCENARIOS = [
    "registered_schedule_id_present",
    "sendfile_only_not_registered",
    "attempted_but_register_failed",
    "registered_but_not_yet_received",
    "received_by_anu_collector",
]
ALL_SCENARIOS = EXISTING_SCENARIOS + NEW_SCENARIOS


def _baseline_axes() -> dict:
    """Return a contradiction-free 5-axis envelope skeleton used as the base
    for each contradiction test. Every test mutates ONE axis to inject the
    target contradiction and asserts the scanner detects it."""
    return {
        "schema": "utils.anu_callback_registrar.v2",
        "task_id": "task-2635+1-contradiction-test",
        "executor_name": "dispatch-executor-dev6",
        "result_path": "memory/tasks/task-2635+1-contradiction-test.result.json",
        "report_path": "memory/reports/task-2635+1-contradiction-test.md",
        "registration_intent": True,
        "registration_attempted": True,
        "registration_result_status": RegistrationResultStatus.REGISTERED.value,
        "callback_delivery_status": CallbackDeliveryStatus.DELIVERED.value,
        "collector_receipt_status": CollectorReceiptStatus.RECEIVED.value,
        "attempted_callback_registration": True,
        "registration_status": RegistrationResultStatus.REGISTERED.value,
        "delivery_method": "anu_cron_callback",
        "collector_role": "ANU",
        "anu_key": "c119085addb0f8b7",
        "cron_schedule_id": "CRON-AXES-BASELINE",
        "registered_at_ts": "2026-05-23T03:30:00Z",
        "collector_durable_success_marker": True,
        "collector_done_marker_path": "memory/events/baseline.callback_done",
        "envelope_built_at": "2026-05-23T03:30:00Z",
        "commit_sha": "0" * 40,
        "file_summary": "5-axis baseline",
        "regression_summary": "5-axis baseline",
        "spec_sha256": "0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d",
    }


# ── 5축 enum range sanity ────────────────────────────────────────────────────


def test_axis_3_enum_has_five_values():
    assert ALL_RESULT_STATUSES == {
        "REGISTERED",
        "NOT_REGISTERED",
        "REGISTER_FAILED",
        "SENDFILE_ONLY",
        "SKIPPED_WITH_EXPLICIT_REASON",
    }


def test_axis_4_enum_has_four_values():
    assert ALL_DELIVERY_STATUSES == {
        "PENDING",
        "DELIVERED",
        "UNDELIVERED",
        "NOT_APPLICABLE",
    }


def test_axis_5_enum_has_four_values():
    assert ALL_RECEIPT_STATUSES == {
        "UNCONFIRMED",
        "RECEIVED",
        "TIMED_OUT",
        "NOT_APPLICABLE",
    }


# ── 6 모순 조합 단언 (task-2635+1 §7) ────────────────────────────────────────


def test_contradiction_1_not_registered_with_schedule_id_fails():
    """NOT_REGISTERED + cron_schedule_id present → must fail."""
    env = _baseline_axes()
    env["registration_result_status"] = RegistrationResultStatus.NOT_REGISTERED.value
    env["registration_status"] = RegistrationResultStatus.NOT_REGISTERED.value
    env["registration_attempted"] = False
    env["callback_delivery_status"] = CallbackDeliveryStatus.NOT_APPLICABLE.value
    env["collector_receipt_status"] = CollectorReceiptStatus.NOT_APPLICABLE.value
    env["attempted_callback_registration"] = False
    # cron_schedule_id stays present from baseline — that's the contradiction.

    contradictions = detect_status_contradictions(env)
    assert any("NOT_REGISTERED" in c and "schedule_id present" in c for c in contradictions), (
        f"contradiction #1 not detected. got: {contradictions}"
    )

    ok, errs = validate_envelope(env)
    assert not ok, "validator must reject NOT_REGISTERED + cron_schedule_id present"
    assert any("NOT_REGISTERED" in e for e in errs)


def test_contradiction_2_registered_without_schedule_id_fails():
    """REGISTERED + cron_schedule_id missing → must fail."""
    env = _baseline_axes()
    env.pop("cron_schedule_id")

    contradictions = detect_status_contradictions(env)
    assert any(
        "REGISTERED" in c and "cron_schedule_id missing" in c for c in contradictions
    ), f"contradiction #2 not detected. got: {contradictions}"

    ok, _errs = validate_envelope(env)
    assert not ok, "validator must reject REGISTERED without cron_schedule_id"


def test_contradiction_3_attempted_false_with_registered_fails():
    """attempted=False + result=REGISTERED → must fail."""
    env = _baseline_axes()
    env["registration_attempted"] = False
    # result stays REGISTERED — that's the contradiction.

    contradictions = detect_status_contradictions(env)
    assert any("registration_attempted=False" in c for c in contradictions), (
        f"contradiction #3 (REGISTERED variant) not detected. got: {contradictions}"
    )


def test_contradiction_3_attempted_false_with_register_failed_fails():
    """attempted=False + result=REGISTER_FAILED → must fail."""
    env = _baseline_axes()
    env["registration_attempted"] = False
    env["registration_result_status"] = RegistrationResultStatus.REGISTER_FAILED.value
    env["registration_status"] = RegistrationResultStatus.REGISTER_FAILED.value
    env["error_message"] = "fake"
    # delivery / receipt aren't the contradiction-3 target; relax them.
    env["callback_delivery_status"] = CallbackDeliveryStatus.UNDELIVERED.value
    env["collector_receipt_status"] = CollectorReceiptStatus.NOT_APPLICABLE.value
    env.pop("cron_schedule_id")

    contradictions = detect_status_contradictions(env)
    assert any("registration_attempted=False" in c for c in contradictions), (
        f"contradiction #3 (REGISTER_FAILED variant) not detected. got: {contradictions}"
    )


def test_contradiction_4_sendfile_only_with_attempted_fails():
    """SENDFILE_ONLY + attempted_callback_registration=True → must fail."""
    env = _baseline_axes()
    env["registration_result_status"] = RegistrationResultStatus.SENDFILE_ONLY.value
    env["registration_status"] = RegistrationResultStatus.SENDFILE_ONLY.value
    env["registration_attempted"] = False
    env["attempted_callback_registration"] = True  # the contradiction.
    env["delivery_method"] = "sendfile_only"
    env["callback_delivery_status"] = CallbackDeliveryStatus.NOT_APPLICABLE.value
    env["collector_receipt_status"] = CollectorReceiptStatus.NOT_APPLICABLE.value
    env.pop("cron_schedule_id")

    contradictions = detect_status_contradictions(env)
    assert any(
        "SENDFILE_ONLY" in c and "attempted_callback_registration=True" in c
        for c in contradictions
    ), f"contradiction #4 not detected. got: {contradictions}"


def test_contradiction_5_delivered_without_registered_fails():
    """DELIVERED + result != REGISTERED → must fail."""
    env = _baseline_axes()
    env["registration_result_status"] = RegistrationResultStatus.REGISTER_FAILED.value
    env["registration_status"] = RegistrationResultStatus.REGISTER_FAILED.value
    env["error_message"] = "fake"
    # delivery stays DELIVERED — the contradiction.
    env["collector_receipt_status"] = CollectorReceiptStatus.NOT_APPLICABLE.value
    env.pop("cron_schedule_id")

    contradictions = detect_status_contradictions(env)
    assert any(
        "callback_delivery_status=DELIVERED" in c for c in contradictions
    ), f"contradiction #5 not detected. got: {contradictions}"


def test_contradiction_6_received_without_delivered_fails():
    """RECEIVED + delivery != DELIVERED → must fail."""
    env = _baseline_axes()
    env["callback_delivery_status"] = CallbackDeliveryStatus.PENDING.value
    # receipt stays RECEIVED — the contradiction.

    contradictions = detect_status_contradictions(env)
    assert any(
        "collector_receipt_status=RECEIVED" in c for c in contradictions
    ), f"contradiction #6 not detected. got: {contradictions}"


# ── baseline passes (no contradictions) ──────────────────────────────────────


def test_baseline_axes_has_zero_contradictions():
    """Sanity: the test baseline itself must be contradiction-free, otherwise
    the per-contradiction tests above could be passing for the wrong reason."""
    env = _baseline_axes()
    assert detect_status_contradictions(env) == []
    ok, errs = validate_envelope(env)
    assert ok, f"baseline envelope failed validation: {errs}"


# ── fixture matrix consistency (10 시나리오 모두 zero contradictions) ─────────


@pytest.mark.parametrize("scenario", ALL_SCENARIOS)
def test_fixture_envelope_has_zero_contradictions(scenario):
    evidence = json.loads(
        (FIXTURE_ROOT / scenario / "evidence.json").read_text(encoding="utf-8")
    )
    contradictions = detect_status_contradictions(evidence)
    assert contradictions == [], (
        f"{scenario}: fixture envelope carries contradictions: {contradictions}"
    )


@pytest.mark.parametrize("scenario", ALL_SCENARIOS)
def test_fixture_expected_axes_match_evidence(scenario):
    """expected.json 5축 값 == evidence.json 5축 값."""
    evidence = json.loads(
        (FIXTURE_ROOT / scenario / "evidence.json").read_text(encoding="utf-8")
    )
    expected = json.loads(
        (FIXTURE_ROOT / scenario / "expected.json").read_text(encoding="utf-8")
    )
    for axis in (
        "registration_intent",
        "registration_attempted",
        "registration_result_status",
        "callback_delivery_status",
        "collector_receipt_status",
    ):
        assert evidence[axis] == expected[axis], (
            f"{scenario}: axis {axis} mismatch — evidence={evidence[axis]!r} "
            f"vs expected={expected[axis]!r}"
        )


@pytest.mark.parametrize("scenario", NEW_SCENARIOS)
def test_new_fixtures_carry_three_files(scenario):
    sdir = FIXTURE_ROOT / scenario
    assert (sdir / "evidence.json").is_file()
    assert (sdir / "expected.json").is_file()
    assert (sdir / "PROVENANCE.md").is_file()


# ── build-then-register-then-update pipeline produces clean axes ─────────────


def test_seed_envelope_axes_are_pre_attempt_defaults():
    """build_callback_envelope(default) seeds NOT_REGISTERED + PENDING."""
    env = build_callback_envelope(
        task_id="task-2635+1-seed",
        result={
            "executor_name": "dispatch-executor-dev6",
            "result_path": "r.json",
            "report_path": "r.md",
            "commit_sha": "0" * 40,
            "spec_sha256": "0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d",
        },
        attempted_callback_registration=True,
        registration_attempted=True,
    )
    assert env["registration_intent"] is True
    assert env["registration_attempted"] is True
    # seed is NOT_REGISTERED until the registrar transitions it.
    assert env["registration_result_status"] == RegistrationResultStatus.NOT_REGISTERED.value
    assert env["registration_status"] == RegistrationResultStatus.NOT_REGISTERED.value
    # No schedule_id in the seed → contradiction #1 should NOT fire.
    assert env.get("cron_schedule_id") is None
    # delivery / receipt mirror the seed.
    assert env["callback_delivery_status"] == CallbackDeliveryStatus.PENDING.value
    assert env["collector_receipt_status"] == CollectorReceiptStatus.UNCONFIRMED.value


def test_merge_registrar_result_transitions_all_five_axes():
    """task-2635+1 §4 — register-after-update: post-register envelope carries
    REGISTERED across axes 3/4/5 + schedule_id + registered_at_ts."""
    env = build_callback_envelope(
        task_id="task-2635+1-merge",
        result={
            "executor_name": "dispatch-executor-dev6",
            "result_path": "r.json",
            "report_path": "r.md",
            "commit_sha": "0" * 40,
            "spec_sha256": "0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d",
        },
    )
    rr = RegistrarResult(
        status=RegistrationResultStatus.REGISTERED.value,
        schedule_id="CRON-MERGE-OK-1",
        registered_at_ts="2026-05-23T03:35:00Z",
        delivery_status=CallbackDeliveryStatus.DELIVERED.value,
        receipt_status=CollectorReceiptStatus.UNCONFIRMED.value,
        byte_count=900,
    )
    merged = merge_registrar_result_into_envelope(env, rr)

    # axis-3
    assert merged["registration_result_status"] == RegistrationResultStatus.REGISTERED.value
    assert merged["registration_status"] == RegistrationResultStatus.REGISTERED.value  # alias
    # schedule_id + ts
    assert merged["cron_schedule_id"] == "CRON-MERGE-OK-1"
    assert merged["registered_at_ts"] == "2026-05-23T03:35:00Z"
    # axes 4 + 5
    assert merged["callback_delivery_status"] == CallbackDeliveryStatus.DELIVERED.value
    assert merged["collector_receipt_status"] == CollectorReceiptStatus.UNCONFIRMED.value
    # zero contradictions
    assert detect_status_contradictions(merged) == []

    # Input envelope is NOT mutated (immutable register-after-update).
    assert env["registration_result_status"] == RegistrationResultStatus.NOT_REGISTERED.value


def test_alias_drift_detected_when_legacy_and_new_disagree():
    """Validator must reject the case where registration_status (legacy) and
    registration_result_status (new) disagree — single source of truth."""
    env = _baseline_axes()
    env["registration_status"] = RegistrationResultStatus.NOT_REGISTERED.value
    # registration_result_status stays REGISTERED from baseline.
    env["registration_attempted"] = True

    ok, errs = validate_envelope(env)
    assert not ok, "validator must reject legacy/new alias drift"
    assert any("alias drift" in e for e in errs)


def test_failure_path_registrar_axes_are_consistent():
    """REGISTER_FAILED outcome must produce delivery=UNDELIVERED +
    receipt=NOT_APPLICABLE (no contradictions)."""
    env = build_callback_envelope(
        task_id="task-2635+1-fail",
        result={
            "executor_name": "dispatch-executor-dev6",
            "result_path": "r.json",
            "report_path": "r.md",
            "commit_sha": "0" * 40,
            "spec_sha256": "0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d",
        },
    )
    rr = RegistrarResult(
        status=RegistrationResultStatus.REGISTER_FAILED.value,
        error="cokacdir exit=2 stderr=daemon offline",
        delivery_status=CallbackDeliveryStatus.UNDELIVERED.value,
        receipt_status=CollectorReceiptStatus.NOT_APPLICABLE.value,
        byte_count=900,
    )
    merged = merge_registrar_result_into_envelope(env, rr)
    assert merged["registration_result_status"] == RegistrationResultStatus.REGISTER_FAILED.value
    assert merged["callback_delivery_status"] == CallbackDeliveryStatus.UNDELIVERED.value
    assert merged["collector_receipt_status"] == CollectorReceiptStatus.NOT_APPLICABLE.value
    assert detect_status_contradictions(merged) == []
