"""
Chaos Engineering: XML Corruption Resilience Tests for TelepathicEngine + SAE.

Verifies that NEITHER engine crashes on any form of corrupted, truncated,
adversarial, or garbage XML input. They must degrade gracefully (return None
or empty lists) without raising unhandled exceptions.

These tests are the "crash barrier" of autonomous navigation — ensuring that
no matter what Android dumps to us, the bot survives and recovers.
"""

import time
from unittest.mock import MagicMock, patch

import pytest

from tests.chaos import generate_corrupted_xml

# ──────────────────────────────────────────────────
# Telepathic Engine Chaos Tests
# ──────────────────────────────────────────────────


@pytest.fixture
def telepathic_engine():
    """Creates a real TelepathicEngine instance with mocked Qdrant."""
    with (
        patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None),
        patch(
            "GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=lambda: property(lambda self: False)
        ),
    ):
        from GramAddict.core.telepathic_engine import TelepathicEngine

        TelepathicEngine._instance = None
        engine = TelepathicEngine.__new__(TelepathicEngine)
        engine.__init__()

        # We need to mock the Qdrant connection in the ActionMemory submodule
        engine._memory.ui_memory = MagicMock()
        engine._memory.ui_memory.is_connected = False
        engine._memory.ui_memory.query_closest = MagicMock(return_value=None)

        # We mock positive memory for chaos tests
        engine.positive_memory = MagicMock()
        engine.positive_memory.is_connected = False
        engine.positive_memory.recall = MagicMock(return_value=None)
        yield engine
        TelepathicEngine._instance = None


ALL_CORRUPTION_TYPES = [
    "EMPTY_STRING",
    "NONE_VALUE",
    "TRUNCATED_MID_TAG",
    "UNICODE_INJECTION",
    "MASSIVE_DOM_10K_NODES",
    "ZERO_SIZE_BOUNDS",
    "NEGATIVE_COORDINATES",
    "MISSING_CLOSING_TAGS",
    "RECURSIVE_NESTING_500_DEEP",
    "NULL_BYTES",
    "MALFORMED_BOUNDS",
    "ONLY_WHITESPACE",
    "HTML_NOT_XML",
    "BINARY_GARBAGE",
    "EXTREMELY_LONG_TEXT",
]


@pytest.mark.chaos
class TestTelepathicEngineChaos:
    """Telepathic Engine must NEVER crash on corrupted XML."""

    @pytest.mark.parametrize("corruption_type", ALL_CORRUPTION_TYPES)
    def test_extract_semantic_nodes_survives(self, telepathic_engine, corruption_type):
        """Engine's XML parser must return empty list on any corruption."""
        xml = generate_corrupted_xml(corruption_type)

        # Must NOT raise. May return empty list.
        if xml is None:
            # None input — directly test defense
            result = telepathic_engine._extract_semantic_nodes("")
        else:
            result = telepathic_engine._extract_semantic_nodes(xml)

        assert isinstance(result, list)

    @pytest.mark.parametrize(
        "corruption_type",
        [
            "EMPTY_STRING",
            "NONE_VALUE",
            "TRUNCATED_MID_TAG",
            "MISSING_CLOSING_TAGS",
            "ONLY_WHITESPACE",
            "HTML_NOT_XML",
            "BINARY_GARBAGE",
        ],
    )
    def test_find_best_node_survives_garbage(self, telepathic_engine, corruption_type):
        """find_best_node must return None on garbage XML, never crash."""
        xml = generate_corrupted_xml(corruption_type)
        if xml is None:
            xml = ""

        result = telepathic_engine._find_best_node_inner(xml, "tap like button", min_confidence=0.82)
        # Must be None or a dict, never an exception
        assert result is None or isinstance(result, dict)

    def test_unicode_injection_doesnt_corrupt_semantics(self, telepathic_engine):
        """Zalgo text in nodes shouldn't crash semantic extraction."""
        xml = generate_corrupted_xml("UNICODE_INJECTION")
        nodes = telepathic_engine._extract_semantic_nodes(xml)
        # Should extract SOME nodes (the XML structure is valid)
        assert isinstance(nodes, list)
        # If nodes found, they should have valid coordinates
        for node in nodes:
            assert isinstance(node.get("x", 0), int)
            assert isinstance(node.get("y", 0), int)

    def test_massive_dom_doesnt_hang(self, telepathic_engine):
        """10K nodes must be parsed within 5 seconds — no infinite loops."""
        xml = generate_corrupted_xml("MASSIVE_DOM_10K_NODES")
        start = time.time()
        nodes = telepathic_engine._extract_semantic_nodes(xml)
        elapsed = time.time() - start

        assert elapsed < 5.0, f"Parsing 10K nodes took {elapsed:.2f}s (limit: 5s)"
        assert isinstance(nodes, list)

    def test_deep_nesting_doesnt_stackoverflow(self, telepathic_engine):
        """500 levels of nesting must not cause stack overflow."""
        xml = generate_corrupted_xml("RECURSIVE_NESTING_500_DEEP")
        # This would crash Python's default recursion limit (1000) if
        # we used recursive parsing. ElementTree uses iterative parsing,
        # so it should survive.
        nodes = telepathic_engine._extract_semantic_nodes(xml)
        assert isinstance(nodes, list)

    def test_null_bytes_stripped(self, telepathic_engine):
        """Null bytes in text content must not cause parsing failures."""
        xml = generate_corrupted_xml("NULL_BYTES")
        nodes = telepathic_engine._extract_semantic_nodes(xml)
        assert isinstance(nodes, list)
        # Verify no null bytes leaked into node semantics
        for node in nodes:
            assert "\x00" not in node.get("semantic_string", "")


# ──────────────────────────────────────────────────
# SAE (Situational Awareness Engine) Chaos Tests
# ──────────────────────────────────────────────────


@pytest.fixture
def sae_engine():
    """Creates a SAE instance with mocked device."""
    from GramAddict.core.situational_awareness import SituationalAwarenessEngine

    SituationalAwarenessEngine.reset()

    device = MagicMock()
    device.app_id = "com.instagram.android"
    device.deviceV2 = MagicMock()
    device.deviceV2.info = {"screenOn": True}

    engine = SituationalAwarenessEngine(device)

    # Mock the episode DB to avoid Qdrant dependency
    engine.episodes = MagicMock()
    engine.episodes.recall = MagicMock(return_value=None)
    engine.episodes.learn = MagicMock()

    yield engine
    SituationalAwarenessEngine.reset()


@pytest.mark.chaos
class TestSAEChaos:
    """SAE perception must be bulletproof against XML corruption."""

    @pytest.mark.parametrize(
        "corruption_type",
        [
            "EMPTY_STRING",
            "TRUNCATED_MID_TAG",
            "MISSING_CLOSING_TAGS",
            "ONLY_WHITESPACE",
            "HTML_NOT_XML",
            "BINARY_GARBAGE",
        ],
    )
    def test_compress_xml_survives_garbage(self, sae_engine, corruption_type):
        """XML compression must never crash, even on garbage."""
        xml = generate_corrupted_xml(corruption_type)
        if xml is None:
            xml = ""

        result = sae_engine._compress_xml(xml)
        assert isinstance(result, str)
        assert len(result) > 0  # Should always return something

    def test_compress_empty_returns_marker(self, sae_engine):
        """Empty/None input must return 'EMPTY_SCREEN' sentinel."""
        assert sae_engine._compress_xml("") == "EMPTY_SCREEN"
        assert sae_engine._compress_xml(None) == "EMPTY_SCREEN"

    @pytest.mark.parametrize(
        "corruption_type",
        [
            "EMPTY_STRING",
            "TRUNCATED_MID_TAG",
            "BINARY_GARBAGE",
            "ONLY_WHITESPACE",
        ],
    )
    def test_perceive_survives_garbage(self, sae_engine, corruption_type):
        """perceive() must return a valid SituationType on any input."""
        from GramAddict.core.situational_awareness import SituationType

        xml = generate_corrupted_xml(corruption_type)
        if xml is None:
            xml = ""

        result = sae_engine.perceive(xml)
        assert isinstance(result, SituationType)

    def test_compute_situation_hash_is_deterministic(self, sae_engine):
        """Same XML must always produce the same hash."""
        xml = generate_corrupted_xml("UNICODE_INJECTION")
        compressed = sae_engine._compress_xml(xml)
        hash1 = sae_engine._compute_situation_hash(compressed)
        hash2 = sae_engine._compute_situation_hash(compressed)
        assert hash1 == hash2

    def test_massive_dom_compression_is_bounded(self, sae_engine):
        """10K nodes must be compressed to < 3000 chars (the cap)."""
        xml = generate_corrupted_xml("MASSIVE_DOM_10K_NODES")
        start = time.time()
        result = sae_engine._compress_xml(xml)
        elapsed = time.time() - start

        assert len(result) <= 3000, f"Compressed output is {len(result)} chars (limit: 3000)"
        assert elapsed < 5.0, f"Compression took {elapsed:.2f}s"
