from unittest.mock import MagicMock, patch

import pytest

# Force mock qdrant_client before importing any core modules that depend on it
from GramAddict.core.bot_flow import _extract_post_content, _run_zero_latency_feed_loop


class TestBotFlowEdgeCases:
    @patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
    def test_extract_post_content_edge_cases(self, mock_get_telepathic):
        mock_engine = MagicMock()
        mock_get_telepathic.return_value = mock_engine

        # 1. Empty string / Invalid XML should not crash (mock finds nothing)
        mock_engine.find_best_node.return_value = None
        res = _extract_post_content("")
        assert res.get("username") == ""
        assert res.get("description") == ""

        # 2. Extract when only username exists
        # Side effect: first call (author) returns node, second (media) returns None
        mock_engine.find_best_node.side_effect = [{"original_attribs": {"text": "just_user"}}, None]
        res = _extract_post_content("<xml/>")
        assert res.get("username") == "just_user"
        assert res.get("description") == ""

        # 3. Extract description
        mock_engine.find_best_node.side_effect = [None, {"original_attribs": {"desc": "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"}}]
        res = _extract_post_content("<xml/>")
        assert res.get("description") == "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"

        # 4. Another valid description tag
        mock_engine.find_best_node.side_effect = [
            None,
            {"original_attribs": {"desc": "some desc with more than 10 chars limits"}},
        ]
        res = _extract_post_content("<xml/>")
        assert res.get("description") == "some desc with more than 10 chars limits"

    @patch("GramAddict.core.bot_flow.random.random", return_value=0.5)
    @patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5)
    @patch("GramAddict.core.bot_flow.sleep")
    @patch("GramAddict.core.bot_flow._humanized_scroll")
    @patch("GramAddict.core.utils.is_ad")
    @patch("GramAddict.core.bot_flow._align_active_post")
    @patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
    def test_zero_node_recovery(
        self, mock_get_telepathic, mock_align, mock_ad, mock_scroll, mock_sleep, mock_uniform, mock_random
    ):
        # Tests the explicit Zero-Node Recovery added previously
        device = MagicMock()
        zero_engine = MagicMock()
        nav_graph = MagicMock()
        configs = MagicMock()
        session_state = MagicMock()

        mock_ad.return_value = False
        mock_align.return_value = False

        cognitive_stack = {
            "dopamine": MagicMock(),
            "darwin": MagicMock(),
            "resonance": MagicMock(),
            "active_inference": MagicMock(),
            "growth_brain": MagicMock(),
            "swarm": MagicMock(),
        }

        # Dopamine breaks loop after 1st iteration
        cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
        cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
        cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False

        # Fake extreme limits => doesn't break limits
        session_state.check_limit.return_value = [False] * 10

        # Telepathic Engine returns ZERO nodes on extract
        mock_engine = MagicMock()
        mock_engine._extract_semantic_nodes.return_value = []
        mock_get_telepathic.return_value = mock_engine

        device.dump_hierarchy.return_value = "<xml></xml>"

        # Execute the main loop
        _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)

        # It should trigger _humanized_scroll
        assert mock_scroll.call_count >= 1

    @patch("GramAddict.core.bot_flow.random.random", return_value=0.5)
    @patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5)
    @patch("GramAddict.core.bot_flow.sleep")
    @patch("GramAddict.core.bot_flow._humanized_scroll")
    @patch("GramAddict.core.bot_flow._extract_post_content")
    @patch("GramAddict.core.utils.is_ad")
    @patch("GramAddict.core.bot_flow._align_active_post")
    @patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
    def test_content_extraction_failed_recovery(
        self,
        mock_get_telepathic,
        mock_align,
        mock_ad,
        mock_extract,
        mock_scroll,
        mock_sleep,
        mock_uniform,
        mock_random,
    ):
        device = MagicMock()
        zero_engine = MagicMock()
        nav_graph = MagicMock()
        configs = MagicMock()
        session_state = MagicMock()

        mock_ad.return_value = False
        mock_align.return_value = False

        cognitive_stack = {"dopamine": MagicMock(), "darwin": MagicMock()}
        # break after 1 loop
        cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
        cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
        cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
        session_state.check_limit.return_value = [False] * 10

        # Ensure it HAS feed markers
        device.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"

        # Ensure interactive_nodes is NOT zero
        mock_engine = MagicMock()
        mock_engine._extract_semantic_nodes.return_value = [{"x": 10}]
        mock_get_telepathic.return_value = mock_engine

        # Make the extraction fail
        mock_extract.return_value = {"username": "", "description": ""}

        _run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)

        # Should call mock_scroll (Graceful degradation)
        mock_scroll.assert_called_once()

    @patch("GramAddict.core.bot_flow.sleep")
    @patch("GramAddict.core.bot_flow._humanized_scroll")
    @patch("GramAddict.core.utils.is_ad")
    @patch("GramAddict.core.bot_flow._align_active_post")
    @patch("GramAddict.core.bot_flow._extract_post_content")
    @patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
    @patch("GramAddict.core.llm_provider.query_llm")
    def test_llm_timeout_handled_smoothly(
        self, mock_query_llm, mock_get_telepathic, mock_extract, mock_align, mock_ad, mock_scroll, mock_sleep
    ):
        """
        TDD Test: Verifies that if qwen3.5:latest times out during comment generation
        (simulated by query_llm returning None after circuit breaker), the bot_flow
        catches the empty response and continues gracefully without crashing.
        """
        device = MagicMock()
        zero_engine = MagicMock()
        nav_graph = MagicMock()
        configs = MagicMock()
        session_state = MagicMock()

        mock_ad.return_value = False
        mock_align.return_value = False

        # Make the LLM generation completely timeout and return None
        mock_query_llm.return_value = None

        cognitive_stack = {"dopamine": MagicMock(), "darwin": MagicMock(), "resonance": MagicMock()}
        # break after 1 loop
        cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
        cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
        cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False

        # Emulate that dopamine WANTS to comment
        cognitive_stack["dopamine"].get_action_desires.return_value = {"comment": True, "like": False}

        # Avoid MagicMock comparison errors in Resonance Engine
        cognitive_stack["resonance"].calculate_resonance.return_value = 0.8

        session_state.check_limit.return_value = [False] * 10

        device.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"

        mock_engine = MagicMock()
        mock_engine._extract_semantic_nodes.return_value = [{"x": 10}]
        mock_get_telepathic.return_value = mock_engine

        # Valid post content so it proceeds to comment generation
        mock_extract.return_value = {"username": "test_user", "description": "a long enough description"}

        # Run feed loop - MUST NOT CRASH
        try:
            _run_zero_latency_feed_loop(
                device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack
            )
        except Exception as e:
            pytest.fail(f"Feed loop crashed on LLM timeout with {e}")
