"""
Keyboard Contamination Guard — TDD Tests

Production Bug 2026-05-04: The VLM clicked on the comment composer text view,
which opened the Android keyboard. All subsequent intents became poisoned because
keyboard key nodes (com.google.android.inputmethod.latin) flooded the candidate
list with 30+ single-character entries (A, B, C, N, M, ...).

The VLM then hallucinated 'N' as the "post username" and clicked it.

These tests enforce:
1. Keyboard nodes are NEVER included in visual discovery candidates
2. The resolver can detect when a keyboard is open
3. When keyboard is present, non-keyboard candidates still resolve correctly
"""

from GramAddict.core.perception.intent_resolver import IntentResolver
from GramAddict.core.perception.spatial_parser import SpatialNode

# ═══════════════════════════════════════════════════════
# Fixtures: Keyboard-Contaminated Candidate Lists
# ═══════════════════════════════════════════════════════


def _make_keyboard_nodes() -> list[SpatialNode]:
    """Generates realistic Android soft-keyboard nodes that pollute the candidate pool."""
    keys = [
        ("Q", "com.google.android.inputmethod.latin:id/B00"),
        ("W", "com.google.android.inputmethod.latin:id/B01"),
        ("E", "com.google.android.inputmethod.latin:id/B02"),
        ("R", "com.google.android.inputmethod.latin:id/B03"),
        ("T", "com.google.android.inputmethod.latin:id/B04"),
        ("Z", "com.google.android.inputmethod.latin:id/B05"),
        ("N", "com.google.android.inputmethod.latin:id/B06"),
        ("Löschen", "com.google.android.inputmethod.latin:id/key_pos_del"),
        ("Senden", "com.google.android.inputmethod.latin:id/key_pos_ime_action"),
        ("Leerzeichen DE • EN", "com.google.android.inputmethod.latin:id/key_pos_space"),
        ("Shift enabled", "com.google.android.inputmethod.latin:id/key_pos_shift"),
        (",", "com.google.android.inputmethod.latin:id/key_pos_comma"),
        (".", "com.google.android.inputmethod.latin:id/key_pos_period"),
        ("Symboltastatur ?123", "com.google.android.inputmethod.latin:id/key_pos_symbol"),
        ("Emoji-Button", "com.google.android.inputmethod.latin:id/key_pos_emoji"),
    ]
    nodes = []
    y = 1800
    for i, (desc, rid) in enumerate(keys):
        x = (i % 10) * 100
        nodes.append(
            SpatialNode(
                resource_id=rid,
                class_name="android.widget.FrameLayout",
                text="",
                content_desc=desc,
                bounds=(x, y, x + 90, y + 100),
                clickable=True,
            )
        )
    return nodes


def _make_instagram_post_detail_nodes() -> list[SpatialNode]:
    """Generates realistic Instagram POST_DETAIL nodes."""
    return [
        SpatialNode(
            resource_id="com.instagram.android:id/row_feed_photo_profile_name",
            class_name="android.widget.TextView",
            text="robert_bohnke",
            content_desc="",
            bounds=(100, 400, 400, 440),
            clickable=True,
        ),
        SpatialNode(
            resource_id="com.instagram.android:id/row_feed_photo_profile_imageview",
            class_name="android.widget.ImageView",
            text="",
            content_desc="Profile picture of robert_bohnke",
            bounds=(20, 400, 80, 460),
            clickable=True,
        ),
        SpatialNode(
            resource_id="com.instagram.android:id/row_feed_button_like",
            class_name="android.widget.ImageView",
            text="",
            content_desc="Like",
            bounds=(20, 1200, 100, 1280),
            clickable=True,
        ),
        SpatialNode(
            resource_id="com.instagram.android:id/row_feed_button_comment",
            class_name="android.widget.ImageView",
            text="",
            content_desc="Comment",
            bounds=(120, 1200, 200, 1280),
            clickable=True,
        ),
        SpatialNode(
            resource_id="com.instagram.android:id/row_feed_button_share",
            class_name="android.widget.ImageView",
            text="",
            content_desc="Send post",
            bounds=(220, 1200, 300, 1280),
            clickable=True,
        ),
        SpatialNode(
            resource_id="com.instagram.android:id/comment_composer_text_view",
            class_name="android.widget.EditText",
            text="Add comment…",
            content_desc="",
            bounds=(100, 1500, 800, 1560),
            clickable=True,
        ),
    ]


# ═══════════════════════════════════════════════════════
# TEST 1: Keyboard nodes are filtered from candidates
# ═══════════════════════════════════════════════════════


class TestKeyboardContaminationGuard:
    def test_keyboard_nodes_filtered_from_visual_discovery_candidates(self):
        """
        When the keyboard is open, _visual_discovery MUST exclude all nodes
        from keyboard packages (com.google.android.inputmethod.*).

        This prevents the VLM from seeing 30+ single-letter boxes
        and hallucinating keyboard keys as valid UI targets.
        """
        resolver = IntentResolver()
        keyboard_nodes = _make_keyboard_nodes()
        instagram_nodes = _make_instagram_post_detail_nodes()
        all_candidates = instagram_nodes + keyboard_nodes

        # The production pre-filter must strip keyboard nodes
        filtered = resolver.pre_filter_candidates(all_candidates)

        # ZERO keyboard nodes should survive
        keyboard_survivors = [n for n in filtered if "inputmethod" in (n.resource_id or "")]
        assert (
            len(keyboard_survivors) == 0
        ), f"Keyboard nodes leaked through filter: {[n.content_desc for n in keyboard_survivors]}"

        # Instagram nodes MUST survive
        instagram_survivors = [n for n in filtered if "instagram" in (n.resource_id or "")]
        assert len(instagram_survivors) > 0, "Instagram nodes were incorrectly filtered!"

    def test_structural_fast_path_ignores_keyboard_even_without_filter(self):
        """
        Even if keyboard nodes somehow pass pre-filtering, the structural
        fast-path for 'tap post username' must NEVER resolve to a keyboard key.
        """
        resolver = IntentResolver()
        keyboard_nodes = _make_keyboard_nodes()
        instagram_nodes = _make_instagram_post_detail_nodes()
        all_candidates = instagram_nodes + keyboard_nodes

        result = resolver.resolve("tap post username", all_candidates, screen_height=2400)

        assert result is not None, "Resolver returned None for 'tap post username'"
        assert "inputmethod" not in (
            result.resource_id or ""
        ), f"Resolver picked a KEYBOARD KEY: {result.resource_id} (desc: {result.content_desc})"
        assert (
            "row_feed_photo_profile" in (result.resource_id or "").lower()
        ), f"Expected profile imageview or name, got: {result.resource_id}"

    def test_keyboard_detection_helper(self):
        """
        The IntentResolver must expose a method to detect if the keyboard
        is open based on the candidate list's package names.
        """
        resolver = IntentResolver()
        keyboard_nodes = _make_keyboard_nodes()
        instagram_nodes = _make_instagram_post_detail_nodes()

        assert resolver.has_keyboard_open(instagram_nodes + keyboard_nodes) is True
        assert resolver.has_keyboard_open(instagram_nodes) is False

    def test_send_post_button_resolves_to_share_not_comment_composer(self):
        """
        The 'tap send post button' intent MUST resolve to row_feed_button_share,
        NOT to comment_composer_text_view.

        Production Bug 2026-05-04: VLM selected comment_composer → keyboard opened.
        """
        resolver = IntentResolver()
        candidates = _make_instagram_post_detail_nodes()

        result = resolver.resolve("tap send post button", candidates, screen_height=2400)

        assert result is not None, "Resolver returned None for 'tap send post button'"
        assert (
            "row_feed_button_share" in (result.resource_id or "").lower()
        ), f"Expected share button, got: {result.resource_id} (desc: {result.content_desc})"
        assert (
            "comment_composer" not in (result.resource_id or "").lower()
        ), "REGRESSION: Resolver picked comment_composer instead of share button!"


class TestTelepathicEngineKeyboardGuard:
    def test_telepathic_engine_auto_dismisses_keyboard(self):
        """
        When the keyboard is detected in find_best_node for a non-typing intent,
        the TelepathicEngine must actively call device.back() and re-fetch the XML
        via device.dump_hierarchy().
        """
        from GramAddict.core.telepathic_engine import TelepathicEngine

        class DummyDeviceForKeyboardTest:
            def __init__(self):
                self.back_called = False
                self.dump_hierarchy_called = False
                self.xml_without_keyboard = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
                <hierarchy rotation="0">
                    <node package="com.instagram.android" class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
                        <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" bounds="[100,400][400,440]" clickable="true" />
                    </node>
                </hierarchy>
                """

            def back(self):
                self.back_called = True

            def dump_hierarchy(self, compressed=False):
                self.dump_hierarchy_called = True
                return self.xml_without_keyboard

        mock_device = DummyDeviceForKeyboardTest()

        xml_with_keyboard = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
        <hierarchy rotation="0">
            <node package="com.instagram.android" class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
                <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" bounds="[100,400][400,440]" clickable="true" />
            </node>
            <node package="com.google.android.inputmethod.latin" class="android.widget.FrameLayout" bounds="[0,1500][1080,2400]">
                <node resource-id="com.google.android.inputmethod.latin:id/B00" content-desc="Q" bounds="[0,1800][100,1900]" clickable="true" />
            </node>
        </hierarchy>
        """

        engine = TelepathicEngine()
        
        # We pass "tap post username" which is NOT a typing intent.
        # It should trigger active dismissal.
        result = engine.find_best_node(
            xml_string=xml_with_keyboard,
            intent_description="tap post username",
            device=mock_device,
            track=False
        )

        # Ensure device.back() was called
        assert mock_device.back_called is True
        # Ensure device.dump_hierarchy() was called to re-fetch
        assert mock_device.dump_hierarchy_called is True
        
        # Result should still resolve to the profile name node
        assert result is not None
        assert "row_feed_photo_profile_name" in result.get("id", "")
