"""Multiplex interactive-auth regressions (#86296, #92840, #72657, #87240 egress).

Real ``GatewayRunner`` methods on an ``object.__new__`` runner, real
``PairingStore`` files under a temp HERMES_HOME, multiplex active.
"""

from pathlib import Path
from types import SimpleNamespace

import pytest

from gateway.config import GatewayConfig, Platform, PlatformConfig
from gateway.pairing import PairingStore
from gateway.profile_routing import ProfileRoute


@pytest.fixture
def mux_home(tmp_path, monkeypatch):
    from agent import secret_scope

    home = tmp_path / "hh"
    (home / "profiles" / "secondary").mkdir(parents=True)
    (home / ".env").write_text("", encoding="utf-8")
    (home / "profiles" / "secondary" / ".env").write_text("", encoding="utf-8")
    monkeypatch.setenv("HERMES_HOME", str(home))
    for key in (
        "TELEGRAM_ALLOWED_USERS",
        "TELEGRAM_ALLOW_BOTS",
        "GATEWAY_ALLOW_ALL_USERS",
        "GATEWAY_ALLOWED_USERS",
        "SLACK_ALLOW_ALL_USERS",
        "SLACK_ALLOWED_USERS",
    ):
        monkeypatch.delenv(key, raising=False)
    prev = secret_scope.is_multiplex_active()
    secret_scope.set_multiplex_active(True)
    yield home
    secret_scope.set_multiplex_active(prev)


def _runner(home):
    from gateway.run import GatewayRunner

    runner = object.__new__(GatewayRunner)
    runner.config = GatewayConfig(multiplex_profiles=True)
    runner.config.profile_routes = [
        ProfileRoute(name="r", platform="telegram", chat_id="-100555", profile="secondary")
    ]
    runner.config.platforms = {Platform.TELEGRAM: PlatformConfig(enabled=True, extra={})}
    runner.pairing_store = PairingStore(profile="default")
    runner.pairing_stores = {
        "default": runner.pairing_store,
        "secondary": PairingStore(profile="secondary"),
    }
    runner._primary_profile_name = "default"
    runner._profile_adapters = {"secondary": {}}
    return runner


def _telegram(runner):
    from plugins.platforms.telegram.adapter import TelegramAdapter

    tg = object.__new__(TelegramAdapter)
    tg.config = PlatformConfig(enabled=True, extra={})
    tg._authorization_check = None
    tg._message_handler = runner._primary_message_handler()  # closure, no __self__
    runner.adapters = {Platform.TELEGRAM: tg}
    tg.set_authorization_check(runner._make_adapter_auth_check(Platform.TELEGRAM))
    return tg


def test_routed_primary_callback_uses_routed_pairing_store_and_transport_allowlist(mux_home):
    """#86296: shared primary bot + profile_routes → the inline-button caller
    is authorized by the ROUTED profile's pairing store, while env allowlists
    resolve under the transport (launch) home, exactly like inbound messages."""
    runner = _runner(mux_home)
    store = runner.pairing_stores["secondary"]
    store._save_json(store._approved_path("telegram"), {"777": {}})
    (mux_home / ".env").write_text("TELEGRAM_ALLOWED_USERS=999\n", encoding="utf-8")
    tg = _telegram(runner)

    # Paired only in the routed profile → allowed in the routed chat only.
    assert tg._is_callback_user_authorized("777", chat_id="-100555", chat_type="supergroup") is True
    assert tg._is_callback_user_authorized("777", chat_id="-100999", chat_type="supergroup") is False
    # Transport-home allowlist honored in the routed chat (not the routed profile's empty scope).
    assert tg._is_callback_user_authorized("999", chat_id="-100555", chat_type="supergroup") is True
    assert tg._is_callback_user_authorized("888", chat_id="-100555", chat_type="supergroup") is False


def test_bot_sender_reaches_allow_bots_policy_through_callback(mux_home):
    """#92840: the early prefilter must carry ``is_bot`` so TELEGRAM_ALLOW_BOTS
    admits bot-authored messages under the multiplex closure handler."""
    from gateway.run import _profile_runtime_scope

    runner = _runner(mux_home)
    (mux_home / ".env").write_text("TELEGRAM_ALLOWED_USERS=999\nTELEGRAM_ALLOW_BOTS=all\n", encoding="utf-8")
    tg = _telegram(runner)

    def msg(uid, is_bot):
        return SimpleNamespace(
            from_user=SimpleNamespace(id=uid, is_bot=is_bot, username="x", full_name="X"),
            chat=SimpleNamespace(id=-100777, type="supergroup", is_forum=False),
            sender_chat=None,
            message_thread_id=None,
            is_topic_message=False,
        )

    with _profile_runtime_scope(mux_home):
        assert tg._is_user_authorized_from_message(msg(4242, True)) is True
        assert tg._is_user_authorized_from_message(msg(4343, False)) is False


def test_secondary_owned_callback_reads_own_profile_allowlist(mux_home, monkeypatch):
    """#120639: a secondary profile's own bot authorizes inline-button callers
    against the OWNING profile's allowlist — the same ``.env`` scope the
    cold-path message handler reads — even though the callback fires outside
    any profile runtime scope, straight off the adapter's event loop. The
    default profile's allowlist never leaks in, and the sync check never
    hydrates external secret sources on that loop (#99519 class)."""
    import hermes_cli.env_loader as env_loader

    hydrated = []
    monkeypatch.setattr(env_loader, "hydrate_profile_secret_sources", hydrated.append)
    runner = _runner(mux_home)
    (mux_home / ".env").write_text("TELEGRAM_ALLOWED_USERS=999\n", encoding="utf-8")
    monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "999")  # the default profile's live os.environ
    (mux_home / "profiles" / "secondary" / ".env").write_text("TELEGRAM_ALLOWED_USERS=555\n", encoding="utf-8")
    tg = _telegram(runner)
    tg._hermes_profile_name = "secondary"
    runner._profile_adapters = {"secondary": {Platform.TELEGRAM: tg}}
    tg.set_authorization_check(
        runner._make_adapter_auth_check(Platform.TELEGRAM, profile_name="secondary")
    )

    # No ambient profile scope: the adapter event loop invokes the callback bare.
    assert tg._is_callback_user_authorized("555", chat_id="111", chat_type="private") is True
    assert tg._is_callback_user_authorized("999", chat_id="111", chat_type="private") is False
    assert hydrated == []


def test_secondary_callback_allowlist_follows_env_edits(mux_home):
    """#120639 review: the secondary's callback check re-enters the owning
    profile's scope per call, so an operator's runtime edit to
    ``profiles/<name>/.env`` reaches the NEXT button tap — the same freshness
    the message path gives (``_make_profile_message_handler`` re-reads the
    ``.env`` per message), no allow/deny split until the adapter reconnects."""
    runner = _runner(mux_home)
    env_path = mux_home / "profiles" / "secondary" / ".env"
    env_path.write_text("TELEGRAM_ALLOWED_USERS=555\n", encoding="utf-8")
    tg = _telegram(runner)
    tg._hermes_profile_name = "secondary"
    runner._profile_adapters = {"secondary": {Platform.TELEGRAM: tg}}
    tg.set_authorization_check(
        runner._make_adapter_auth_check(Platform.TELEGRAM, profile_name="secondary")
    )

    assert tg._is_callback_user_authorized("777", chat_id="111", chat_type="private") is False
    env_path.write_text("TELEGRAM_ALLOWED_USERS=555,777\n", encoding="utf-8")  # operator adds a user at runtime
    assert tg._is_callback_user_authorized("777", chat_id="111", chat_type="private") is True


def test_slack_interactive_auth_prefers_wired_profile_check(mux_home, monkeypatch):
    """#72657: a multiplexed Slack adapter's button gate resolves through the
    wired ``_make_adapter_auth_check`` for its own profile; the DEFAULT
    profile's process-env allow-all never leaks in — not through the
    injected path, and not through the env-only fallback either."""
    from gateway.run import _profile_runtime_scope
    from plugins.platforms.slack.adapter import SlackAdapter

    runner = _runner(mux_home)
    runner.adapters = {}
    sec_home = mux_home / "profiles" / "secondary"
    (sec_home / ".env").write_text("SLACK_ALLOWED_USERS=U_SEC\n", encoding="utf-8")
    monkeypatch.setenv("SLACK_ALLOW_ALL_USERS", "true")

    def slack(with_check):
        sl = object.__new__(SlackAdapter)
        sl.config = PlatformConfig(enabled=True, extra={})
        sl._authorization_check = None
        sl._message_handler = runner._make_profile_message_handler("secondary")
        if with_check:
            runner._profile_adapters = {"secondary": {Platform.SLACK: sl}}
            sl.set_authorization_check(
                runner._make_adapter_auth_check(Platform.SLACK, profile_name="secondary")
            )
        return sl

    with _profile_runtime_scope(sec_home):
        wired = slack(True)
        assert wired._is_interactive_user_authorized("U_SEC", channel_id="C1") is True
        assert wired._is_interactive_user_authorized("U_X", channel_id="C1") is False
        assert slack(False)._is_interactive_user_authorized("U_X", channel_id="C1") is False


def test_authorization_adapter_ignores_per_turn_active_profile(mux_home):
    """#87240 egress half: inside a secondary profile's runtime scope the
    default bot must not be handed to that profile (fail-closed None); the
    launch profile still resolves ``self.adapters``. The secondary's own bot
    is down (reconnect pending), so it is not a route-only satellite."""
    from gateway.run import _profile_runtime_scope

    runner = _runner(mux_home)
    default_bot = object()
    runner.adapters = {Platform.TELEGRAM: default_bot}
    runner._profile_failed_platforms = {"secondary": {Platform.TELEGRAM: object()}}

    with _profile_runtime_scope(mux_home / "profiles" / "secondary"):
        assert runner._authorization_adapter(Platform.TELEGRAM, profile="secondary") is None
    assert runner._authorization_adapter(Platform.TELEGRAM, profile="default") is default_bot


def test_channel_directory_path_follows_current_home(mux_home):
    """#87240: the directory file resolves against the CURRENT profile home,
    not the home that happened to import the module."""
    import gateway.channel_directory as cd
    from gateway.run import _profile_runtime_scope

    assert cd.DIRECTORY_PATH is None
    with _profile_runtime_scope(mux_home / "profiles" / "secondary"):
        assert cd._directory_path() == Path(mux_home / "profiles" / "secondary" / "channel_directory.json")
    assert cd._directory_path() == Path(mux_home / "channel_directory.json")
