import os
import time
from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.bot_flow import _run_zero_latency_feed_loop, _wait_for_post_loaded
from GramAddict.core.perception.feed_analysis import FEED_MARKERS

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DUMPS = {
    "organic": os.path.join(ROOT_DIR, "fixtures", "organic_post.xml"),
    "explore": os.path.join(ROOT_DIR, "fixtures", "explore_feed_dump.xml"),
}
FIXTURE_DIR = os.path.join(ROOT_DIR, "fixtures")


def mutate_xml_to_foreign(xml_content: str) -> str:
    """Removes meaningful text content to simulate a language failure or empty state."""
    import re

    # Strip text and content-desc
    xml = re.sub(r'text="[^"]*"', 'text=""', xml_content)
    xml = re.sub(r'content-desc="[^"]*"', 'content-desc=""', xml)
    return xml


def mutate_xml_remove_feed_markers(xml_content: str) -> str:
    """Removes all feed markers to simulate a grid view or random popup."""
    xml = xml_content
    for marker in FEED_MARKERS:
        xml = xml.replace(marker, "some_random_id")
    return xml


class ConfigMock:
    def __init__(self):
        self.args = MagicMock()
        self.args.interact_percentage = 0
        self.args.comment_percentage = 0


@pytest.fixture
def test_dumps():
    dumps = {}
    with open(DUMPS["organic"], "r") as f:
        dumps["post"] = f.read()
    # Fake explore grid that lacks ALL feed markers
    dumps["grid"] = (
        '<?xml version="1.0"?><hierarchy><node resource-id="com.instagram.android:id/explore_grid_container" /></hierarchy>'
    )
    return dumps


def test_slow_loading_post_recovery(test_dumps):
    """
    Test that _wait_for_post_loaded correctly handles a delay where the
    first few dumps are grids, and only later it becomes a post.
    """
    device = MagicMock()
    # Simulate: Grid -> Grid -> Error -> Post
    device.dump_hierarchy.side_effect = [
        test_dumps["grid"],
        test_dumps["grid"],
        Exception("uiautomator2 temp failure"),
        test_dumps["post"],
    ]

    # We patch sleep to make the test super fast
    with patch("GramAddict.core.bot_flow.sleep", return_value=None):
        time.time()
        success = _wait_for_post_loaded(device, timeout=5)
        # Should return true when it hits the 4th element
        assert success is True
        assert device.dump_hierarchy.call_count == 4


def test_wait_timeout_aborts_gracefully(test_dumps):
    """Test what happens if the network is so slow it times out entirely."""
    device = MagicMock()
    # Always return grid
    device.dump_hierarchy.return_value = test_dumps["grid"]

    # Patch time.time to simulate 6 seconds passing immediately
    # We add sequence padding because python's logger internally uses time.time()
    with patch("time.time", side_effect=[0, 1, 6, 6, 6, 6, 6, 6, 6, 6]):
        with patch("GramAddict.core.bot_flow.sleep", return_value=None):
            success = _wait_for_post_loaded(device, timeout=5)
            assert success is False


def test_empty_content_extraction_guard(test_dumps):
    """
    Test that if a post is loaded, but it has strange empty text (foreign language or bug),
    the bot aborts interaction and scrolls instead of judging empty content.
    """
    device = MagicMock()
    nav_graph = MagicMock()
    configs = ConfigMock()

    # We create a fake active inference engine to just break the loop after 1 iteration
    ai = MagicMock()
    # Dopamine engine controls loop exit
    dopamine = MagicMock()
    dopamine.is_app_session_over.side_effect = [False, True]  # Run once, then exit
    dopamine.wants_to_change_feed.return_value = False
    dopamine.wants_to_doomscroll.return_value = False

    cognitive_stack = {
        "dopamine": dopamine,
        "active_inference": ai,
        "resonance": None,
        "growth_brain": None,
        "swarm": None,
        "darwin": None,
    }

    # Mutate the post so it has NO text or description
    broken_xml = mutate_xml_to_foreign(test_dumps["post"])
    device.dump_hierarchy.return_value = broken_xml

    from GramAddict.core.situational_awareness import SituationType

    with (
        patch("GramAddict.core.bot_flow._humanized_scroll") as mock_scroll,
        patch("GramAddict.core.bot_flow.sleep"),
        patch(
            "GramAddict.core.situational_awareness.SituationalAwarenessEngine.perceive",
            return_value=SituationType.NORMAL,
        ),
    ):
        result = _run_zero_latency_feed_loop(device, None, nav_graph, configs, MagicMock(), "HomeFeed", cognitive_stack)

        # Ensure scroll was called (the recovery mechanism)
        assert mock_scroll.called
        # Check that we never called resonance evaluation because we broke early
        assert not ai.predict_state.called
        assert result == "FEED_EXHAUSTED"


def test_missing_feed_markers_guard(test_dumps):
    """
    Test that if the UI is completely foreign (e.g., a system popup),
    the bot detects missing feed markers and scrolls to recover.
    """
    device = MagicMock()
    configs = ConfigMock()

    dopamine = MagicMock()
    dopamine.is_app_session_over.side_effect = [False, True]
    dopamine.wants_to_change_feed.return_value = False
    dopamine.wants_to_doomscroll.return_value = False

    cognitive_stack = {"dopamine": dopamine, "growth_brain": None, "active_inference": None}

    # Mutate XML to remove all FEED MARKERS
    alien_xml = mutate_xml_remove_feed_markers(test_dumps["post"])
    device.dump_hierarchy.return_value = alien_xml

    with patch("GramAddict.core.bot_flow._humanized_scroll"), patch("GramAddict.core.bot_flow.sleep"):
        _run_zero_latency_feed_loop(device, None, MagicMock(), configs, MagicMock(), "HomeFeed", cognitive_stack)


@patch("GramAddict.core.device_facade.u2")
def test_xpath_watcher_initialization(mock_u2):
    """
    Test fixing the critical watcher API bug.
    Ensures that device facade uses .watcher("name").when(xpath=...)
    """
    mock_d = MagicMock()
    mock_u2.connect.return_value = mock_d

    # Setup mock chain: deviceV2.watcher("crash_dialog").when(...)
    mock_watcher = MagicMock()
    mock_d.watcher.return_value = mock_watcher
    mock_when = MagicMock()
    mock_watcher.when.return_value = mock_when

    # Just init the facade
    from GramAddict.core.device_facade import create_device

    create_device("fake_serial", "com.fake.app", MagicMock())

    # Verify exact API call structure for XPath
    mock_d.watcher.assert_any_call("crash_dialog")
    mock_d.watcher.assert_any_call("system_dialog")

    # We can't perfectly assert the chained arguments natively without a bit of inspection,
    # but we can verify it didn't crash and called start
    assert mock_d.watcher.start.called
