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

task-2635 — registrar ANU key 단언 + envelope ≤3900 + cron 호출 시그니처.

Spec §5: ANU key 정확 단언 (c119085addb0f8b7) · self-key 차단 · envelope UTF-8
byte 측정 ≤3900 · cron 등록 시도 subprocess 호출 시그니처 일관.

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

import json
from typing import Any, Dict, List

import pytest

from utils.anu_callback_registrar import (
    COKACDIR_CLI,
    DEFAULT_CHAT_ID,
    INDEPENDENT_ANU_KEY,
    SelfKeyForbidden,
    _assert_independent_anu_key,
    _build_cokacdir_cron_argv,
    _parse_schedule_id,
    build_callback_envelope,
    envelope_byte_warning,
    merge_registrar_result_into_envelope,
    register_normal_callback,
)
from utils.callback_envelope_schema import (
    ENVELOPE_BYTE_LIMIT,
    NormalCallbackRegistrationStatus,
    envelope_utf8_byte_count,
    validate_envelope,
)


# ── ANU key invariants ───────────────────────────────────────────────────────


def test_independent_anu_key_constant_is_hardcoded():
    assert INDEPENDENT_ANU_KEY == "c119085addb0f8b7"


def test_assert_independent_anu_key_accepts_correct_key():
    _assert_independent_anu_key(INDEPENDENT_ANU_KEY, env_lookup=lambda _: None)


def test_assert_independent_anu_key_rejects_empty():
    with pytest.raises(SelfKeyForbidden):
        _assert_independent_anu_key("", env_lookup=lambda _: None)


def test_assert_independent_anu_key_rejects_drift():
    with pytest.raises(SelfKeyForbidden, match="drift"):
        _assert_independent_anu_key("deadbeefdeadbeef", env_lookup=lambda _: None)


def test_assert_independent_anu_key_rejects_self_key_collision():
    # spec §3.1 fail-closed: hardcoded constant collides with a per-bot env key.
    def env(name: str):
        return INDEPENDENT_ANU_KEY if name == "COKACDIR_KEY_DEV6" else None

    with pytest.raises(SelfKeyForbidden, match="self-key"):
        _assert_independent_anu_key(INDEPENDENT_ANU_KEY, env_lookup=env)


# ── envelope construction & byte budget ──────────────────────────────────────


def _minimal_result() -> Dict[str, Any]:
    return {
        "executor_name": "dispatch-executor-dev6",
        "result_path": "memory/tasks/task-2635-test.result.json",
        "report_path": "memory/reports/task-2635-test.md",
        "commit_sha": "0" * 40,
        "file_summary": "test envelope",
        "regression_summary": "test envelope",
        "spec_sha256": "0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d",
    }


def test_build_callback_envelope_seeds_not_registered_by_default():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    assert env["task_id"] == "task-2635-test"
    assert env["anu_key"] == INDEPENDENT_ANU_KEY
    assert env["collector_role"] == "ANU"
    # spec §2 마지막 줄 — seed 는 NOT_REGISTERED.
    assert env["registration_status"] == NormalCallbackRegistrationStatus.NOT_REGISTERED.value
    assert env["attempted_callback_registration"] is True
    assert env["delivery_method"] == "anu_cron_callback"


def test_build_callback_envelope_rejects_wrong_collector_role():
    with pytest.raises(ValueError, match="collector_role"):
        build_callback_envelope(
            "task-2635-test", _minimal_result(), collector_role="executor"
        )


def test_build_callback_envelope_rejects_empty_task_id():
    with pytest.raises(ValueError):
        build_callback_envelope("", _minimal_result())


def test_envelope_byte_count_under_3900():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    n = envelope_utf8_byte_count(env)
    assert n < ENVELOPE_BYTE_LIMIT, f"envelope size {n} >= hard limit {ENVELOPE_BYTE_LIMIT}"


def test_envelope_byte_warning_band_signal():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    # bloat with repeated chars until inside warn band but under hard limit.
    env["file_summary"] = "x" * 2800
    warn = envelope_byte_warning(env)
    # not None when inside band, None otherwise.
    if warn is not None:
        assert "warn band" in warn


def test_envelope_over_hard_limit_triggers_register_failed():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    env["file_summary"] = "x" * 5000  # blow past 3900
    captured: List[List[str]] = []

    def fake_runner(argv, timeout):  # pragma: no cover - shouldn't be called
        captured.append(list(argv))

        class _P:
            returncode = 0
            stdout = '{"status":"ok","id":"X"}'
            stderr = ""

        return _P()

    rr = register_normal_callback(
        env,
        subprocess_runner=fake_runner,
        cli_exists_check=lambda _: True,
    )
    assert rr.status == NormalCallbackRegistrationStatus.REGISTER_FAILED.value
    assert "hard limit" in (rr.error or "")
    assert captured == [], "subprocess must NOT be invoked when envelope exceeds limit"


# ── cron argv signature ──────────────────────────────────────────────────────


def test_cron_argv_signature_is_stable_and_safe():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    argv = _build_cokacdir_cron_argv(
        envelope=env,
        delay_seconds=10,
        chat_id=DEFAULT_CHAT_ID,
        anu_key=INDEPENDENT_ANU_KEY,
        cokacdir_path=COKACDIR_CLI,
    )
    # cokacdir --cron <prompt> --at 10s --chat 6937032012 --key <ANU> --once
    assert argv[0] == COKACDIR_CLI
    assert argv[1] == "--cron"
    # prompt is the third positional argv entry — a JSON string of the envelope.
    parsed = json.loads(argv[2])
    assert parsed["task_id"] == "task-2635-test"
    assert "--at" in argv and argv[argv.index("--at") + 1] == "10s"
    assert "--chat" in argv and argv[argv.index("--chat") + 1] == DEFAULT_CHAT_ID
    assert "--key" in argv and argv[argv.index("--key") + 1] == INDEPENDENT_ANU_KEY
    assert "--once" in argv  # one-time cron — collector spawns then auto-deletes


# ── registrar end-to-end (mocked subprocess) ─────────────────────────────────


class _FakeProc:
    def __init__(self, returncode: int, stdout: str = "", stderr: str = ""):
        self.returncode = returncode
        self.stdout = stdout
        self.stderr = stderr


def test_register_normal_callback_happy_path_returns_registered():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    fake_stdout = json.dumps({"status": "ok", "id": "CRON-ABC-001"})

    def fake_runner(argv, timeout):
        # spec §7 — argv must carry the independent ANU key.
        assert INDEPENDENT_ANU_KEY in argv
        return _FakeProc(0, fake_stdout, "")

    rr = register_normal_callback(
        env, subprocess_runner=fake_runner, cli_exists_check=lambda _: True
    )
    assert rr.status == NormalCallbackRegistrationStatus.REGISTERED.value
    assert rr.schedule_id == "CRON-ABC-001"
    assert rr.registered_at_ts is not None
    assert rr.error is None


def test_register_normal_callback_cli_non_zero_exit_returns_register_failed():
    env = build_callback_envelope("task-2635-test", _minimal_result())

    def fake_runner(argv, timeout):
        return _FakeProc(2, "", '{"status":"error","message":"network unreachable"}')

    rr = register_normal_callback(
        env, subprocess_runner=fake_runner, cli_exists_check=lambda _: True
    )
    assert rr.status == NormalCallbackRegistrationStatus.REGISTER_FAILED.value
    assert rr.schedule_id is None
    assert "exit=2" in (rr.error or "")


def test_register_normal_callback_missing_schedule_id_returns_register_failed():
    env = build_callback_envelope("task-2635-test", _minimal_result())

    def fake_runner(argv, timeout):
        return _FakeProc(0, '{"status":"ok"}', "")  # no id field

    rr = register_normal_callback(
        env, subprocess_runner=fake_runner, cli_exists_check=lambda _: True
    )
    assert rr.status == NormalCallbackRegistrationStatus.REGISTER_FAILED.value


def test_register_normal_callback_cli_missing_returns_register_failed():
    env = build_callback_envelope("task-2635-test", _minimal_result())

    def fake_runner(argv, timeout):  # pragma: no cover
        raise AssertionError("subprocess must not be invoked when CLI is missing")

    rr = register_normal_callback(
        env, subprocess_runner=fake_runner, cli_exists_check=lambda _: False
    )
    assert rr.status == NormalCallbackRegistrationStatus.REGISTER_FAILED.value
    assert "not found" in (rr.error or "")


def test_register_normal_callback_runner_exception_returns_register_failed():
    env = build_callback_envelope("task-2635-test", _minimal_result())

    def fake_runner(argv, timeout):
        raise OSError("boom")

    rr = register_normal_callback(
        env, subprocess_runner=fake_runner, cli_exists_check=lambda _: True
    )
    assert rr.status == NormalCallbackRegistrationStatus.REGISTER_FAILED.value
    assert "raised" in (rr.error or "")


def test_merge_registrar_result_into_envelope_is_immutable_on_input():
    env = build_callback_envelope("task-2635-test", _minimal_result())
    snap = dict(env)
    from utils.anu_callback_registrar import RegistrarResult

    rr = RegistrarResult(
        status=NormalCallbackRegistrationStatus.REGISTERED.value,
        schedule_id="CRON-XYZ-9",
        registered_at_ts="2026-05-23T02:00:10Z",
        byte_count=envelope_utf8_byte_count(env),
    )
    merged = merge_registrar_result_into_envelope(env, rr)
    assert merged is not env
    assert env == snap, "input envelope must not be mutated"
    assert merged["registration_status"] == NormalCallbackRegistrationStatus.REGISTERED.value
    assert merged["cron_schedule_id"] == "CRON-XYZ-9"
    ok, errs = validate_envelope(merged)
    assert ok, f"merged envelope failed validation: {errs}"


def test_parse_schedule_id_handles_garbage_input():
    assert _parse_schedule_id("") is None
    assert _parse_schedule_id("not json") is None
    assert _parse_schedule_id('{"status":"error"}') is None
    assert _parse_schedule_id('{"status":"ok","id":"OK1"}') == "OK1"
