from unittest.mock import MagicMock, patch

from GramAddict.core.q_nav_graph import QNavGraph


class TestAnomalyInterruptions:
    def setup_method(self):
        self.mock_device = MagicMock()
        self.mock_device.deviceV2 = MagicMock()
        self.mock_device.app_id = "com.instagram.android"
        self.mock_device._get_current_app.return_value = "com.instagram.android"

        self.nav_graph = QNavGraph(self.mock_device)
        self.nav_graph = QNavGraph(self.mock_device)
        # We test the LLM fallback planner logic here.
        # Since the legacy structural planner was removed, we must mock the LLM
        # to ensure deterministic test execution.
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine

        sae = SituationalAwarenessEngine.get_instance(self.mock_device)
        sae.episodes.recall = MagicMock(return_value=None)
        sae.episodes.learn = MagicMock()

        # We must also mock ScreenMemoryDB to prevent cached misclassifications
        # from bypassing the LLM in perceive()
        self.screen_memory_patcher = patch("GramAddict.core.qdrant_memory.ScreenMemoryDB")
        self.mock_screen_memory_cls = self.screen_memory_patcher.start()
        self.mock_screen_memory = self.mock_screen_memory_cls.return_value
        self.mock_screen_memory.get_screen_type.return_value = None

    def teardown_method(self):
        self.screen_memory_patcher.stop()

    def test_os_permission_dialog_denial(self):
        """
        Z-Depth Guard must explicitly attempt to find a 'Deny' / 'Don't allow' button
        when encountering the Android permission modal instead of just pressing BACK.
        """
        obstacle_xml = """
        <hierarchy>
          <node package="com.android.permissioncontroller" resource-id="com.android.permissioncontroller:id/grant_dialog">
            <node package="com.android.permissioncontroller" text="Allow Instagram to access your location?" />
            <node package="com.android.permissioncontroller" text="While using the app" resource-id="com.android.permissioncontroller:id/permission_allow_button" bounds="[100,500][900,600]" clickable="true" />
            <node package="com.android.permissioncontroller" text="Don't allow" resource-id="com.android.permissioncontroller:id/permission_deny_button" bounds="[100,650][900,750]" clickable="true" />
          </node>
        </hierarchy>
        """
        normal_xml = '<hierarchy><node package="com.instagram.android" /></hierarchy>'

        # 1. Obstacle Check (Attempt 1 Start) uses initial_xml (passed in _clear_anomaly_obstacles)
        # 2. Re-dump in evaluate loop (next iterations)
        self.mock_device.dump_hierarchy.side_effect = [normal_xml, normal_xml, normal_xml]

        def mock_llm_side_effect(*args, **kwargs):
            system_arg = kwargs.get("system")
            if not system_arg and len(args) > 4:
                system_arg = args[4]
            prompt_arg = kwargs.get("prompt")
            if not prompt_arg and len(args) > 2:
                prompt_arg = args[2]

            if system_arg == "Strict JSON classifier.":
                if "How are we doing?" in prompt_arg or "Allow Instagram" in prompt_arg:
                    return {"response": '{"situation": "OBSTACLE_MODAL"}'}
                return {"response": '{"situation": "NORMAL"}'}
            return {"response": '{"action": "click", "x": 500, "y": 700, "reason": "Deny permission"}'}

        with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
            mock_llm.side_effect = mock_llm_side_effect
            # When checking for obstacles, it should clear it by clicking deny
            cleared = self.nav_graph._clear_anomaly_obstacles(xml_dump=obstacle_xml)

        assert cleared is True, "Z-Depth Guard failed to clear the OS permission modal"
        assert self.mock_device.click.call_count >= 1
        # Verify it clicked the 'Don't allow' button coordinates ([100,650][900,750] avg is [500, 700])
        args, _ = self.mock_device.click.call_args
        assert args[0] == 500
        assert args[1] == 700

    def test_instagram_survey_dismissal(self):
        """
        Instagram can prompt surveys mid-run. We must dismiss them gracefully.

        The SAE prioritises BACK (priority=-1) for OBSTACLE_MODAL obstacles — this is
        semantically correct because BACK dismisses Instagram modals reliably without
        risk of accidentally tapping dangerous buttons.  If BACK clears the screen
        (i.e. post-action XML is NORMAL) the SAE considers the obstacle resolved and
        returns True immediately without ever needing to click 'Not Now'.

        This test verifies that the SAE takes at least one dismissal action
        (either a BACK press OR a direct click on 'Not Now') and that the function
        reports the obstacle as cleared.
        """
        obstacle_xml = """
        <hierarchy>
          <node package="com.instagram.android" resource-id="com.instagram.android:id/survey_container">
            <node package="com.instagram.android" text="How are we doing?" resource-id="com.instagram.android:id/survey_title" />
            <node package="com.instagram.android" text="Take Survey" resource-id="com.instagram.android:id/take_survey_btn" bounds="[200,800][800,900]" clickable="true" />
            <node package="com.instagram.android" text="Not Now" resource-id="com.instagram.android:id/button_negative" bounds="[200,950][800,1050]" clickable="true" />
          </node>
        </hierarchy>
        """
        normal_xml = '<hierarchy><node package="com.instagram.android" /></hierarchy>'

        # After any dismissal action the screen returns to normal
        self.mock_device.dump_hierarchy.side_effect = [normal_xml, normal_xml, normal_xml]

        def mock_llm_side_effect(*args, **kwargs):
            system_arg = kwargs.get("system")
            if not system_arg and len(args) > 4:
                system_arg = args[4]
            prompt_arg = kwargs.get("prompt")
            if not prompt_arg and len(args) > 2:
                prompt_arg = args[2]

            if system_arg == "Strict JSON classifier.":
                if "How are we doing?" in prompt_arg or "Allow Instagram" in prompt_arg:
                    return {"response": '{"situation": "OBSTACLE_MODAL"}'}
                return {"response": '{"situation": "NORMAL"}'}
            return {"response": '{"action": "back", "reason": "Safe dismissal of modal"}'}

        with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
            mock_llm.side_effect = mock_llm_side_effect
            cleared = self.nav_graph._clear_anomaly_obstacles(xml_dump=obstacle_xml)

        # Primary assertion: the SAE reported success
        assert cleared is True, "Instagram survey was not dismissed"

        # Secondary assertion: at least one dismissal action occurred.
        # The SAE may press BACK (priority=-1 for OBSTACLE_MODAL) or click 'Not Now'.
        pressed_back = self.mock_device.press.called and any(
            (a.args[0] if a.args else None) == "back" for a in self.mock_device.press.call_args_list
        )
        did_click = self.mock_device.click.call_count >= 1

        assert (
            pressed_back or did_click
        ), "SAE did not take any dismissal action (expected BACK press or click on 'Not Now')"

    def test_fake_creation_flow_in_bio_is_ignored(self):
        """
        Ensures that a user bio containing 'quick_capture' or 'creation_flow'
        does not falsely trigger the structural OBSTACLE_MODAL states.
        """
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

        sae = SituationalAwarenessEngine.get_instance()

        # XML containing the marker in text, not id
        xml = """<hierarchy><node package="com.instagram.android" text="I love the post creation_flow" /></hierarchy>"""

        with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
            mock_llm.return_value = {"response": '{"situation": "NORMAL"}'}
            result = sae.perceive(xml)
            assert result == SituationType.NORMAL

    def test_real_creation_flow_is_caught(self):
        """
        Ensures that a real structural marker in the resource-id triggers OBSTACLE_MODAL.
        """
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

        sae = SituationalAwarenessEngine.get_instance()

        xml = """<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/quick_capture_root_container" /></hierarchy>"""

        with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
            result = sae.perceive(xml)
            assert result == SituationType.OBSTACLE_MODAL
            mock_llm.assert_not_called()

    def test_fake_action_blocked_in_caption_is_ignored(self):
        """
        Ensures that 'action blocked' in a text attribute without a dialog container
        does NOT trigger DANGER_ACTION_BLOCKED.
        """
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

        sae = SituationalAwarenessEngine.get_instance()

        xml = """<hierarchy><node package="com.instagram.android" text="My account was action blocked yesterday!" /></hierarchy>"""

        with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
            mock_llm.return_value = {"response": '{"situation": "NORMAL"}'}
            result = sae.perceive(xml)
            assert result != SituationType.DANGER_ACTION_BLOCKED

    def test_real_action_blocked_is_caught(self):
        """
        Ensures that 'action blocked' with a dialog container triggers DANGER_ACTION_BLOCKED.
        """
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

        sae = SituationalAwarenessEngine.get_instance()

        xml = """<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/dialog_container"><node package="com.instagram.android" text="Action Blocked" /></node></hierarchy>"""

        result = sae.perceive(xml)
        assert result == SituationType.DANGER_ACTION_BLOCKED
