"""Cross-surface approval mode/timeout parity invariant.

The approval mode (``approvals.mode``) and timeout (``approvals.timeout``)
must resolve identically on every surface that consults them:

  - the canonical core: ``tools.approval._get_approval_mode`` /
    ``tools.approval._get_approval_timeout``
  - the TUI gateway: ``tui_gateway.server._load_approval_mode`` (delegates
    to the core as of the decision-core migration)
  - the codex app-server surface: ``agent/codex_runtime.py`` feeds
    ``auto_approve_*`` from ``tools.approval.is_approval_bypass_active()``,
    which itself reads the core resolver — so parity there reduces to
    ``is_approval_bypass_active() == (mode == "off")`` when no yolo
    source is active.

Historic drift class: tui_gateway re-read config raw and normalized
locally (see commits f9cd577915, 1e652cca7a, bd246db10d — repeated parity
re-alignments). This test pins the invariant so drift regressions fail
loudly instead of silently disagreeing per surface.

There is no per-platform ``approvals.mode`` override in the config schema;
mode/timeout are global, so the synthetic configs below cover global-set,
unset (defaults), and malformed values.
"""

from __future__ import annotations

import importlib
from unittest.mock import MagicMock, patch

import pytest


@pytest.fixture()
def hermes_home(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    home.mkdir()
    monkeypatch.setenv("HERMES_HOME", str(home))
    return home


@pytest.fixture()
def tui_server():
    with patch.dict(
        "sys.modules",
        {
            "hermes_cli.env_loader": MagicMock(),
            "hermes_cli.banner": MagicMock(),
        },
    ):
        yield importlib.import_module("tui_gateway.server")


def _write_config(home, yaml_text: str | None) -> None:
    cfg = home / "config.yaml"
    if yaml_text is None:
        if cfg.exists():
            cfg.unlink()
    else:
        cfg.write_text(yaml_text, encoding="utf-8")


# (config yaml, expected mode, expected timeout)
CASES = [
    pytest.param(None, "smart", 300, id="unset-defaults"),
    pytest.param(
        "approvals:\n  mode: manual\n", "manual", 300, id="global-manual"
    ),
    pytest.param(
        "approvals:\n  mode: smart\n  timeout: 120\n",
        "smart",
        120,
        id="global-smart-timeout",
    ),
    pytest.param(
        # YAML 1.1 parses bare OFF as boolean False; the normalizer maps
        # False -> "off". Both surfaces must agree on that quirk.
        "approvals:\n  mode: OFF\n  timeout: 45\n",
        "off",
        45,
        id="yaml-bool-off",
    ),
    pytest.param(
        "approvals:\n  mode: bogus-value\n  timeout: not-a-number\n",
        "manual",
        300,
        id="malformed-values",
    ),
    pytest.param(
        "approvals:\n  mode: '  Smart  '\n", "smart", 300, id="whitespace-case"
    ),
]


def _approval_module():
    """Resolve tools.approval via sys.modules, not the package attribute.

    The ``tui_server`` fixture's ``patch.dict("sys.modules", ...)`` purges
    modules imported during its block at teardown; ``from tools import
    approval`` can then hand back a stale attribute cached on the ``tools``
    package while the server re-imports a fresh module object. Going
    through ``importlib.import_module`` keeps the test and the server on
    the same sys.modules entry.
    """
    return importlib.import_module("tools.approval")


@pytest.mark.parametrize("yaml_text,expected_mode,expected_timeout", CASES)
def test_mode_and_timeout_parity_across_surfaces(
    hermes_home, tui_server, yaml_text, expected_mode, expected_timeout
):
    approval_mod = _approval_module()

    _write_config(hermes_home, yaml_text)

    core_mode = approval_mod._get_approval_mode()
    core_timeout = approval_mod._get_approval_timeout()
    tui_mode = tui_server._load_approval_mode()

    # Canonical resolver matches expectations.
    assert core_mode == expected_mode
    assert core_timeout == expected_timeout

    # TUI surface returns the identical mode (delegation invariant).
    assert tui_mode == core_mode

    # Codex surface: auto-approve routing is derived from
    # is_approval_bypass_active(), which must equal (mode == "off")
    # whenever no yolo source is active in this process.
    if not approval_mod._YOLO_MODE_FROZEN:
        with patch.object(
            approval_mod, "is_current_session_yolo_enabled", return_value=False
        ):
            assert approval_mod.is_approval_bypass_active() == (
                core_mode == "off"
            )


def test_tui_loader_delegates_to_core(hermes_home, tui_server):
    """The TUI must not re-resolve mode itself — it delegates to the core.

    Pin the delegation seam directly: patching the core resolver changes
    what the TUI reports, proving there is no independent config read left.
    """
    approval_mod = _approval_module()

    with patch.object(approval_mod, "_get_approval_mode", return_value="smart"):
        assert tui_server._load_approval_mode() == "smart"
    with patch.object(approval_mod, "_get_approval_mode", return_value="off"):
        assert tui_server._load_approval_mode() == "off"
    # Defensive clamp: an out-of-vocabulary value from the core is coerced
    # to manual rather than leaking an unknown mode to the TUI client.
    with patch.object(
        approval_mod, "_get_approval_mode", return_value="weird"
    ):
        assert tui_server._load_approval_mode() == "manual"
