"""
🔴 RED Phase — DM Engine Integrity Tests
==========================================

These tests expose 4 critical production bugs discovered in run 0f1475ff:
1. DM Engine ignores `dm_reply.enabled: false` — fires DMs anyway
2. DM Engine logs "Successfully sent" without verifying actual send
3. DM Engine generates replies with "No previous context" → garbage output
4. DM Engine lacks max-iteration guard → sent 8 DMs in 2 minutes

Each test MUST fail before any production code is touched (TDD RED).
"""

# ═══════════════════════════════════════════════════════
# Helpers — Minimal realistic mocks (no lying)
# ═══════════════════════════════════════════════════════
import os

FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")


def _load_fixture_xml(name):
    with open(os.path.join(FIXTURES_DIR, name), "r", encoding="utf-8") as f:
        return f.read()


def _load_fixture_img(name):
    return os.path.join(FIXTURES_DIR, name)


def _get_dm_inbox_pair():
    return _load_fixture_img("dm_inbox_dump.jpg"), _load_fixture_xml("dm_inbox_dump.xml")


def _get_dm_thread_pair():
    return _load_fixture_img("dm_thread_dump.jpg"), _load_fixture_xml("dm_thread_dump.xml")


# ═══════════════════════════════════════════════════════
# Test 1: DM Engine MUST respect dm_reply.enabled config
# ═══════════════════════════════════════════════════════


class TestDMConfigGating:
    """Verifies that dm_reply.enabled=false prevents ALL DM interactions."""

    def test_dm_engine_blocks_when_dm_reply_disabled(
        self, make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory
    ):
        """BUG: dm_engine.py:96 checks 'disable_ai_messaging' (doesn't exist)
        instead of dm_reply.enabled from config. This means DMs fire even when
        config says enabled: false.

        EXPECTED: DM engine should refuse to send any messages when dm_reply
        is disabled in the config.
        """
        from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
        from GramAddict.core.session_state import SessionState

        inbox_img, inbox_xml = _get_dm_inbox_pair()
        device = make_real_device_with_image(inbox_img, inbox_xml)

        if "dm_reply" not in e2e_configs.config["plugins"]:
            e2e_configs.config["plugins"]["dm_reply"] = {}
        e2e_configs.config["plugins"]["dm_reply"]["enabled"] = False

        session_state = SessionState(e2e_configs)
        session_state.set_limits_session()

        cognitive_stack = e2e_cognitive_stack_factory(device)

        # No patches, 100% real engine
        _run_zero_latency_dm_loop(
            device,
            make_real_device_with_image(inbox_img, inbox_xml),
            None,
            e2e_configs,
            session_state,
            "MessageInbox",
            cognitive_stack,
        )

        # No messages should be counted
        assert (
            getattr(session_state, "totalMessages", 0) == 0
        ), f"DM Engine sent {getattr(session_state, 'totalMessages', 0)} messages with dm_reply DISABLED!"


# ═══════════════════════════════════════════════════════
# Test 2: DM Engine MUST verify send actually happened
# ═══════════════════════════════════════════════════════


class TestDMSendVerification:
    """Verifies that 'Successfully sent' is only logged when the message was actually sent."""

    def test_dm_engine_successfully_identifies_and_sends_reply(
        self, make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory
    ):
        """
        EXPECTED: DM engine must use structural verification and successfully find the
        message input field, type the message, find the 'Send' button, and send the message
        using the real UI hierarchy and real LLM calls.
        """
        from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
        from GramAddict.core.session_state import SessionState

        inbox_img, inbox_xml = _get_dm_inbox_pair()
        thread_img, thread_xml = _get_dm_thread_pair()

        # Sequence for 1 reply:
        # Loop 1: Inbox (start) -> Thread (read msg) -> Thread (find Send button) -> Thread (check after 1st back press)
        # Note: We don't provide a loop 2 here since we test success. Dopamine might hit boredom and exit cleanly.
        # But wait, DM engine loops until dopamine triggers or MAX_REPLIES (3).
        # We will set boredom artificially high so it exits after 1 reply.
        device = make_real_device_with_image(
            [inbox_img, thread_img, thread_img, thread_img, inbox_img, thread_img],
            [inbox_xml, thread_xml, thread_xml, thread_xml, inbox_xml, thread_xml],
        )

        if "dm_reply" not in e2e_configs.config["plugins"]:
            e2e_configs.config["plugins"]["dm_reply"] = {}
        e2e_configs.config["plugins"]["dm_reply"]["enabled"] = True

        session_state = SessionState(e2e_configs)
        session_state.set_limits_session()

        cognitive_stack = e2e_cognitive_stack_factory(device)
        cognitive_stack["dopamine"].boredom = 99.0  # Will trigger BOREDOM_CHANGE_FEED on the next loop

        _run_zero_latency_dm_loop(
            device,
            make_real_device_with_image(inbox_img, inbox_xml),
            None,
            e2e_configs,
            session_state,
            "MessageInbox",
            cognitive_stack,
        )

        # Since we use real thread, it should successfully send 1 message!
        assert session_state.totalMessages > 0, "DM Engine failed to send a message on a real dm_thread_dump!"


# ═══════════════════════════════════════════════════════
# Test 3: DM Engine MUST NOT reply to context-less threads
# ═══════════════════════════════════════════════════════


class TestDMContextRequirement:
    """Verifies that the DM engine refuses to generate replies without context."""

    def test_dm_engine_skips_thread_with_no_extractable_message(
        self, make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory
    ):
        """BUG: dm_engine.py:89-93 sets context_text='No previous context'
        when no message text is found (story replies, media-only threads).
        Then proceeds to call the LLM with that string, producing garbage
        like 'the to the'.

        EXPECTED: When context_text is 'No previous context' or empty,
        the DM engine must SKIP the thread entirely (press back, continue).
        """
        from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
        from GramAddict.core.session_state import SessionState

        inbox_img, inbox_xml = _get_dm_inbox_pair()

        thread_img = "tests/fixtures/dm_thread_no_message.jpg"
        with open("tests/fixtures/dm_thread_no_message.xml", "r", encoding="utf-8") as f:
            thread_xml = f.read()

        # The VLM will correctly identify this as a thread, but will fail to extract a message context.
        # It should skip it and return to the inbox. Then we will provide a final inbox view.
        # We add an empty string at the end to simulate the thread being marked as read,
        # so it doesn't infinite loop on the same unread inbox.
        device = make_real_device_with_image(
            [inbox_img, thread_img, inbox_img, ""], [inbox_xml, thread_xml, inbox_xml, ""]
        )

        if "dm_reply" not in e2e_configs.config["plugins"]:
            e2e_configs.config["plugins"]["dm_reply"] = {}
        e2e_configs.config["plugins"]["dm_reply"]["enabled"] = True

        session_state = SessionState(e2e_configs)
        session_state.set_limits_session()

        cognitive_stack = e2e_cognitive_stack_factory(device)

        _run_zero_latency_dm_loop(
            device,
            None,
            None,
            e2e_configs,
            session_state,
            "MessageInbox",
            cognitive_stack,
        )

        # Since the thread has no extractable message, it MUST skip it and not reply.
        assert session_state.totalMessages == 0, "Replied to a thread with no message context!"


# ═══════════════════════════════════════════════════════
# Test 4: DM Engine MUST have max-iteration guard
# ═══════════════════════════════════════════════════════


class TestDMIterationLimit:
    """Verifies the DM engine doesn't spam infinite replies."""

    def test_dm_engine_caps_replies_per_session(
        self, make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory
    ):
        """BUG: dm_engine.py:34 while loop only exits on session timeout or
        boredom. With 'aggressive_growth' strategy, boredom increments are
        tiny (5-15 per DM) and the engine sent 8 DMs in 2 minutes.

        EXPECTED: DM engine must have an explicit max_replies_per_inbox
        cap (3 by default) to prevent spam behavior. After reaching the cap,
        it should return 'BOREDOM_CHANGE_FEED'.
        """
        from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
        from GramAddict.core.session_state import SessionState

        inbox_img, inbox_xml = _get_dm_inbox_pair()
        thread_img, thread_xml = _get_dm_thread_pair()

        # Sequence for 3 replies (production cap is 3):
        # Loop 1: Inbox -> Thread -> Thread -> Thread
        # Loop 2: Inbox -> Thread -> Thread -> Thread
        # Loop 3: Inbox -> Thread -> Thread -> Thread
        # Loop 4: Inbox -> Thread -> Exits because cap=3
        device = make_real_device_with_image(
            [
                inbox_img,
                thread_img,
                thread_img,
                thread_img,  # Loop 1
                inbox_img,
                thread_img,
                thread_img,
                thread_img,  # Loop 2
                inbox_img,
                thread_img,
                thread_img,
                thread_img,  # Loop 3
                inbox_img,
                thread_img,  # Loop 4 (hits limit)
            ],
            [
                inbox_xml,
                thread_xml,
                thread_xml,
                thread_xml,
                inbox_xml,
                thread_xml,
                thread_xml,
                thread_xml,
                inbox_xml,
                thread_xml,
                thread_xml,
                thread_xml,
                inbox_xml,
                thread_xml,
            ],
        )

        if "dm_reply" not in e2e_configs.config["plugins"]:
            e2e_configs.config["plugins"]["dm_reply"] = {}
        e2e_configs.config["plugins"]["dm_reply"]["enabled"] = True

        session_state = SessionState(e2e_configs)
        session_state.set_limits_session()

        cognitive_stack = e2e_cognitive_stack_factory(device)

        # Force the session limits higher so it strictly tests the inner loop cap
        e2e_configs.args.current_success_limit = 8
        e2e_configs.args.current_pm_limit = 8
        session_state.totalMessages = 0

        _run_zero_latency_dm_loop(
            device,
            make_real_device_with_image(inbox_img, inbox_xml),
            None,
            e2e_configs,
            session_state,
            "MessageInbox",
            cognitive_stack,
        )

        # The engine should have self-limited to at most 5 replies
        assert session_state.totalMessages <= 5, (
            f"DM Engine sent {session_state.totalMessages} messages in one inbox visit. "
            f"Expected hard cap of <= 5 to prevent spam."
        )


# ═══════════════════════════════════════════════════════
# Test 5: DM Engine config gating uses the REAL production path
# ═══════════════════════════════════════════════════════


class TestDMConfigGatingProduction:
    """Verifies the REAL dm_engine._run_zero_latency_dm_loop config check,
    not a local re-implementation of bot_flow.py logic."""

    def test_dm_engine_config_gating_reads_real_plugin_config(self, e2e_configs):
        """The dm_engine kill-switch at line 46-50 reads configs.get_plugin_config('dm_reply').
        We verify this path with the real Config class — NOT a local dict simulation."""

        if "dm_reply" not in e2e_configs.config["plugins"]:
            e2e_configs.config["plugins"]["dm_reply"] = {}

        e2e_configs.config["plugins"]["dm_reply"]["enabled"] = False
        dm_config_off = e2e_configs.get_plugin_config("dm_reply")
        assert dm_config_off.get("enabled", False) is False, "Config should report dm_reply as disabled"

        e2e_configs.config["plugins"]["dm_reply"]["enabled"] = True
        dm_config_on = e2e_configs.get_plugin_config("dm_reply")
        assert dm_config_on.get("enabled", False) is True, "Config should report dm_reply as enabled"
