"""Tests for per-mesh LLM scoping changes.

Covers:
  (a) should_respond DM gate decoupling (MeshCore vs Meshtastic)
  (b) get_context_block(transport=...) per-mesh filtering
  (c) Meshtastic channel-index filter is skipped for MeshCore observations
  (d) ContextConfig.max_age default is 14 days (1_209_600 seconds)
"""

import sys
from unittest.mock import MagicMock

import pytest

# ---------------------------------------------------------------------------
# Stub optional heavy deps so meshai.router can be imported in this env.
# These stubs are set before any meshai.router import in this process;
# they are no-ops in production where the real packages are installed.
# ---------------------------------------------------------------------------
for _mod in ("openai", "aiosqlite", "anthropic", "google", "google.genai"):
    sys.modules.setdefault(_mod, MagicMock())

from meshai.config import (  # noqa: E402
    BotConfig,
    Config,
    ContextConfig,
    MeshCoreContextConfig,
)
from meshai.connector import MeshMessage  # noqa: E402
from meshai.context import MeshContext  # noqa: E402


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_message(transport="meshtastic", is_dm=True, text="hello"):
    return MeshMessage(
        sender_id="abc1",
        sender_name="Alice",
        text=text,
        channel=0,
        is_dm=is_dm,
        transport=transport,
    )


def _make_router(respond_to_dms: bool, meshcore_respond_to_dms: bool = True):
    """Build a minimal Router with mocked dependencies."""
    from meshai.router import MessageRouter  # noqa: PLC0415

    config = Config()
    config.bot = BotConfig(respond_to_dms=respond_to_dms)
    config.meshcore_context = MeshCoreContextConfig(
        respond_to_dms=meshcore_respond_to_dms
    )

    connector = MagicMock()
    connector.my_node_id = "zzzz"

    router = MessageRouter.__new__(MessageRouter)
    router.config = config
    router.connector = connector
    router.meshmonitor_sync = None
    router.continuations = MagicMock()
    router.continuations.has_pending.return_value = False
    return router


# ---------------------------------------------------------------------------
# (a) should_respond — DM gate decoupling
# ---------------------------------------------------------------------------

def test_meshcore_dm_passes_when_bot_respond_to_dms_false():
    """MeshCore DM should reach should_respond=True even if bot.respond_to_dms=False.

    The global bot.respond_to_dms toggle is Meshtastic-only; MeshCore DMs
    are pre-filtered at the transport by meshcore_context.respond_to_dms.
    """
    router = _make_router(respond_to_dms=False, meshcore_respond_to_dms=True)
    msg = _make_message(transport="meshcore", is_dm=True)
    assert router.should_respond(msg) is True


def test_meshtastic_dm_blocked_when_bot_respond_to_dms_false():
    """Meshtastic DM must be blocked when bot.respond_to_dms=False."""
    router = _make_router(respond_to_dms=False)
    msg = _make_message(transport="meshtastic", is_dm=True)
    assert router.should_respond(msg) is False


def test_meshtastic_dm_passes_when_bot_respond_to_dms_true():
    """Meshtastic DM passes when bot.respond_to_dms=True."""
    router = _make_router(respond_to_dms=True)
    msg = _make_message(transport="meshtastic", is_dm=True)
    assert router.should_respond(msg) is True


# ---------------------------------------------------------------------------
# (b) get_context_block(transport=...) — per-mesh filtering
# ---------------------------------------------------------------------------

def test_get_context_block_no_transport_filter_returns_all():
    ctx = MeshContext()
    ctx.observe("Alice", "a1", "hello from MT", channel=0, is_dm=False, transport="meshtastic")
    ctx.observe("Bob", "b1", "hello from MC", channel=1, is_dm=False, transport="meshcore")
    block = ctx.get_context_block(transport=None)
    assert "hello from MT" in block
    assert "hello from MC" in block


def test_get_context_block_transport_meshtastic_only():
    ctx = MeshContext()
    ctx.observe("Alice", "a1", "hello from MT", channel=0, is_dm=False, transport="meshtastic")
    ctx.observe("Bob", "b1", "hello from MC", channel=1, is_dm=False, transport="meshcore")
    block = ctx.get_context_block(transport="meshtastic")
    assert "hello from MT" in block
    assert "hello from MC" not in block


def test_get_context_block_transport_meshcore_only():
    ctx = MeshContext()
    ctx.observe("Alice", "a1", "hello from MT", channel=0, is_dm=False, transport="meshtastic")
    ctx.observe("Bob", "b1", "hello from MC", channel=1, is_dm=False, transport="meshcore")
    block = ctx.get_context_block(transport="meshcore")
    assert "hello from MC" in block
    assert "hello from MT" not in block


def test_get_context_block_empty_when_no_matching_transport():
    ctx = MeshContext()
    ctx.observe("Alice", "a1", "hello from MT", channel=0, is_dm=False, transport="meshtastic")
    block = ctx.get_context_block(transport="meshcore")
    assert block == ""


# ---------------------------------------------------------------------------
# (c) Meshtastic channel-index filter is skipped for MeshCore observations
# ---------------------------------------------------------------------------

def test_meshtastic_channel_filter_skipped_for_meshcore():
    """MeshCore observations bypass the Meshtastic _observe_channels index filter.

    observe_channels=[0] would normally block channel=5 for Meshtastic, but a
    MeshCore observation with channel=5 must still be recorded.
    """
    ctx = MeshContext(observe_channels=[0])  # only Meshtastic ch0 allowed
    # MeshCore channel slot 5: should NOT be filtered by Meshtastic indices
    ctx.observe("MCNode", "mc1", "meshcore msg", channel=5, is_dm=False, transport="meshcore")
    # Meshtastic ch5: should be filtered out
    ctx.observe("MTNode", "mt1", "meshtastic msg", channel=5, is_dm=False, transport="meshtastic")

    assert ctx.count == 1
    block = ctx.get_context_block()
    assert "meshcore msg" in block
    assert "meshtastic msg" not in block


def test_meshtastic_channel_filter_still_applies_to_meshtastic():
    """Meshtastic channel-index filter still blocks unlisted Meshtastic channels."""
    ctx = MeshContext(observe_channels=[0])
    ctx.observe("MTNode", "mt1", "ch0 msg", channel=0, is_dm=False, transport="meshtastic")
    ctx.observe("MTNode", "mt1", "ch1 msg", channel=1, is_dm=False, transport="meshtastic")
    assert ctx.count == 1
    block = ctx.get_context_block()
    assert "ch0 msg" in block
    assert "ch1 msg" not in block


# ---------------------------------------------------------------------------
# (d) ContextConfig.max_age default is 14 days
# ---------------------------------------------------------------------------

def test_context_config_max_age_default_is_14_days():
    cfg = ContextConfig()
    assert cfg.max_age == 1_209_600, (
        f"Expected 1_209_600 (14 days) but got {cfg.max_age}"
    )
