"""
🔴 RED: Zero-Maintenance Compliance for SituationalAwarenessEngine

These tests enforce that the SAE contains ZERO hardcoded UI element identifiers
and ZERO hardcoded model/URL defaults. The bot must:

1. Detect obstacles via autonomous LLM+Qdrant pipeline — NOT via hardcoded resource-ids or text patterns.
2. Pull ALL model names/URLs from Config() — NO fallback literals scattered across the codebase.
3. Rely on structural package checks (app-agnostic) and ScreenIdentity delegation — NOT marker tuples.

Violating any of these rules means the bot breaks when Instagram updates its UI.
That is the opposite of Zero Maintenance.
"""

import inspect
import re

# ══════════════════════════════════════════════════════════
# 1. NO HARDCODED INSTAGRAM UI ELEMENT IDENTIFIERS IN SAE
# ══════════════════════════════════════════════════════════


class TestSAEContainsNoHardcodedUIElements:
    """
    The SAE perceive() must NOT contain any hardcoded Instagram resource-id
    fragments, button text patterns, or content-desc strings.
    ALL obstacle detection must flow through: package check → Qdrant cache → LLM fallback.
    """

    FORBIDDEN_RESOURCE_ID_FRAGMENTS = [
        "survey_overlay",
        "survey_title",
        "interstitial_container",
        "mystery_interstitial",
        "nux_overlay",
        "rating_prompt",
        "feedback_dialog",
        "action_bar_browser",
        "browser_action_bar",
        "quick_capture",
        "gallery_cancel_button",
        "creation_flow",
        "reel_camera",
    ]

    FORBIDDEN_BUTTON_TEXT_PATTERNS = [
        "Not Now",
        "not now",
        "Nicht jetzt",
        "Take Survey",
        "Bewerten",
        "rate \\d+ stars",
    ]

    FORBIDDEN_CONTENT_DESC_PATTERNS = [
        "Close browser",
    ]

    def _get_perceive_source(self):
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine

        return inspect.getsource(SituationalAwarenessEngine.perceive)

    def test_no_hardcoded_resource_id_markers_in_perceive(self):
        """perceive() must not contain hardcoded Instagram resource-id fragments."""
        source = self._get_perceive_source()
        for fragment in self.FORBIDDEN_RESOURCE_ID_FRAGMENTS:
            assert fragment not in source, (
                f"SAE.perceive() contains hardcoded resource-id fragment '{fragment}'! "
                f"This must be detected autonomously via LLM+Qdrant, not hardcoded."
            )

    def test_no_hardcoded_button_text_patterns_in_perceive(self):
        """perceive() must not contain hardcoded dismiss button text patterns."""
        source = self._get_perceive_source()
        for pattern in self.FORBIDDEN_BUTTON_TEXT_PATTERNS:
            assert pattern not in source, (
                f"SAE.perceive() contains hardcoded button text pattern '{pattern}'! "
                f"Modal detection must be autonomous, not text-matching."
            )

    def test_no_hardcoded_content_desc_in_perceive(self):
        """perceive() must not contain hardcoded content-desc strings."""
        source = self._get_perceive_source()
        for pattern in self.FORBIDDEN_CONTENT_DESC_PATTERNS:
            assert pattern not in source, (
                f"SAE.perceive() contains hardcoded content-desc '{pattern}'! " f"Must be discovered autonomously."
            )

    def test_no_marker_tuples_in_perceive(self):
        """perceive() must not define any '_markers' or '_patterns' tuples."""
        source = self._get_perceive_source()
        marker_defs = re.findall(r"\b\w+_markers\s*=\s*\(", source)
        pattern_defs = re.findall(r"\b\w+_patterns\s*=\s*\(", source)
        all_defs = marker_defs + pattern_defs
        assert len(all_defs) == 0, (
            f"SAE.perceive() defines hardcoded marker/pattern tuples: {all_defs}. "
            f"All obstacle detection must be autonomous."
        )


# ══════════════════════════════════════════════════════════
# 2. NO HARDCODED MODEL NAMES / URLS IN SAE
# ══════════════════════════════════════════════════════════


class TestSAEContainsNoHardcodedModelDefaults:
    """
    The SAE must never contain hardcoded model names or URLs as fallback strings.
    ALL model config must come from Config() — the single source of truth.
    A naked `except: model = "llava:latest"` is a maintenance bomb.
    """

    FORBIDDEN_MODEL_LITERALS = [
        "llava:latest",
        "qwen3.5:latest",
        "llava",
    ]

    FORBIDDEN_URL_LITERALS = [
        "localhost:11434",
        "http://localhost:11434/api/generate",
    ]

    def _get_sae_source(self):
        from GramAddict.core.situational_awareness import SituationalAwarenessEngine

        return inspect.getsource(SituationalAwarenessEngine)

    def test_no_hardcoded_model_names(self):
        """SAE must not contain any hardcoded model name strings."""
        source = self._get_sae_source()
        for literal in self.FORBIDDEN_MODEL_LITERALS:
            # Only check actual string literals (quoted), not comments
            pattern = rf"""['"]({re.escape(literal)})['"]"""
            matches = re.findall(pattern, source)
            assert len(matches) == 0, (
                f"SAE contains hardcoded model name '{literal}' ({len(matches)} occurrence(s)). "
                f"Model config must come exclusively from Config()."
            )

    def test_no_hardcoded_urls(self):
        """SAE must not contain any hardcoded Ollama/API URLs."""
        source = self._get_sae_source()
        for literal in self.FORBIDDEN_URL_LITERALS:
            pattern = rf"""['"]([^'"]*{re.escape(literal)}[^'"]*)['"]"""
            matches = re.findall(pattern, source)
            assert len(matches) == 0, (
                f"SAE contains hardcoded URL '{literal}' ({len(matches)} occurrence(s)). "
                f"All URLs must come from Config()."
            )

    def test_no_naked_except_with_model_defaults(self):
        """SAE must not have except blocks that hardcode model fallbacks."""
        source = self._get_sae_source()
        # Pattern: `except` followed within 5 lines by a model/url assignment
        except_blocks = re.findall(
            r"except.*?:\s*\n(?:.*\n){0,5}.*(?:model|url)\s*=\s*['\"]",
            source,
        )
        assert len(except_blocks) == 0, (
            f"SAE has {len(except_blocks)} except block(s) with hardcoded model/URL fallbacks. "
            f"Config() must be the SSOT — if it fails, crash loudly (Fail Fast)."
        )


# ══════════════════════════════════════════════════════════
# 2b. NO HARDCODED MODEL/URL DEFAULTS IN ANY MODULE (codebase-wide)
# ══════════════════════════════════════════════════════════


class TestCodebaseContainsNoHardcodedModelDefaults:
    """
    CODEBASE-WIDE enforcement: No Python module in GramAddict/core/ may contain
    hardcoded model names or localhost URLs as getattr() default values.

    The ONLY file allowed to define these defaults is config.py (the SSOT).
    Every other module must read from Config().args WITHOUT providing a fallback literal.
    If Config().args is missing, the system must crash (Fail Fast) — not silently
    degrade to a potentially wrong model.
    """

    # These are the exact default-value patterns that are FORBIDDEN outside config.py.
    # They indicate a getattr(..., "ai_xxx", "HARDCODED_DEFAULT") anti-pattern.
    FORBIDDEN_DEFAULT_PATTERNS = [
        r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']http://localhost:11434',
        r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llava:',
        r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']qwen3\.',
        r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.',
        r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.2-vision',
        r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']nomic-embed-text',
    ]

    # Files that are ALLOWED to have these defaults (SSOT only)
    ALLOWED_FILES = {"config.py"}

    def _get_all_core_python_files(self):
        import os

        core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core")
        core_dir = os.path.normpath(core_dir)
        files = []
        for root, _dirs, filenames in os.walk(core_dir):
            for f in filenames:
                if f.endswith(".py") and f not in self.ALLOWED_FILES:
                    files.append(os.path.join(root, f))
        return files

    def test_no_getattr_with_hardcoded_model_defaults_outside_config(self):
        """
        No module except config.py may use getattr(args, 'ai_xxx', 'HARDCODED_DEFAULT').
        The correct pattern is: getattr(cfg.args, 'ai_xxx') — no default, crash if missing.
        """
        import os

        violations = []
        for filepath in self._get_all_core_python_files():
            with open(filepath) as f:
                content = f.read()
            for pattern in self.FORBIDDEN_DEFAULT_PATTERNS:
                matches = re.findall(pattern, content)
                if matches:
                    basename = os.path.basename(filepath)
                    violations.append(f"{basename}: {len(matches)}x pattern '{pattern[:60]}...'")

        assert len(violations) == 0, (
            f"Found {len(violations)} file(s) with hardcoded model/URL defaults outside config.py!\n"
            f"Config() is the SSOT. Remove default values from getattr() calls.\n"
            + "\n".join(f"  ❌ {v}" for v in violations)
        )

    def test_no_bare_localhost_url_string_literals_outside_config(self):
        """
        No module except config.py and llm_provider.py (routing logic) may contain
        'http://localhost:11434' as a bare string literal used as a fallback value.
        """
        import os

        allowed = {"config.py", "llm_provider.py"}
        violations = []
        core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core")
        core_dir = os.path.normpath(core_dir)

        for root, _dirs, filenames in os.walk(core_dir):
            for f in filenames:
                if f.endswith(".py") and f not in allowed:
                    path = os.path.join(root, f)
                    with open(path) as fh:
                        for i, line in enumerate(fh, 1):
                            if "localhost:11434" in line and not line.strip().startswith("#"):
                                violations.append(f"{f}:{i}: {line.strip()[:100]}")

        assert len(violations) == 0, (
            f"Found {len(violations)} hardcoded localhost:11434 references outside config.py/llm_provider.py!\n"
            + "\n".join(f"  ❌ {v}" for v in violations)
        )

# ══════════════════════════════════════════════════════════
# 3. GOAP SMART UI STABILIZATION (Poll instead of static sleep)
# ══════════════════════════════════════════════════════════


class TestGOAPSmartUIStabilization:
    """
    After clicking an element, GOAP must poll for UI changes instead of
    using a static sleep. This prevents false 'inconclusive' results on
    slow devices/networks where UI transitions take >2s.
    """

    def test_goap_polls_dump_hierarchy_multiple_times_after_click(self):
        """
        The GOAP _execute_action must call dump_hierarchy multiple times
        (polling loop) instead of a single post-click dump.
        We verify this by inspecting the source code for the poll pattern.
        """
        import inspect

        from GramAddict.core.goap import GoalExecutor

        source = inspect.getsource(GoalExecutor._execute_action)

        # Must have a polling loop with MAX_POLLS
        assert "MAX_POLLS" in source, "GOAP._execute_action must use a MAX_POLLS polling loop, not a static sleep."

        # Must NOT have the old static random.uniform sleep
        assert "random.uniform" not in source, (
            "GOAP._execute_action still uses random.uniform for static sleep! " "Must use smart UI polling instead."
        )

        # Must poll dump_hierarchy inside a loop
        assert "dump_hierarchy()" in source, "GOAP._execute_action must call dump_hierarchy() inside the poll loop."

    def test_goap_has_no_static_sleep_after_click(self):
        """The old pattern was: click → sleep(random) → single dump. This must be gone."""
        import inspect

        from GramAddict.core.goap import GoalExecutor

        source = inspect.getsource(GoalExecutor._execute_action)

        # The click line and the next significant operation must NOT be a raw sleep with random
        click_idx = source.index("self.device.click(")
        code_after_click = source[click_idx : click_idx + 500]

        assert "random.uniform" not in code_after_click, (
            "GOAP still uses random.uniform sleep after click! "
            "Replace with smart UI polling (poll dump_hierarchy until XML changes)."
        )
