from unittest.mock import MagicMock, patch

from GramAddict.core.situational_awareness import EscapeAction, SituationalAwarenessEngine, SituationEpisodeDB


class TestSAETeslaUpgrade:
    def setup_method(self):
        self.device_mock = MagicMock()
        self.sae = SituationalAwarenessEngine(self.device_mock)

    def test_sae_foreground_extraction(self):
        """
        Ensures that modals/popups located at the END of the XML document
        (highest Z-index) are prioritized in the compressed signature.
        The old system truncated at elements[:50], missing the actual popup.
        """
        # Create an XML with 60 background nodes, and 1 dialog node at the end
        xml_dump = "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>\n<hierarchy rotation='0'>\n"
        for i in range(60):
            xml_dump += f'  <node package="com.instagram.android" resource-id="id/feed_item_{i}" text="Feed {i}" bounds="[0,0][100,100]" />\n'

        # The critical modal at the end
        xml_dump += '  <node package="com.instagram.android" resource-id="id/dialog_container" text="Not Now" bounds="[100,100][200,200]" />\n'
        xml_dump += "</hierarchy>"

        compressed = self.sae._compress_xml(xml_dump)

        # The compressed string MUST contain the dialog container
        assert "dialog_container" in compressed, "Foreground extraction failed: modal was truncated!"
        assert "Not Now" in compressed, "Foreground extraction failed: modal text was truncated!"

        # It should prioritize the END of the document, so feed_item_0 should ideally be gone if capped at 50
        assert "feed_item_0" not in compressed, "Background elements are still being prioritized over foreground!"

    def test_sae_structural_generalization(self):
        """
        Ensures that dynamic user content is stripped to allow cross-post modal generalization,
        while short, static UI text (like "OK", "Cancel") is preserved.
        """
        xml_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
        <hierarchy rotation='0'>
            <node package="com.instagram.android" resource-id="id/comment" text="This is a very long user comment that changes every time we see this modal so it should be stripped!" />
            <node package="com.instagram.android" resource-id="id/button" text="OK" />
        </hierarchy>
        """

        compressed = self.sae._compress_xml(xml_dump)

        # Long dynamic text should be stripped or truncated to not pollute the vector space
        assert "This is a very long user comment" not in compressed, "Dynamic text > 20 chars was not stripped!"
        assert "text='<STRIPPED_DYNAMIC>'" in compressed or "This is a very lo" not in compressed
        # Short static text should be kept
        assert "OK" in compressed, "Short static UI text was incorrectly stripped!"

    @patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new=True)
    def test_sae_negative_reinforcement(self):
        """
        Ensures that failed escapes decay the confidence of the vector,
        and eventually purge it, instead of just storing a useless 0.0 vector alongside it.
        """
        db = SituationEpisodeDB()

        # We need to mock db._db.client directly since it's an instance property
        mock_client = MagicMock()
        db._db._client = mock_client
        db._db.client = mock_client

        with patch.object(db._db, "upsert_point"):
            # Mock retrieve to return an existing point with confidence 0.4
            mock_payload = {"confidence": 0.4, "action": {"action_type": "back", "x": 0, "y": 0, "reason": ""}}
            mock_client.retrieve.return_value = [MagicMock(payload=mock_payload)]

            # Simulate a FAILURE
            action = EscapeAction("back")
            db.learn("some_signature", action, success=False)

            # Verify that it fetched the current confidence and updated it, or deleted it if < 0.1
            # If confidence was 0.4 and delta is -0.5, it drops to -0.1 -> DELETED
            mock_client.delete.assert_called_once()
