from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.behaviors import PluginRegistry, load_all_plugins
from GramAddict.core.bot_flow import (
    _align_active_post,
    _extract_post_content,
    _run_zero_latency_feed_loop,
    _run_zero_latency_stories_loop,
)
from GramAddict.core.utils import is_ad


@pytest.fixture(autouse=True)
def setup_plugins():
    PluginRegistry.reset()
    load_all_plugins()


@pytest.fixture
def mock_device():
    device = MagicMock()
    device.deviceV2 = MagicMock()
    device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
    # Link facade method to deviceV2 mock so old tests keep working
    device.dump_hierarchy = device.dump_hierarchy
    return device


@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
def test_extract_post_content(mock_get_telepathic):
    mock_engine = MagicMock()
    mock_engine.find_best_node.side_effect = [
        {"original_attribs": {"text": "test_user"}},
        {"original_attribs": {"desc": "test description of image with more than 10 chars"}},
    ]
    mock_get_telepathic.return_value = mock_engine
    xml = "<xml/>"
    res = _extract_post_content(xml)
    assert res["username"] == "test_user"
    assert "test description" in res["description"]


@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
def test_extract_post_content_fallback_caption(mock_get_telepathic):
    mock_engine = MagicMock()
    mock_engine.find_best_node.side_effect = [{"original_attribs": {"text": "other_user"}}, None]
    mock_get_telepathic.return_value = mock_engine
    xml = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
    <hierarchy>
        <node resource-id="" text="other_user this is a very long caption text that we want to extract as fallback" />
    </hierarchy>"""
    res = _extract_post_content(xml)
    assert res["username"] == "other_user"
    assert "this is a very long caption" in res["caption"]


def testis_ad():
    assert is_ad('<node resource-id="com.instagram.android:id/ad_cta_button" />')
    assert is_ad('<node resource-id="com.instagram.android:id/clips_single_image_ads_media_content" />')
    assert is_ad('<node resource-id="com.instagram.android:id/secondary_label" text="Sponsored" />')
    assert not is_ad('<node resource-id="com.instagram.android:id/secondary_label" text="regular post" />')
    assert not is_ad('<node resource-id="com.instagram.android:id/normal_post" />')


@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
def test_align_active_post(mock_get_telepathic, mock_device):
    # Test snapping when post is far from ideal coordinates
    mock_engine = MagicMock()
    mock_engine.find_best_node.return_value = {"bounds": "[0,800][1080,900]"}
    mock_get_telepathic.return_value = mock_engine
    mock_device.dump_hierarchy.return_value = "<xml/>"
    _align_active_post(mock_device)
    # The header is at 850px. Target is 250px. Diff is 600px. It should swipe.
    assert mock_device.swipe.called


def test_feed_loop_boredom_change_feed(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
    mock_cognitive_stack["growth_brain"].evaluate_governance.return_value = "SHIFT_CONTEXT"

    configs = MagicMock()
    session_state = MagicMock()
    session_state.check_limit.return_value = (False, False, False, False)

    res = _run_zero_latency_feed_loop(
        mock_device,
        mock_cognitive_stack["zero_engine"],
        mock_cognitive_stack["nav_graph"],
        configs,
        session_state,
        "HomeFeed",
        mock_cognitive_stack,
    )
    assert res == "BOREDOM_CHANGE_FEED"


@pytest.mark.skip(reason="Legacy integration test needs refactor for Plugin architecture")
def test_feed_loop_context_lost(mock_device, mock_cognitive_stack):
    # Simulate not having any feed markers 3 times
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, False, False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False

    mock_device.dump_hierarchy.return_value = "<hierarchy></hierarchy>"  # Blind

    # Needs telepathic engine mock
    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.diagnostic_dump.dump_ui_state"),
    ):
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.return_value = [
            {"x": 1, "y": 2}
        ]  # Pretend we have nodes so it doesn't trigger zero-node immediately

        configs = MagicMock()
        session_state = MagicMock()
        session_state.check_limit.return_value = (False, False, False, False)

        res = _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )
        assert res == "CONTEXT_LOST"


def test_feed_loop_zero_nodes(mock_device, mock_cognitive_stack):
    # Tests the Zero-Node recovery anomaly handler
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False

    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.bot_flow._humanized_scroll") as mock_scroll,
    ):
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.return_value = []  # Zero interactive nodes

        configs = MagicMock()
        session_state = MagicMock()
        session_state.check_limit.return_value = (False, False, False, False)

        _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )

        assert mock_device.press.called_with("back")
        assert mock_scroll.called


def test_feed_loop_ad_skip(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False

    mock_device.dump_hierarchy.return_value = """<?xml version='1.0' ?>
    <hierarchy>
        <node resource-id="com.instagram.android:id/row_feed_button_like" />
        <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="ad_account" />
        <node resource-id="com.instagram.android:id/ad_cta_button" />
    </hierarchy>"""

    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.bot_flow._humanized_scroll") as mock_scroll,
        patch("GramAddict.core.bot_flow._align_active_post") as mock_align,
    ):
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.return_value = [{"x": 1}]
        mock_align.return_value = False

        configs = MagicMock()
        session_state = MagicMock()
        session_state.check_limit.return_value = (False, False, False, False)

        _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )
        assert mock_scroll.called


def test_stories_loop_success(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False

    configs = MagicMock()
    configs.args.stories = "1"
    session_state = MagicMock()

    mock_device.dump_hierarchy.return_value = """<?xml version='1.0' ?>
    <hierarchy>
        <node resource-id="com.instagram.android:id/story_viewer" />
    </hierarchy>"""

    with patch("GramAddict.core.bot_flow._humanized_click") as mock_click, patch("GramAddict.core.bot_flow.sleep"):
        res = _run_zero_latency_stories_loop(mock_device, configs, session_state, mock_cognitive_stack)
        assert res == "FEED_EXHAUSTED"
        assert mock_click.called


def test_stories_loop_boredom(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = True

    configs = MagicMock()
    session_state = MagicMock()

    with patch("GramAddict.core.bot_flow.sleep"):
        res = _run_zero_latency_stories_loop(mock_device, configs, session_state, mock_cognitive_stack)
        assert res == "BOREDOM_CHANGE_FEED"
        assert mock_device.press.called_with("back")


def test_start_bot_interrupt():
    from GramAddict.core.bot_flow import start_bot

    # Mock all the heavy initialization
    with (
        patch("GramAddict.core.bot_flow.Config") as MockConfig,
        patch("GramAddict.core.bot_flow.configure_logger"),
        patch("GramAddict.core.bot_flow.check_if_updated"),
        patch("GramAddict.core.benchmark_guard.check_model_benchmarks"),
        patch("GramAddict.core.llm_provider.log_openrouter_burn"),
        patch("GramAddict.core.bot_flow.create_device"),
        patch("GramAddict.core.bot_flow.set_time_delta"),
        patch("GramAddict.core.bot_flow.SessionState") as MockSession,
        patch("GramAddict.core.bot_flow.open_instagram", side_effect=KeyboardInterrupt()),
    ):
        MockConfig.return_value.args.feed = True
        MockConfig.return_value.args.explore = False
        MockConfig.return_value.args.reels = False
        MockConfig.return_value.args.stories = False
        MockConfig.return_value.args.capture_e2e_dumps = False
        MockConfig.return_value.args.working_hours = [10, 20]
        MockConfig.return_value.args.time_delta_session = 30
        MockConfig.return_value.args.username = "test_user"
        MockConfig.return_value.args.blank_start = False
        MockConfig.return_value.args.ai_embedding_url = "http://localhost:11434/api/chat"
        MockConfig.return_value.args.ai_embedding_model = "llama3"
        MockConfig.return_value.args.agent_strategy = "conservative"

        MockSession.inside_working_hours.return_value = (True, 0)

        with pytest.raises(KeyboardInterrupt):
            start_bot(username="test_user", device_id="123")


@pytest.mark.skip(reason="Legacy integration test needs refactor for Plugin architecture")
def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
    # This test hits the core interaction (Lines 900 - 1300)
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
    mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.85

    configs = MagicMock()
    configs.args.likes_percentage = 100
    configs.args.follow_percentage = 100
    configs.args.comment_percentage = 100
    configs.args.ai_vibe = "friendly"
    configs.args.ai_condenser_model = "test-model"
    configs.args.ai_condenser_url = "test-url"
    configs.args.dry_run_comments = False

    session_state = MagicMock()
    # If checking ALL, return a tuple of Falses. If specific limit like LIKES/COMMENTS, return False bool.
    session_state.check_limit.side_effect = (
        lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
    )

    # Needs to report a structure that has NO ad, HAS content, and HAS feed markers.
    mock_device.dump_hierarchy.return_value = """<?xml version='1.0' ?>
    <hierarchy>
        <node resource-id="com.instagram.android:id/row_feed_button_like" />
        <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
        <node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
        <!-- Comment sheet structure for the sub-engagement logic -->
        <node class="android.widget.LinearLayout">
            <node resource-id="com.instagram.android:id/row_comment_textview_comment" text="This is a fantastic picture!" />
            <node resource-id="com.instagram.android:id/row_comment_button_like" bounds="[10,10][20,20]" />
        </node>
    </hierarchy>"""

    # Ensure radome doesn't destroy our XML string
    mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
    # Simulate that nav_graph transitions work
    mock_cognitive_stack["nav_graph"].do.return_value = True

    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.bot_flow._extract_post_content") as mock_extract,
        patch("GramAddict.core.llm_provider.query_llm") as mock_llm,
        patch("GramAddict.core.bot_flow.random.random", return_value=0.11),
        patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5),
        patch("GramAddict.core.bot_flow.random.randint", return_value=1),
        patch("GramAddict.core.bot_flow._align_active_post", return_value=False),
        patch("GramAddict.core.bot_flow._humanized_scroll"),
        patch("GramAddict.core.bot_flow._humanized_click") as mock_click,
        patch("GramAddict.core.stealth_typing.ghost_type", autospec=True) as mock_type,
    ):
        mock_extract.return_value = {"username": "legit_user", "description": "test image", "caption": ""}
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.return_value = [
            {"x": 1, "y": 2, "original_attribs": {"text": "This is a fantastic picture!"}}
        ]
        mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}
        mock_llm.return_value = {"response": "Great shot!"}

        mock_cognitive_stack["telepathic"] = mock_instance

        # We need to ensure that the configs allow interacting!
        configs.args.interact_percentage = 100

        _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )

        assert mock_click.called
        assert mock_type.called


def test_feed_loop_repost(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
    mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.85

    configs = MagicMock()
    configs.args.repost_percentage = 100
    configs.args.likes_percentage = 0
    configs.args.comment_percentage = 0

    session_state = MagicMock()
    session_state.check_limit.side_effect = (
        lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
    )

    mock_device.dump_hierarchy.return_value = """<?xml version='1.0' ?>
    <hierarchy>
        <node resource-id="com.instagram.android:id/row_feed_button_like" />
        <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
        <node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
    </hierarchy>"""

    mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
    mock_cognitive_stack["nav_graph"].do.return_value = True

    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.bot_flow.random.random", return_value=0.11),
        patch("GramAddict.core.bot_flow._align_active_post", return_value=False),
        patch("GramAddict.core.bot_flow._humanized_scroll"),
        patch("GramAddict.core.bot_flow._humanized_click"),
    ):
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2}]
        mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}

        mock_cognitive_stack["telepathic"] = mock_instance
        configs.args.interact_percentage = 100

        from GramAddict.core.bot_flow import _run_zero_latency_feed_loop

        _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )


@pytest.mark.skip(reason="Legacy integration test needs refactor for Plugin architecture")
def test_profile_learning_percentage_trigger(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
    mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.50  # Not high enough to trigger default

    configs = MagicMock()
    configs.args.profile_learning_percentage = 100  # Should force visit
    # In the new architecture, ProfileVisitPlugin uses profile_visit_percentage
    configs.args.profile_visit_percentage = 100
    configs.args.profile_learning_percentage = 100

    session_state = MagicMock()
    session_state.check_limit.side_effect = (
        lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
    )

    mock_device.dump_hierarchy.return_value = """<?xml version='1.0' ?>
    <hierarchy>
        <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
        <node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
    </hierarchy>"""

    mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
    mock_cognitive_stack["nav_graph"].do.return_value = True

    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.bot_flow._extract_post_content") as mock_extract,
        patch("GramAddict.core.bot_flow.random.random", return_value=0.5),
        patch("GramAddict.core.behaviors.profile_visit.random.random", return_value=0.01),
        patch("GramAddict.core.bot_flow._align_active_post", return_value=False),
        patch("GramAddict.core.bot_flow._humanized_scroll"),
        patch("GramAddict.core.bot_flow._interact_with_profile"),
    ):
        mock_extract.return_value = {"username": "legit_user", "description": "test image", "caption": ""}
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2, "original_attribs": {"text": "dummy"}}]
        mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}

        mock_cognitive_stack["telepathic"] = mock_instance
        configs.args.interact_percentage = 100

        from GramAddict.core.bot_flow import _run_zero_latency_feed_loop

        _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )

        # In the new architecture, ProfileVisitPlugin calls nav_graph.do("tap post username")
        assert mock_cognitive_stack["nav_graph"].do.called
        # Check if 'tap post username' was one of the calls
        args_list = [call.args[0] for call in mock_cognitive_stack["nav_graph"].do.call_args_list]
        assert "tap post username" in args_list


def test_ai_learn_own_profile_triggers_goap():
    with (
        patch("GramAddict.core.bot_flow.Config") as MockConfig,
        patch("GramAddict.core.bot_flow.configure_logger"),
        patch("GramAddict.core.bot_flow.check_if_updated"),
        patch("GramAddict.core.benchmark_guard.check_model_benchmarks"),
        patch("GramAddict.core.llm_provider.log_openrouter_burn"),
        patch("GramAddict.core.llm_provider.prewarm_ollama_models"),
        patch("GramAddict.core.bot_flow.create_device"),
        patch("GramAddict.core.bot_flow.set_time_delta"),
        patch("GramAddict.core.bot_flow.SessionState") as MockSession,
        patch("GramAddict.core.bot_flow.open_instagram", return_value=True),
        patch("GramAddict.core.bot_flow.verify_and_switch_account", return_value=True),
        patch("GramAddict.core.bot_flow.get_instagram_version", return_value="1.0"),
        patch("GramAddict.core.goap.GoalExecutor") as MockGoalExecutor,
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.llm_provider.query_llm") as mock_query,
        patch("GramAddict.core.bot_flow.DojoEngine"),
        patch("GramAddict.core.bot_flow.sleep"),
    ):
        MockConfig.return_value.args.ai_learn_own_profile = True
        MockConfig.return_value.args.agent_strategy = "aggressive_growth"
        MockConfig.return_value.args.capture_e2e_dumps = False
        MockConfig.return_value.args.explore = False
        MockConfig.return_value.args.feed = False
        MockConfig.return_value.args.reels = False
        MockConfig.return_value.args.stories = False
        MockConfig.return_value.args.working_hours = [10, 20]
        MockConfig.return_value.args.time_delta_session = 30

        MockSession.inside_working_hours.return_value = (True, 0)

        mock_goap = MockGoalExecutor.get_instance.return_value
        mock_goap.achieve.return_value = True

        mock_telepathic = MockTelepathic.return_value  # the class constructor is called inside start_bot
        mock_telepathic._extract_semantic_nodes.return_value = [{"original_attribs": {"text": "my cool bio"}}]

        mock_query.return_value = {"persona": "cool dev", "vibe": "chill"}

        from GramAddict.core.bot_flow import start_bot

        try:
            with patch("GramAddict.core.bot_flow.random_sleep", side_effect=KeyboardInterrupt()):
                start_bot(username="testuser", device_id="123")
        except KeyboardInterrupt:
            pass

        mock_goap.achieve.assert_any_call("learn own profile")
        # resonance is created internally, so we can't easily assert on update_identity unless we patch ResonanceEngine too.
        # It's sufficient to know the GOAP goal was triggered.


@pytest.mark.skip(reason="Legacy integration test needs refactor for Plugin architecture")
def test_profile_mismatch_recovery(mock_device, mock_cognitive_stack):
    mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
    mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
    mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
    mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.50

    configs = MagicMock()
    configs.args.profile_learning_percentage = 100  # Should force visit
    configs.args.likes_percentage = 0
    configs.args.comment_percentage = 0
    configs.args.follow_percentage = 0

    session_state = MagicMock()
    session_state.check_limit.side_effect = (
        lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
    )

    feed_xml = """<?xml version='1.0' ?>
        <hierarchy>
            <node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="amorextravel" />
            <node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
        </hierarchy>"""
    profile_xml = """<?xml version='1.0' ?>
        <hierarchy>
            <node resource-id="com.instagram.android:id/action_bar_title" text="ryanresatka" />
            <node resource-id="com.instagram.android:id/row_profile_header_textview_biography" text="cool bio" />
        </hierarchy>"""

    call_count = [0]

    def dump_hierarchy_mock(*args, **kwargs):
        call_count[0] += 1
        return feed_xml if call_count[0] == 1 else profile_xml

    mock_device.dump_hierarchy.side_effect = dump_hierarchy_mock

    mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
    mock_cognitive_stack["nav_graph"].do.return_value = True

    with (
        patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
        patch("GramAddict.core.bot_flow._extract_post_content") as mock_extract,
        patch("GramAddict.core.bot_flow.random.random", return_value=0.5),
        patch("GramAddict.core.behaviors.profile_visit.random.random", return_value=0.01),
        patch("GramAddict.core.bot_flow._align_active_post", return_value=False),
        patch("GramAddict.core.bot_flow._humanized_scroll"),
        patch("GramAddict.core.bot_flow._interact_with_profile"),
    ):
        mock_extract.return_value = {"username": "amorextravel", "description": "test image", "caption": ""}
        mock_instance = MockTelepathic.get_instance.return_value
        mock_instance._extract_semantic_nodes.side_effect = [
            [{"x": 1, "y": 2, "original_attribs": {"text": "amorextravel"}}],  # 1st call at top of loop
            [{"x": 1, "y": 2, "original_attribs": {"text": "amorextravel"}}],  # 2nd call before Targeted UX
            [
                {"x": 1, "y": 2, "text": "ryanresatka", "resource_id": "com.instagram.android:id/action_bar_title"}
            ],  # 3rd call on profile
        ]
        mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}

        mock_cognitive_stack["telepathic"] = mock_instance
        configs.args.interact_percentage = 100

        from GramAddict.core.bot_flow import _run_zero_latency_feed_loop

        _run_zero_latency_feed_loop(
            mock_device,
            mock_cognitive_stack["zero_engine"],
            mock_cognitive_stack["nav_graph"],
            configs,
            session_state,
            "HomeFeed",
            mock_cognitive_stack,
        )

        # In the new architecture, ProfileVisitPlugin calls nav_graph.do("tap post username")
        assert mock_cognitive_stack["nav_graph"].do.called
        args_list = [call.args[0] for call in mock_cognitive_stack["nav_graph"].do.call_args_list]
        assert "tap post username" in args_list
