from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop


@pytest.fixture
def unfollow_mock_dependencies():
    device = MagicMock()
    zero_engine = MagicMock()
    nav_graph = MagicMock()

    class ConfigArgs:
        total_unfollows_limit = 5

    configs = MagicMock()
    configs.args = ConfigArgs()

    session_state = MagicMock()
    session_state.check_limit.return_value = (False, False, False, False)
    session_state.totalUnfollowed = 0

    telepathic = MagicMock()
    dopamine = MagicMock()
    dopamine.is_app_session_over.side_effect = [False, False, True]
    dopamine.wants_to_change_feed.return_value = False
    dopamine.boredom = 0.0

    resonance = MagicMock()
    resonance.calculate_resonance.return_value = 0.2

    cognitive_stack = {
        "telepathic": telepathic,
        "dopamine": dopamine,
        "resonance": resonance,
    }

    return device, zero_engine, nav_graph, configs, session_state, cognitive_stack


def test_unfollow_engine_basic_loop(unfollow_mock_dependencies):
    device, zero_engine, nav_graph, configs, session_state, cognitive_stack = unfollow_mock_dependencies

    telepathic = cognitive_stack["telepathic"]

    # 1. Finds profile row
    # 2. Finds following button on profile
    # 3. Finds confirm dialog
    telepathic._extract_semantic_nodes.side_effect = [
        [{"semantic_string": "Profile Row", "x": 100, "y": 200, "skip": False, "bounds": "..."}],
        [{"semantic_string": "Following Button", "x": 150, "y": 250, "skip": False}],
        [{"semantic_string": "Unfollow Confirm", "x": 200, "y": 300, "skip": False}],
        [],  # second iteration
        [],
    ]

    with (
        patch("GramAddict.core.unfollow_engine._humanized_scroll_down"),
        patch("GramAddict.core.bot_flow.sleep"),
        patch("GramAddict.core.bot_flow.random_sleep"),
        patch("GramAddict.core.utils.random_sleep"),
        patch("GramAddict.core.bot_flow._humanized_click") as mock_click,
    ):
        device.dump_hierarchy.return_value = '<node text="Basic Bio"/>'

        res = _run_zero_latency_unfollow_loop(
            device, zero_engine, nav_graph, configs, session_state, "FollowingList", cognitive_stack
        )

        # Clicked profile -> following button -> confirm
        assert mock_click.call_count == 3
        assert session_state.totalUnfollowed == 1
        assert res == "FEED_EXHAUSTED"


def test_unfollow_engine_chaos_mode(unfollow_mock_dependencies):
    device, zero_engine, nav_graph, configs, session_state, cognitive_stack = unfollow_mock_dependencies

    telepathic = cognitive_stack["telepathic"]

    # Simulate Telepathic failure / missing nodes
    telepathic._extract_semantic_nodes.side_effect = Exception("UI DUMP CORRUPTED")

    with (
        patch("GramAddict.core.unfollow_engine._humanized_scroll_down") as mock_scroll,
        patch("GramAddict.core.bot_flow.sleep"),
        patch("GramAddict.core.bot_flow._humanized_click"),
    ):
        res = _run_zero_latency_unfollow_loop(
            device, zero_engine, nav_graph, configs, session_state, "FollowingList", cognitive_stack
        )

        # It should catch the exception, scroll down, and increment failed scans until it realizes context is lost
        assert mock_scroll.call_count > 0
        assert res == "CONTEXT_LOST" or res == "FEED_EXHAUSTED"


def test_unfollow_engine_limits(unfollow_mock_dependencies):
    device, zero_engine, nav_graph, configs, session_state, cognitive_stack = unfollow_mock_dependencies
    session_state.check_limit.return_value = (True, False, False, False)

    res = _run_zero_latency_unfollow_loop(
        device, zero_engine, nav_graph, configs, session_state, "FollowingList", cognitive_stack
    )
    assert res == "BOREDOM_CHANGE_FEED"
