"""
Chaos Engineering: Network & Dependency Failure Tests.

Verifies that the bot degrades gracefully when external services
(Qdrant, Ollama, OpenRouter) are unavailable, slow, or return errors.

Tesla's FSD doesn't crash if the map server is unreachable — neither should we.
"""

from unittest.mock import MagicMock, patch

import pytest

from tests.chaos import VALID_FEED_XML

# ──────────────────────────────────────────────────
# Qdrant Failure Tests
# ──────────────────────────────────────────────────


@pytest.mark.chaos
class TestQdrantFailure:
    """Bot must survive total Qdrant outage."""

    def test_telepathic_works_without_qdrant(self):
        """TelepathicEngine must still resolve nodes via keyword fast-path when Qdrant is down."""
        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__()
            engine._memory.ui_memory = MagicMock()
            engine._memory.ui_memory.is_connected = False
            engine._memory.ui_memory.query_closest = MagicMock(return_value=None)

            engine.positive_memory = MagicMock()
            engine.positive_memory.is_connected = False
            engine.positive_memory.recall = MagicMock(return_value=None)

            nodes = engine._extract_semantic_nodes(VALID_FEED_XML)
            # Should still find clickable nodes via structural parsing
            assert len(nodes) > 0
            TelepathicEngine._instance = None

    def test_sae_recall_returns_none_without_qdrant(self):
        """SAE episodic memory must return None (not crash) when Qdrant is down."""
        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.situational_awareness import SituationEpisodeDB

            db = SituationEpisodeDB()
            db._db = MagicMock()
            db._db.is_connected = False

            result = db.recall("test_situation_signature")
            assert result is None

    def test_sae_learn_silently_fails_without_qdrant(self):
        """SAE learning must silently skip (not crash) when Qdrant is down."""
        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.situational_awareness import EscapeAction, SituationEpisodeDB

            db = SituationEpisodeDB()
            db._db = MagicMock()
            db._db.is_connected = False

            action = EscapeAction("back", reason="test")
            # Must not raise
            db.learn("test_signature", action, True)

    def test_qdrant_timeout_doesnt_hang_extraction(self):
        """If Qdrant queries time out, node extraction must still complete."""
        import time

        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__()
            engine._memory.ui_memory = MagicMock()
            engine._memory.ui_memory.is_connected = False
            engine._memory.ui_memory.query_closest = MagicMock(side_effect=TimeoutError("Qdrant timeout"))
            engine.positive_memory = MagicMock()
            engine.positive_memory.is_connected = False
            engine.positive_memory.recall = MagicMock(side_effect=TimeoutError("Qdrant timeout"))

            start = time.time()
            nodes = engine._extract_semantic_nodes(VALID_FEED_XML)
            elapsed = time.time() - start

            assert elapsed < 5.0
            assert isinstance(nodes, list)
            TelepathicEngine._instance = None


# ──────────────────────────────────────────────────
# LLM (Ollama/OpenRouter) Failure Tests
# ──────────────────────────────────────────────────


@pytest.mark.chaos
class TestLLMFailure:
    """Bot must survive LLM outages."""

    def test_sae_perceive_defaults_to_normal_on_llm_failure(self):
        """If LLM classification fails, SAE must default to NORMAL (safe fallback)."""
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

        SituationalAwarenessEngine.reset()

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

        sae = SituationalAwarenessEngine(device)
        sae.episodes = MagicMock()
        sae.episodes.recall = MagicMock(return_value=None)

        with patch("GramAddict.core.qdrant_memory.ScreenMemoryDB") as MockScreenDB:
            mock_screen_db = MagicMock()
            mock_screen_db.get_screen_type = MagicMock(return_value=None)
            MockScreenDB.return_value = mock_screen_db

            with patch("GramAddict.core.llm_provider.query_telepathic_llm", side_effect=ConnectionError("Ollama down")):
                result = sae.perceive(VALID_FEED_XML)
                # Must default to NORMAL, not crash
                assert result == SituationType.NORMAL

        SituationalAwarenessEngine.reset()

    def test_sae_escape_planning_defaults_to_back_on_llm_failure(self):
        """If LLM escape planning fails, SAE must default to BACK press."""
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

        SituationalAwarenessEngine.reset()

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

        sae = SituationalAwarenessEngine(device)

        with patch("GramAddict.core.llm_provider.query_llm", side_effect=ConnectionError("LLM down")):
            action = sae._plan_escape_via_llm(VALID_FEED_XML, "compressed_sig", SituationType.OBSTACLE_MODAL)
            assert action.action_type == "back"
            assert "failed" in action.reason.lower() or "default" in action.reason.lower()

        SituationalAwarenessEngine.reset()


# ──────────────────────────────────────────────────
# Active Inference Resilience
# ──────────────────────────────────────────────────


@pytest.mark.chaos
class TestActiveInferenceChaos:
    """Active Inference engine must survive edge cases."""

    def test_evaluate_with_empty_history(self):
        """Evaluating without any predictions must return True (no-op)."""
        from GramAddict.core.active_inference import ActiveInferenceEngine

        ai = ActiveInferenceEngine("test_user")
        assert ai.evaluate_prediction("<hierarchy/>") is True

    def test_extreme_free_energy_doesnt_overflow(self):
        """Repeated errors must not cause float overflow."""
        from GramAddict.core.active_inference import ActiveInferenceEngine

        ai = ActiveInferenceEngine("test_user")

        for _ in range(1000):
            ai.predict_state(["nonexistent_element"])
            ai.evaluate_prediction("<hierarchy><node text='wrong'/></hierarchy>")

        assert ai.free_energy < float("inf")
        assert ai.free_energy >= 0

    def test_surprise_with_identical_prediction_is_zero(self):
        """Perfect prediction (predicted == observed) must produce near-zero surprise."""
        from GramAddict.core.active_inference import ActiveInferenceEngine

        ai = ActiveInferenceEngine("test_user")
        ai.free_energy = 0.0

        result = ai.calculate_surprise(1.0, 1.0)
        assert result < 0.1  # Near-zero free energy

    def test_sleep_modifier_bounds(self):
        """Sleep modifier must always be between 1.0 and 5.0."""
        from GramAddict.core.active_inference import ActiveInferenceEngine

        ai = ActiveInferenceEngine("test_user")

        for policy in ["STABLE", "CAUTIOUS", "DORMANT"]:
            ai.policy = policy
            mod = ai.get_sleep_modifier()
            assert 1.0 <= mod <= 5.0
