from unittest.mock import MagicMock

import pytest

from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB


def test_parasocial_crm_missing_log_sent_dm():
    """
    RED: This test proves that ParasocialCRMDB lacks log_sent_dm,
    which caused the crash in the last production run.
    """
    crm = ParasocialCRMDB()
    with pytest.raises(AttributeError) as excinfo:
        crm.log_sent_dm("test_user", "hello", "bio", [])

    assert "'ParasocialCRMDB' object has no attribute 'log_sent_dm'" in str(excinfo.value)


def test_dm_engine_uses_correct_dm_memory_logging(monkeypatch):
    """
    RED: This test checks if dm_engine correctly uses dm_memory from cognitive_stack.
    Note: I am writing this to PROVE the fix is necessary.
    """
    mock_device = MagicMock()
    mock_device.dump_hierarchy.return_value = (
        '<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>'
    )

    mock_telepathic = MagicMock()
    # Mock unread thread found
    mock_thread = {"x": 100, "y": 100, "text": "Mariischen"}

    def side_effect_logging(*args, **kwargs):
        res = next(iterator)
        print(f"DEBUG: extract_semantic_nodes called with {args[1]}. Returning {res}")
        return res

    iterator = iter(
        [
            [mock_thread],  # Step 1: unread threads
            [{"text": "Hey"}],  # Step 2: context
            [{"x": 200, "y": 200}],  # Step 3: input field
            [{"x": 300, "y": 300}],  # Step 4: send button
            [],  # Step 5: next iteration no unread
        ]
    )
    mock_telepathic._extract_semantic_nodes.side_effect = side_effect_logging

    mock_dopamine = MagicMock()
    mock_dopamine.is_app_session_over.return_value = False
    mock_dopamine.wants_to_change_feed.return_value = True  # exit after one
    mock_dopamine.boredom = 0

    mock_crm = ParasocialCRMDB()
    mock_dm_memory = MagicMock(spec=DMMemoryDB)
    mock_resonance = MagicMock()
    mock_resonance.persona_prompt = "Persona prompt"
    mock_resonance.args.ai_model = "test-model"

    cognitive_stack = {
        "telepathic": mock_telepathic,
        "dopamine": mock_dopamine,
        "crm": mock_crm,
        "dm_memory": mock_dm_memory,
        "resonance": mock_resonance,
    }

    mock_session = MagicMock()
    mock_session.check_limit.return_value = False
    mock_session.totalMessages = 0

    # Mock LLM response
    monkeypatch.setattr("GramAddict.core.llm_provider.query_llm", lambda **k: {"response": "hi"})
    monkeypatch.setattr("GramAddict.core.bot_flow._humanized_click", lambda *a: None)
    monkeypatch.setattr("GramAddict.core.bot_flow.sleep", lambda *a: None)
    monkeypatch.setattr("GramAddict.core.stealth_typing.ghost_type", lambda *a, **k: None)

    mock_configs = MagicMock()
    mock_configs.args.disable_ai_messaging = False
    mock_configs.args.ai_condenser_model = "test-model"
    mock_configs.args.ai_condenser_url = "http://localhost:11434/api/generate"

    # This should NOT crash now because I fixed it, but we are testing the logic.

    _run_zero_latency_dm_loop(
        mock_device, MagicMock(), MagicMock(), mock_configs, mock_session, "target", cognitive_stack
    )

    # Verify dm_memory was used, NOT crm
    mock_dm_memory.log_sent_dm.assert_called_once()


def test_dm_navigation_double_back_guard():
    """
    Verifies that if we are still in a thread after one back press,
    we press back again.
    """
    mock_device = MagicMock()
    # Mocking hierarchy sequence
    mock_device.dump_hierarchy.side_effect = [
        '<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>',  # Loop 1 start
        '<xml>resource-id="com.instagram.android:id/direct_thread_header"</xml>',  # Context read
        '<xml>resource-id="com.instagram.android:id/direct_thread_header"</xml>',  # Send button find
        '<xml>resource-id="com.instagram.android:id/direct_thread_header"</xml>',  # Navigation check AFTER back
        '<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>',  # Loop 2 start (exit)
        '<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>',  # Buffer
    ]

    mock_telepathic = MagicMock()
    mock_telepathic._extract_semantic_nodes.side_effect = [
        [{"x": 1, "y": 1}],  # unread found in Loop 1
        [{"text": "msg"}],  # context
        [{"x": 2, "y": 2}],  # input
        [{"x": 3, "y": 3}],  # send
        [],  # Loop 2: no unread
        [],  # Buffer
    ]

    mock_dopamine = MagicMock()
    mock_dopamine.is_app_session_over.return_value = False
    mock_dopamine.wants_to_change_feed.side_effect = [False, True, True]  # exit after Loop 1
    mock_dopamine.boredom = 0

    mock_resonance = MagicMock()
    mock_resonance.persona_prompt = "Persona"
    mock_resonance.args.ai_model = "model"

    cognitive_stack = {
        "telepathic": mock_telepathic,
        "dopamine": mock_dopamine,
        "dm_memory": MagicMock(),
        "resonance": mock_resonance,
    }

    mock_session = MagicMock()
    mock_session.check_limit.return_value = False
    mock_session.totalMessages = 0

    mock_configs = MagicMock()
    mock_configs.args.disable_ai_messaging = False
    mock_configs.args.ai_condenser_model = "test-model"
    mock_configs.args.ai_condenser_url = "http://localhost:11434/api/generate"

    from unittest.mock import patch

    import GramAddict.core.dm_engine as dm_engine

    with (
        patch("GramAddict.core.llm_provider.query_llm", return_value={"response": "hi"}),
        patch("GramAddict.core.bot_flow._humanized_click"),
        patch("GramAddict.core.bot_flow.sleep"),
        patch("GramAddict.core.stealth_typing.ghost_type"),
    ):
        dm_engine._run_zero_latency_dm_loop(
            mock_device, MagicMock(), MagicMock(), mock_configs, mock_session, "target", cognitive_stack
        )

    # Expected calls:
    # 1. First back from success flow (thread -> inbox)
    # 2. Second back from guard check (if still in thread)
    # 3. Third back from inbox exit (boredom check)
    assert mock_device.press.call_count == 3
    mock_device.press.assert_called_with("back")
