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

task-2635 — ANCHOR-3 sendfile/report 전송 ≠ callback 단언.

Spec §5: sendfile_only 만으로는 callback 충족 안 됨 단언 · envelope sendfile
함수 호출 ≠ cron 등록 함수 호출 (별개 함수) · 두 함수 시그니처/책임 분리.
"""
from __future__ import annotations

import inspect
import json
import sys
from pathlib import Path

# tests/conftest.py adds /home/jay/workspace (live) to sys.path[0]. The pytest
# default ``prepend`` import mode loads ``dispatch`` from there during this
# module's collection — before tests/regression/conftest.py can swap it. We
# proactively ensure the worktree root sits at sys.path[0] so the worktree
# ``dispatch.finalize_hooks`` module resolves before the live shim.
_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)
# Evict any stale ``dispatch`` import that was resolved from the live tree.
for _mod in list(sys.modules):
    if _mod == "dispatch" or _mod.startswith("dispatch."):
        _cached = getattr(sys.modules[_mod], "__file__", "") or ""
        if _WORKTREE_ROOT not in _cached:
            sys.modules.pop(_mod, None)

from dispatch.finalize_hooks import send_envelope_to_chat  # noqa: E402
from utils.anu_callback_fallback import (  # noqa: E402
    decide_fallback_cancel,
    expected_collector_spawn,
)
from utils.anu_callback_registrar import (  # noqa: E402
    build_callback_envelope,
    register_normal_callback,
)
from utils.callback_envelope_schema import (  # noqa: E402
    DeliveryMethod,
    NormalCallbackRegistrationStatus,
    is_callback_complete,
)

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


def test_send_envelope_to_chat_is_distinct_function_from_registrar():
    # ANCHOR-3 separation: both functions exist and are not the same object.
    assert send_envelope_to_chat is not register_normal_callback
    # Different module homes too (dispatch.finalize_hooks vs utils.anu_callback_registrar).
    assert send_envelope_to_chat.__module__ != register_normal_callback.__module__


def test_send_envelope_to_chat_signature_does_not_take_anu_key_or_schedule_args():
    sig = inspect.signature(send_envelope_to_chat)
    params = set(sig.parameters.keys())
    # sendfile is auxiliary — must NOT accept registration-relevant args.
    forbidden = {"anu_key", "schedule_id", "cron_schedule_id", "delay_seconds"}
    assert params.isdisjoint(forbidden), (
        f"sendfile signature leaks registration params: {params & forbidden}"
    )
    # Must accept the basics for the auxiliary path.
    assert {"envelope", "chat_id"}.issubset(params)


def test_register_normal_callback_signature_takes_anu_key_and_delay():
    sig = inspect.signature(register_normal_callback)
    params = set(sig.parameters.keys())
    # Registration-specific params must be present in the cron path.
    assert {"envelope", "anu_key", "delay_seconds"}.issubset(params)


def test_sendfile_call_does_not_change_registration_status():
    """Calling send_envelope_to_chat must NEVER mutate registration_status."""
    result = {
        "executor_name": "dispatch-executor-dev6",
        "result_path": "memory/tasks/task-2635-sep.result.json",
        "report_path": "memory/reports/task-2635-sep.md",
        "commit_sha": "0" * 40,
        "spec_sha256": "0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d",
    }
    env = build_callback_envelope("task-2635-sep", result)
    before = dict(env)

    # auxiliary path — does NOT register a cron.
    audit = send_envelope_to_chat(env, chat_id="6937032012")
    assert audit["is_callback_substitute"] is False
    assert audit["delivered_to"] == "6937032012"

    # envelope (passed by reference) must not gain REGISTERED status from sendfile.
    assert env["registration_status"] == before["registration_status"]
    assert env["registration_status"] == NormalCallbackRegistrationStatus.NOT_REGISTERED.value


def test_sendfile_only_fixture_is_not_callback_complete():
    evidence = json.loads((SENDFILE_FIXTURE / "evidence.json").read_text(encoding="utf-8"))
    expected = json.loads((SENDFILE_FIXTURE / "expected.json").read_text(encoding="utf-8"))

    assert evidence["registration_status"] == NormalCallbackRegistrationStatus.SENDFILE_ONLY.value
    assert evidence["delivery_method"] == DeliveryMethod.SENDFILE_ONLY.value

    assert is_callback_complete(evidence) is False, (
        "sendfile_only fixture must NOT count as callback complete (ANCHOR-3)"
    )
    assert expected_collector_spawn(evidence) is False
    decision = decide_fallback_cancel(evidence)
    assert decision.cancel_fallback is False, (
        "sendfile_only must NEVER cancel fallback safety-net"
    )
    # fixture-level expected matches the runtime decision.
    assert expected["callback_substitute_by_sendfile"] is False


def test_sendfile_with_custom_runner_returns_audit_only():
    runner_called = []

    def my_runner(env, cid):
        runner_called.append((dict(env), cid))
        return {"ok": True, "chat": cid}

    audit = send_envelope_to_chat({"task_id": "x"}, "6937032012", sendfile_runner=my_runner)
    assert audit["is_callback_substitute"] is False
    assert audit["outcome"] == {"ok": True, "chat": "6937032012"}
    assert len(runner_called) == 1


def test_no_call_to_register_normal_callback_from_send_envelope_to_chat():
    """sendfile must not invoke the registrar (ANCHOR-3 separation).

    We parse the AST of ``send_envelope_to_chat`` and check there is no
    Call node whose callable resolves to ``register_normal_callback``.
    A textual mention in the docstring (educational note) is allowed.
    """
    import ast
    import textwrap

    src = inspect.getsource(send_envelope_to_chat)
    tree = ast.parse(textwrap.dedent(src))

    forbidden = {"register_normal_callback"}
    called_names: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            callee = node.func
            if isinstance(callee, ast.Name):
                called_names.append(callee.id)
            elif isinstance(callee, ast.Attribute):
                called_names.append(callee.attr)
    leaked = sorted(set(called_names) & forbidden)
    assert not leaked, (
        f"send_envelope_to_chat calls forbidden registrar functions {leaked} — "
        "ANCHOR-3 separation violated."
    )
    assert "--cron" not in src, "sendfile must not assemble cron args"

    # Runtime sentinel — invoking sendfile must return a non-substitute audit
    # record (no implicit registration).
    audit = send_envelope_to_chat({"task_id": "x"}, "6937032012")
    assert audit["is_callback_substitute"] is False
