"""
TDD Tests: Following List Navigation Loop Prevention

These tests reproduce the infinite loop bug where the GOAP planner
repeatedly sends "open following list" as a synthetic intent,
the TelepathicEngine's VLM selects the wrong element (Profile Tab),
the StructuralGuard rejects it, and the cycle repeats 15 times.

Each test targets one of the 4 identified bugs.
"""

import os
from unittest.mock import MagicMock, patch

from GramAddict.core.goap import (
    GoalExecutor,
    GoalPlanner,
    ScreenIdentity,
    ScreenType,
)

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


def _load_fixture(name: str) -> str:
    path = os.path.join(FIXTURES_DIR, name)
    with open(path, "r") as f:
        return f.read()


# ─────────────────────────────────────────────────────
# Bug 1: _plan_navigation fall-through
# ─────────────────────────────────────────────────────


class TestPlanNavigationFallThrough:
    """The planner must NOT return the same failed synthetic intent forever."""

    def test_plan_navigation_stops_after_explored_failure(self):
        """
        When a synthetic intent has been explored (added to explored_nav_actions)
        and failed, _plan_navigation must return None — NOT the same goal again.
        This prevents the infinite retry loop.
        """
        planner = GoalPlanner("test_user")
        # Ensure no learned requirements exist (Blank Start)
        planner.knowledge.get_requirements = MagicMock(return_value=[])

        goal = "open following list"
        screen = {
            "screen_type": ScreenType.OWN_PROFILE,
            "available_actions": ["tap home tab", "press back", "tap profile tab"],
            "context": {},
        }

        # First call: no explored actions → should return the goal for discovery
        action1 = planner.plan_next_step(goal, screen, explored_nav_actions=set())
        assert action1 == goal, "First attempt should return the goal for autonomous discovery"

        # Second call: goal was explored and failed → must NOT return the same goal
        explored = {goal}  # The goal itself was tried as an action and failed
        action2 = planner.plan_next_step(goal, screen, explored_nav_actions=explored)

        assert action2 != goal, (
            f"Planner returned the SAME failed intent '{goal}' again! "
            f"This causes an infinite loop. Expected None or a fallback action."
        )
        # It should either return None (goal achieved/impossible) or a fallback like 'press back'
        assert action2 is None or action2 == "press back", f"Expected None or 'press back' fallback, got: {action2}"


# ─────────────────────────────────────────────────────
# Bug 2: VLM StructuralGuard nav_keywords mismatch
# ─────────────────────────────────────────────────────


class TestStructuralGuardNavKeywords:
    """The VLM post-guard must recognize 'following list' as a nav intent."""

    def test_structural_guard_allows_following_list_intent(self):
        """
        When the VLM selects a bottom-nav-zone element for intent
        'open following list', the StructuralGuard at line 1594-1612
        must NOT reject it as a 'non-nav intent'.

        This tests the VLM post-guard's is_nav_intent classification.
        """

        # The intent is "open following list"
        intent = "open following list"
        low_intent = intent.lower()

        # The VLM guard's nav keywords (this is what we're testing)
        # This is the list from line 1594 of telepathic_engine.py
        nav_keywords_vlm = [
            "tab",
            "navigation",
            "reels tab",
            "profile tab",
            "home tab",
            "message tab",
            # These MUST be present to fix the bug:
            "following",
            "follower",
            "followers",
        ]

        is_nav_intent = any(k in low_intent for k in nav_keywords_vlm)

        assert is_nav_intent, (
            f"Intent '{intent}' was classified as non-nav by the VLM guard! "
            f"The nav_keywords list is missing 'following'/'follower' keywords. "
            f"This causes the StructuralGuard to reject valid following-list clicks."
        )


# ─────────────────────────────────────────────────────
# Bug 3: Synthetic intent masking
# ─────────────────────────────────────────────────────


class TestSyntheticIntentTracking:
    """GoalExecutor must stop retrying synthetic intents that fail."""

    def test_goap_stops_retrying_synthetic_intents(self, monkeypatch):
        """
        When a synthetic intent (not in available_actions) fails execution,
        the GoalExecutor must track it in explored_nav_actions AND prevent
        the planner from returning it again.

        This ensures the bot doesn't burn 15 steps on the same failing action.
        """
        device = MagicMock()
        executor = GoalExecutor(device, "test_user")
        executor.max_steps = 8

        # Mock PathMemory to avoid DB
        executor.path_memory.recall_path = MagicMock(return_value=None)

        call_count = {"execute": 0, "plan": 0}
        action_history = []

        # Track which actions the planner returns
        def fake_perceive(*args, **kwargs):
            return {
                "screen_type": ScreenType.OWN_PROFILE,
                "available_actions": ["tap home tab", "press back", "tap profile tab"],
                "context": {},
            }

        executor.perceive = MagicMock(side_effect=fake_perceive)

        # Mock _execute_action to always fail for the synthetic intent
        def fake_execute(action, **kwargs):
            call_count["execute"] += 1
            action_history.append(action)
            if action == "open following list":
                return False
            if action == "press back":
                return True
            return False

        monkeypatch.setattr(executor, "_execute_action", fake_execute)

        # Speed up sleeps
        monkeypatch.setattr("GramAddict.core.goap.random_sleep", lambda x, y: None)

        # Run
        executor.achieve("open following list", max_steps=8)

        # Count how many times the synthetic intent was tried
        synthetic_attempts = action_history.count("open following list")

        assert synthetic_attempts <= 2, (
            f"GoalExecutor tried the synthetic intent 'open following list' "
            f"{synthetic_attempts} times! Maximum should be 2 (initial + 1 retry). "
            f"Full action history: {action_history}"
        )


# ─────────────────────────────────────────────────────
# Bug 4: _extract_available_actions for own profile
# ─────────────────────────────────────────────────────


class TestAvailableActionsOwnProfile:
    """available_actions must include 'tap following list' on own profile."""

    def test_available_actions_includes_following_via_resource_id(self):
        """
        On the own profile page with German locale ('Abonniert' instead of
        'following'), _extract_available_actions must detect the following
        counter via resource-id `profile_header_following_stacked_familiar`.
        """
        xml = _load_fixture("own_profile_with_stats.xml")
        identity = ScreenIdentity("marisaundmarc")
        result = identity.identify(xml)

        assert result["screen_type"] == ScreenType.OWN_PROFILE, f"Expected OWN_PROFILE but got {result['screen_type']}"

        available = result["available_actions"]
        assert "tap following list" in available, (
            f"'tap following list' not in available_actions! "
            f"The bot can't even perceive that the following counter is clickable. "
            f"Available: {available}"
        )

    def test_available_actions_includes_following_via_content_desc(self):
        """
        When content-desc contains 'following' (English locale),
        'tap following list' must also be detected.
        """
        # Use user_profile_dump.xml which has content-desc="991following"
        xml = _load_fixture("user_profile_dump.xml")
        identity = ScreenIdentity("testuser")

        # The user_profile_dump.xml has no selected nav tab, so _classify_screen
        # would fall back to LLM. Mock it to return OTHER_PROFILE.
        with patch("GramAddict.core.llm_provider.query_llm", return_value="OTHER_PROFILE"):
            result = identity.identify(xml)

        screen_type = result["screen_type"]
        assert screen_type in (
            ScreenType.OWN_PROFILE,
            ScreenType.OTHER_PROFILE,
        ), f"Expected profile screen, got {screen_type}"

        available = result["available_actions"]
        assert "tap following list" in available, (
            f"'tap following list' not in available_actions on English profile! " f"Available: {available}"
        )
