from unittest.mock import MagicMock, patch

import pytest

# Force mock qdrant_client before importing any core modules that depend on it
from GramAddict.core.q_nav_graph import QNavGraph


class TestQNavGraphEdgeCases:
    @pytest.fixture(autouse=True)
    def setup_graph(self):
        self.device = MagicMock()
        self.device.app_id = "com.instagram.android"
        self.device.info = {"screenOn": True}
        self.device.dump_hierarchy.return_value = '<hierarchy><node package="com.instagram.android" /></hierarchy>'
        self.device._get_current_app = MagicMock(return_value="com.instagram.android")

        # Prevent Dojo engine instantiation during tests
        with patch("GramAddict.core.compiler_engine.VLMCompilerEngine"):
            self.graph = QNavGraph(self.device)

    def test_find_path_edge_cases(self):
        # 1. Start == End
        assert self.graph._find_path("HomeFeed", "HomeFeed") == []

        # 2. Start not in nodes
        assert self.graph._find_path("UnknownState", "HomeFeed") is None

        # 3. Unreachable states
        self.graph.nodes = {
            "HomeFeed": {"transitions": {"tap_explore": "ExploreFeed"}},
            "IsolatedFeed": {"transitions": {}},
        }
        assert self.graph._find_path("HomeFeed", "IsolatedFeed") is None

        # 4. Infinite loop protection (A -> B -> A)
        self.graph.nodes = {"A": {"transitions": {"to_b": "B"}}, "B": {"transitions": {"to_a": "A"}}}
        assert (
            self.graph._find_path("A", "C") is None
        )  # Should safely return None without exceeding recursion/loop depth

        # 5. Longest path possible before unreachability is confirmed
        assert self.graph._find_path("B", "D") is None

        # 6. Diamond shape path
        self.graph.nodes = {
            "Start": {"transitions": {"top": "Top", "bottom": "Bottom"}},
            "Top": {"transitions": {"top_to_end": "End"}},
            "Bottom": {"transitions": {"bottom_to_end": "End"}},
            "End": {},
        }
        # BFS should find shortest path (len 2)
        assert len(self.graph._find_path("Start", "End")) == 2

    @patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
    @patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
    @patch("GramAddict.core.situational_awareness.random_sleep", return_value=None)
    @patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
    def test_execute_transition_edge_cases(self, mock_get_telepathic, mock_sae_sleep, mock_q_rand_sleep, mock_q_sleep):
        from GramAddict.core.telepathic_engine import TelepathicEngine

        mock_engine = MagicMock(spec=TelepathicEngine)
        mock_get_telepathic.return_value = mock_engine

        # Case 1: Telepathic engine finds nothing
        mock_engine.find_best_node.return_value = None

        # If still in Instagram, it returns False
        self.device._get_current_app.return_value = "com.instagram.android"
        assert not self.graph._execute_transition("unknown_action", mock_engine)

        # If app is different, it returns "CONTEXT_LOST"
        self.device._get_current_app.return_value = "com.android.launcher3"
        assert self.graph._execute_transition("unknown_action", mock_engine) == "CONTEXT_LOST"

        # Case 2: Best node has skip flag
        mock_engine.find_best_node.return_value = {"skip": True}
        assert self.graph._execute_transition("already_done_action", mock_engine)

        # Case 3: Proper interaction, but XML doesn't change (verification fail)
        mock_engine.find_best_node.return_value = {"x": 10, "y": 10, "score": 0.9}
        same_xml = '<hierarchy><node package="com.instagram.android" class="same" /></hierarchy>'
        self.device.dump_hierarchy.side_effect = None
        self.device.dump_hierarchy.return_value = same_xml
        assert not self.graph._execute_transition("click_action", mock_engine)
        assert mock_engine.reject_click.call_count == 3

        # Case 4: Proper interaction, XML changes (verification pass)
        mock_engine.reset_mock()
        mock_engine.find_best_node.return_value = {"x": 10, "y": 10, "score": 0.9}
        before_xml = '<hierarchy><node package="com.instagram.android" class="before" /></hierarchy>'
        after_xml = '<hierarchy><node package="com.instagram.android" class="after" /></hierarchy>'

        initial_clicks = self.device.click.call_count

        def dynamic_xml(*args, **kwargs):
            return after_xml if self.device.click.call_count > initial_clicks else before_xml

        self.device.dump_hierarchy.side_effect = dynamic_xml
        # Explicitly ensure verify_success is truthy
        mock_engine.verify_success.return_value = True

        assert self.graph._execute_transition("click_action", mock_engine)
        mock_engine.confirm_click.assert_called_once()

    @patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
    @patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
    @patch("GramAddict.core.situational_awareness.random_sleep", return_value=None)
    @patch("GramAddict.core.dojo_engine.DojoEngine.get_instance")
    def test_navigate_to_recovery_edge_cases(self, mock_dojo, mock_sae_sleep, mock_q_rand_sleep, mock_q_sleep):
        # We test the deepest recovery logic: when everything fails

        zero_engine = MagicMock()

        # Mock transitions completely failing
        with patch.object(self.graph.goap, "navigate_to_screen", return_value=False):
            # Recovery attempts maxed out
            assert not self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=3)

            # Start logic where path is None and direct fallback also fails
            self.graph.current_state = "IsolatedNode"
            # It should trigger fallback and then return False because `navigate_to_screen` always returns False
            assert not self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=0)
