"""
Test Suite: Real XML Fixture Validation
========================================
These tests use REAL UIAutomator XML dumps captured from a live Instagram
session on the device. No hand-crafted node arrays — the full XML goes
through _extract_semantic_nodes() exactly like in production.

This catches bugs that mock-based tests miss:
- Parser skipping nodes due to missing attribs
- Parser including non-interactive system UI elements
- Safety guard false positives on real Instagram layouts
- Ad detection on real ad XML structures
"""

import json
import os
import re
from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.telepathic_engine import TelepathicEngine

FIXTURE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")


def load_fixture(name: str) -> str:
    """Load a real XML capture from tests/fixtures/"""
    path = os.path.join(FIXTURE_DIR, name)
    with open(path, "r") as f:
        return f.read()


class TestNodeExtraction:
    """
    Tests that _extract_semantic_nodes correctly parses REAL Instagram XML.
    Uses captured XML dumps from the actual device.
    """

    def test_home_feed_extracts_like_button(self):
        """
        In a real Home Feed dump, the parser MUST find the Like button node
        with resource-id 'row_feed_button_like'.
        """
        engine = TelepathicEngine()
        xml = load_fixture("home_feed_with_ad.xml")

        # Test raw extraction (backward compatibility)
        nodes = engine._extract_semantic_nodes(xml)

        like_nodes = [n for n in nodes if "row feed button like" in n["semantic_string"]]
        # The real dump has an organic post AND an ad post, both with like buttons
        assert len(like_nodes) > 0, (
            "Like buttons must be extracted despite clickable=false, because our "
            "new TelepathicEngine extraction heuristic includes highly semantic buttons."
        )

        # Instead, look for the parent Add to Saved button (which IS clickable)
        save_nodes = [n for n in nodes if "row feed button save" in n["semantic_string"]]
        assert len(save_nodes) >= 1, (
            f"Expected to find 'Add to Saved' button in real home feed XML. "
            f"Found nodes: {[n['semantic_string'][:60] for n in nodes]}"
        )

    def test_home_feed_extracts_tab_bar(self):
        """
        The parser must find the bottom tab bar items (Home, Reels, Search, Profile).
        These are critical for navigation.
        """
        engine = TelepathicEngine()
        xml = load_fixture("home_feed_with_ad.xml")
        nodes = engine._extract_semantic_nodes(xml)

        tab_descriptions = [n["semantic_string"] for n in nodes if "tab" in n["semantic_string"].lower()]
        assert any("Home" in t for t in tab_descriptions), "Must find Home tab"
        assert any("Search and explore" in t for t in tab_descriptions), "Must find Search tab"
        assert any("Profile" in t for t in tab_descriptions), "Must find Profile tab"

    def test_home_feed_node_count_is_realistic(self):
        """
        A real Instagram home feed XML produces 20-40 interactive nodes.
        If we get <10 or >100, the parser is broken.
        """
        engine = TelepathicEngine()
        xml = load_fixture("home_feed_with_ad.xml")
        nodes = engine._extract_semantic_nodes(xml)

        assert 10 <= len(nodes) <= 100, (
            f"Expected 10-100 interactive nodes from real XML, got {len(nodes)}. "
            f"Parser may be too strict or too lenient."
        )

    def test_home_feed_extracts_post_author_and_content(self):
        """
        In a real Home Feed dump, we MUST confidently extract the author node
        and the content node without hitting VLM, using our struct-aliases.
        """
        engine = TelepathicEngine()
        xml = load_fixture("home_feed_with_ad.xml")

        # Test exact strings from feed_analysis.py & timing.py
        author_node1 = engine.find_best_node(xml, "post author username header", min_confidence=0.35)
        author_node2 = engine.find_best_node(xml, "post author header profile", min_confidence=0.35)
        content_node = engine.find_best_node(xml, "post media content", min_confidence=0.35)

        assert author_node1 is not None, "Failed to find 'post author username header'"
        assert author_node2 is not None, "Failed to find 'post author header profile'"
        assert content_node is not None, "Failed to find 'post media content'"

        # Should be resolved by fast path -> score >= 0.75
        assert author_node1.get("score", 0) >= 0.75, "Author extraction fell out of Fast Path!"
        assert content_node.get("score", 0) >= 0.75, "Content extraction fell out of Fast Path!"

    def test_explore_feed_extracts_like_button(self):
        """
        In the real Explore/Reels feed, the Like button has id 'like_button'
        and description 'Like'. The parser must find it.
        """
        engine = TelepathicEngine()
        xml = load_fixture("explore_feed_reel.xml")
        nodes = engine._extract_semantic_nodes(xml)

        like_nodes = [
            n for n in nodes if "like" in n["semantic_string"].lower() and "button" in n["semantic_string"].lower()
        ]
        assert len(like_nodes) >= 1, (
            f"Expected to find Like button in explore feed. " f"Found: {[n['semantic_string'][:60] for n in nodes]}"
        )

    def test_explore_feed_has_fullscreen_containers(self):
        """
        Verify that the parser extracts the fullscreen containers
        (swipeable_nav_view_pager_inner_recycler_view, clips_viewer_view_pager)
        so that the Safety Guard has something to reject.
        """
        engine = TelepathicEngine()
        xml = load_fixture("explore_feed_reel.xml")
        nodes = engine._extract_semantic_nodes(xml)

        fullscreen = []
        for n in nodes:
            m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", n["raw_bounds"])
            if m:
                l, t, r, b = map(int, m.groups())
                if (r - l) > 900 and (b - t) > 1600:
                    fullscreen.append(n)

        assert len(fullscreen) >= 2, (
            f"Expected at least 2 fullscreen containers in explore XML "
            f"(swipeable pager + recycler view). Got {len(fullscreen)}."
        )


class TestSafetyGuard:
    """
    Tests the VLM Safety Guard against nodes extracted from REAL XML.
    Ensures that if a hallucinating VLM picks a fullscreen container,
    the guard catches it.
    """

    @pytest.fixture(autouse=True)
    def setup_real_nodes(self):
        """Pre-parse real XML nodes BEFORE any mocking happens."""
        engine = TelepathicEngine()
        explore_xml = load_fixture("explore_feed_reel.xml")
        self.explore_nodes = engine._extract_semantic_nodes(explore_xml)

    @patch("builtins.open", new_callable=MagicMock)
    @patch("GramAddict.core.telepathic_engine.query_telepathic_llm")
    @patch("os.path.exists")
    def test_real_explore_fullscreen_container_rejected(self, mock_exists, mock_query, mock_open):
        """
        Feed real explore XML nodes to the VLM fallback.
        Force VLM to pick the fullscreen swipeable_nav_view_pager.
        The safety guard MUST reject it.
        """
        mock_exists.return_value = False
        engine = TelepathicEngine()
        device = MagicMock()
        device.screenshot = MagicMock()
        mock_open.return_value.__enter__.return_value.read.return_value = b"fakeimage"

        nodes = self.explore_nodes

        # Find any fullscreen structural container
        container_idx = None
        for i, n in enumerate(nodes):
            m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", n["raw_bounds"])
            if m:
                l, t, r, b = map(int, m.groups())
                if (r - l) > 900 and (b - t) > 1600:
                    # It's not a media container (no vid/clip keyword)
                    sem = n["semantic_string"].lower()
                    if not any(k in sem for k in ["vid", "img", "image", "clip", "reel"]):
                        container_idx = i
                        break

        assert container_idx is not None, (
            f"Test setup: couldn't find a non-media fullscreen container. "
            f"Nodes: {[n['semantic_string'][:60] for n in nodes]}"
        )

        mock_query.return_value = json.dumps({"index": container_idx, "reason": "This looks like the like button"})
        result = engine._vision_cortex_fallback("tap like button", nodes, device)

        assert result is None, (
            f"CRITICAL: Safety guard failed on REAL XML! "
            f"Accepted node {container_idx}: {nodes[container_idx]['semantic_string']}"
        )

    @patch("builtins.open", new_callable=MagicMock)
    @patch("GramAddict.core.telepathic_engine.query_telepathic_llm")
    @patch("os.path.exists")
    def test_real_explore_like_button_accepted(self, mock_exists, mock_query, mock_open):
        """
        Feed real explore XML nodes.
        Force VLM to pick the actual like button.
        The safety guard MUST allow it through.
        """
        mock_exists.return_value = False
        engine = TelepathicEngine()
        device = MagicMock()
        device.screenshot = MagicMock()
        mock_open.return_value.__enter__.return_value.read.return_value = b"fakeimage"

        nodes = self.explore_nodes

        # Find the like button
        like_idx = None
        for i, n in enumerate(nodes):
            if "like" in n["semantic_string"].lower() and "button" in n["semantic_string"].lower():
                # Ensure it's a small button, not a container
                m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", n["raw_bounds"])
                if m:
                    l, t, r, b = map(int, m.groups())
                    if (r - l) < 200 and (b - t) < 200:
                        like_idx = i
                        break

        assert like_idx is not None, (
            f"Test setup: couldn't find Like button in real explore XML. "
            f"Nodes: {[n['semantic_string'][:60] for n in nodes]}"
        )

        mock_query.return_value = json.dumps({"index": like_idx, "reason": "It literally says Like"})
        result = engine._vision_cortex_fallback("tap like button", nodes, device)

        assert result is not None, "The real Like button (116x116) must be accepted"
        assert nodes[like_idx]["x"] == result["x"]
        assert nodes[like_idx]["y"] == result["y"]


class TestAdDetection:
    """
    Tests ad detection against REAL XML captured from the device.
    The dump.xml actually contains a real Instagram ad (hoeltlfinanzgmbh).
    """

    def test_real_explore_feed_is_not_ad(self):
        """
        The explore_feed.xml is a real Reel without any ad markers.
        It should NOT be flagged.
        """
        from GramAddict.core.utils import is_ad

        xml = load_fixture("explore_feed_reel.xml")
        assert is_ad(xml) is False, "Real explore/reel content was falsely flagged as an ad!"


class TestFeedMarkers:
    """
    Tests feed marker detection against REAL XML.
    """

    def test_real_home_feed_has_markers(self):
        """The real home feed XML must match our feed markers."""
        from GramAddict.core.bot_flow import FEED_MARKERS

        xml = load_fixture("home_feed_with_ad.xml")
        has_markers = any(m in xml for m in FEED_MARKERS)
        assert has_markers, "Real home feed XML did not match any feed markers! " f"Markers: {FEED_MARKERS}"

    def test_real_explore_feed_has_markers(self):
        """The real explore feed XML must match our feed markers."""
        from GramAddict.core.bot_flow import FEED_MARKERS

        xml = load_fixture("explore_feed_reel.xml")
        has_markers = any(m in xml for m in FEED_MARKERS)
        assert has_markers, "Real explore feed XML did not match any feed markers! " f"Markers: {FEED_MARKERS}"


class TestTelepathicResolutionCascade:
    """
    Tests that the Telepathic Engine's resolution cascade strictly enforces
    cheapest-first processing (Fast Path -> Embeddings -> VLM) to avoid
    token overkill and API spam. Uses real XML dumps.
    """

    @patch("GramAddict.core.telepathic_engine.query_telepathic_llm")
    @patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding")
    def test_keyword_fast_path_bypasses_ai(self, mock_get_embedding, mock_vlm):
        """
        A direct keyword match (like 'tap like button') MUST be resolved by Stage 1.5.
        It must never reach the Embedding (Stage 2) or VLM (Stage 3).
        """
        from GramAddict.core.telepathic_engine import TelepathicEngine

        engine = TelepathicEngine()
        engine._embedding_cache.clear()
        engine._intent_cache.clear()

        # home_feed_with_ad.xml contains standard UI elements
        xml_content = load_fixture("home_feed_with_ad.xml")

        result = engine.find_best_node(xml_content, "tap comment button")

        assert result is not None, "Failed to find the node."
        assert "comment" in result["semantic"].lower(), "Did not find comment button."
        assert result.get("score", 0) >= 0.4, "Fast path score ratio should be valid"

        # PROOF: Zero AI tokens used!
        mock_get_embedding.assert_not_called()
        mock_vlm.assert_not_called()

    @patch("GramAddict.core.telepathic_engine.query_telepathic_llm")
    @patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding")
    def test_embedding_fallback_bypasses_vlm_if_confident(self, mock_get_embedding, mock_vlm):
        """
        If we ask something without an exact keyword match, it should fail Stage 1.5,
        hit Stage 2 (Embeddings), and if confident enough, avoid Stage 3 (VLM).
        """
        from GramAddict.core.telepathic_engine import TelepathicEngine

        engine = TelepathicEngine()
        engine._embedding_cache.clear()
        engine._intent_cache.clear()

        xml_content = load_fixture("home_feed_with_ad.xml")

        def fake_embed(text):
            if "heart" in text.lower():
                return [1.0, 0.0]  # Intent vector
            if "like" in text.lower() and "button" in text.lower():
                return [0.99, 0.1]  # Very similar to intent
            return [0.0, 1.0]  # Completely different for all other UI nodes

        mock_get_embedding.side_effect = fake_embed

        # "heart" is not mechanically in the keyword of the Like button,
        # causing Keyword Path to fail. Vector Similarity will know Heart == Like.
        result = engine.find_best_node(xml_content, "tap the heart symbol")

        assert result is not None, "Failed to find the node through embeddings."
        assert "like" in result["semantic"].lower(), "Embeddings didn't pick the like button."
        assert result.get("score", 0) >= 0.82, "Confidence threshold should be met"

        # PROOF: Embeddings matched it, VLM was NOT used
        assert mock_get_embedding.call_count > 0, "Embeddings were not called"
        mock_vlm.assert_not_called()

    @patch("GramAddict.core.telepathic_engine.query_telepathic_llm")
    @patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding")
    def test_vlm_fallback_triggered_on_low_confidence(self, mock_get_embedding, mock_vlm):
        """
        If Embeddings fail to find a confident match (< 0.82), it must trigger
        the Stage 3 VLM fallback.
        """
        from GramAddict.core.telepathic_engine import TelepathicEngine

        engine = TelepathicEngine()
        engine._embedding_cache.clear()
        engine._intent_cache.clear()

        xml_content = load_fixture("home_feed_with_ad.xml")

        def fake_embed(text):
            if "mystical" in text:
                return [1.0, 0.0]
            return [0.0, 1.0]

        mock_get_embedding.side_effect = fake_embed
        mock_vlm.return_value = '{"index": 2, "reason": "Fallback chosen by VLM"}'

        # A mockup device is needed for VLM fallback
        import unittest.mock

        device = unittest.mock.MagicMock()

        engine.find_best_node(xml_content, "tap the mystical artifact", device=device)

        # It might return a result (from VLM) or None depending on the XML structure,
        # but we ONLY care that VLM was indeed queried!
        mock_get_embedding.assert_called()
        mock_vlm.assert_called_once()
