import logging
from unittest.mock import MagicMock, call

from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop


class TestUnfollowEngine:
    def setup_method(self):
        self.mock_device = MagicMock()
        self.mock_device.deviceV2 = MagicMock()
        self.mock_device.app_id = "com.instagram.android"
        self.mock_device._get_current_app.return_value = "com.instagram.android"
        self.mock_device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}

        self.mock_telepathic = MagicMock()
        self.mock_dopamine = MagicMock()
        self.mock_dopamine.is_app_session_over.return_value = False
        self.mock_dopamine.wants_to_change_feed.return_value = False
        self.mock_dopamine.boredom = 0.0

        self.mock_resonance = MagicMock()
        self.mock_resonance.calculate_resonance.return_value = 0.2

        self.cognitive_stack = {
            "telepathic": self.mock_telepathic,
            "dopamine": self.mock_dopamine,
            "resonance": self.mock_resonance,
        }

        self.mock_configs = MagicMock()
        self.mock_configs.args.total_unfollows_limit = 50

        self.mock_session_state = MagicMock()
        self.mock_session_state.totalUnfollowed = 0
        self.mock_session_state.check_limit.return_value = False

        self.logger = logging.getLogger("test")

    def test_unfollow_loop_unfollows_low_resonance(self, monkeypatch):
        """
        Happy path (Smart Unfollow):
        1. Clicks profile row.
        2. Dumps profile UI.
        3. Resonance is low (< 0.4).
        4. Clicks 'Following' button on profile.
        5. Clicks 'Unfollow' confirm.
        6. Clicks back.
        """

        def fake_extract_semantic_nodes(xml, intent, **kwargs):
            if "profile rows" in intent:
                return [{"semantic_string": "Profile Row", "x": 100, "y": 200, "bounds": "[50,150]", "skip": False}]
            elif "Unfollow" in intent:
                return [
                    {
                        "semantic_string": "Unfollow Confirmation",
                        "x": 500,
                        "y": 1500,
                        "bounds": "[100,200]",
                        "skip": False,
                    }
                ]
            else:
                return [
                    {"semantic_string": "Following Button", "x": 900, "y": 600, "bounds": "[100,200]", "skip": False}
                ]

        self.mock_telepathic._extract_semantic_nodes.side_effect = fake_extract_semantic_nodes
        self.mock_dopamine.wants_to_change_feed.side_effect = [True]
        self.mock_device.dump_hierarchy.return_value = '<node text="Some Bio"/>'

        # Patch local imports inside the bot_flow / unfollow_engine namespace
        mock_sleep = MagicMock()
        mock_click = MagicMock()
        import GramAddict.core.bot_flow

        monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", mock_sleep)
        monkeypatch.setattr(GramAddict.core.bot_flow, "random_sleep", mock_sleep)
        monkeypatch.setattr(GramAddict.core.bot_flow, "_humanized_click", mock_click)

        import GramAddict.core.utils

        monkeypatch.setattr(GramAddict.core.utils, "random_sleep", mock_sleep)

        result = _run_zero_latency_unfollow_loop(
            self.mock_device,
            None,
            None,
            self.mock_configs,
            self.mock_session_state,
            "FollowingList",
            self.cognitive_stack,
        )

        # Verify result and clicks
        assert result == "BOREDOM_CHANGE_FEED"
        assert self.mock_session_state.totalUnfollowed == 1

        # Assert humanized clicks were logged correctly (Profile Row -> Following Button -> Unfollow Confirm)
        assert mock_click.call_count == 3
        mock_click.assert_has_calls(
            [call(self.mock_device, 100, 200), call(self.mock_device, 900, 600), call(self.mock_device, 500, 1500)]
        )
        assert self.mock_device.back.call_count == 1

    def test_unfollow_loop_keeps_high_resonance_profile(self, monkeypatch):
        """
        If resonance is high, we keep following the account and go back to the list.
        """
        self.mock_resonance.calculate_resonance.return_value = 0.8

        def fake_extract_semantic_nodes(xml, intent, **kwargs):
            if "profile rows" in intent:
                return [{"semantic_string": "Profile Row", "x": 100, "y": 200, "bounds": "[50,150]", "skip": False}]
            return []

        self.mock_telepathic._extract_semantic_nodes.side_effect = fake_extract_semantic_nodes
        self.mock_dopamine.wants_to_change_feed.side_effect = [True]
        self.mock_device.dump_hierarchy.return_value = '<node text="Awesome Bio"/>'

        mock_sleep = MagicMock()
        mock_click = MagicMock()
        import GramAddict.core.bot_flow

        monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", mock_sleep)
        monkeypatch.setattr(GramAddict.core.bot_flow, "random_sleep", mock_sleep)
        monkeypatch.setattr(GramAddict.core.bot_flow, "_humanized_click", mock_click)
        import GramAddict.core.utils

        monkeypatch.setattr(GramAddict.core.utils, "random_sleep", mock_sleep)

        result = _run_zero_latency_unfollow_loop(
            self.mock_device,
            None,
            None,
            self.mock_configs,
            self.mock_session_state,
            "FollowingList",
            self.cognitive_stack,
        )

        assert result == "BOREDOM_CHANGE_FEED"
        assert self.mock_session_state.totalUnfollowed == 0

        # Clicked profile, then went back (no unfollow clicks)
        assert mock_click.call_count == 1
        mock_click.assert_has_calls([call(self.mock_device, 100, 200)])
        assert self.mock_device.back.call_count == 1

    def test_unfollow_loop_skips_close_friend(self, monkeypatch):
        """
        If 'enge freunde' is in the XML, skip resonance eval, skip unfollowing, just go back.
        """

        def fake_extract_semantic_nodes(xml, intent, **kwargs):
            if "profile rows" in intent:
                return [{"semantic_string": "Profile Row", "x": 100, "y": 200, "bounds": "[50,150]", "skip": False}]
            return []

        self.mock_telepathic._extract_semantic_nodes.side_effect = fake_extract_semantic_nodes
        self.mock_dopamine.wants_to_change_feed.side_effect = [True]

        # Mock XML with Enge Freunde
        self.mock_device.dump_hierarchy.side_effect = [
            '<node text="FollowingList"/>',  # First dump on list
            '<node text="Enge Freunde"/>',  # Second dump on profile
        ]

        mock_sleep = MagicMock()
        mock_click = MagicMock()
        import GramAddict.core.bot_flow

        monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", mock_sleep)
        monkeypatch.setattr(GramAddict.core.bot_flow, "random_sleep", mock_sleep)
        monkeypatch.setattr(GramAddict.core.bot_flow, "_humanized_click", mock_click)
        import GramAddict.core.utils

        monkeypatch.setattr(GramAddict.core.utils, "random_sleep", mock_sleep)

        result = _run_zero_latency_unfollow_loop(
            self.mock_device,
            None,
            None,
            self.mock_configs,
            self.mock_session_state,
            "FollowingList",
            self.cognitive_stack,
        )

        assert result == "BOREDOM_CHANGE_FEED"
        assert self.mock_session_state.totalUnfollowed == 0
        assert mock_click.call_count == 1
        assert self.mock_device.back.call_count == 1
        # Resonance shouldn't even be called for close friends
        self.mock_resonance.calculate_resonance.assert_not_called()

    def test_unfollow_loop_scrolls_if_empty(self, monkeypatch):
        """
        End of list path: If no following buttons are found, it should scroll _humanized_scroll_down.
        After 5 failed scrolls, it should gracefully return BOREDOM_CHANGE_FEED without crashing.
        """
        # Always return empty nodes
        self.mock_telepathic._extract_semantic_nodes.return_value = []

        # Patch the imported _humanized_scroll_down so we can track it
        mock_scroll = MagicMock()
        import GramAddict.core.unfollow_engine

        monkeypatch.setattr(GramAddict.core.unfollow_engine, "_humanized_scroll_down", mock_scroll)

        result = _run_zero_latency_unfollow_loop(
            self.mock_device,
            None,
            None,
            self.mock_configs,
            self.mock_session_state,
            "FollowingList",
            self.cognitive_stack,
        )

        assert result == "BOREDOM_CHANGE_FEED"
        # It should scroll exactly 6 times (failed_scrolls > 5)
        assert mock_scroll.call_count == 6
        assert self.mock_session_state.totalUnfollowed == 0

    def test_unfollow_loop_respects_limits(self):
        """
        Limit protection: If session limit is reached at the start, abort immediately.
        No clicks or UI dumps should occur.
        """
        # Tell session_state that UNFOLLOWS limit is hit
        self.mock_session_state.check_limit.return_value = (True, "Unfollow limit reached")

        result = _run_zero_latency_unfollow_loop(
            self.mock_device,
            None,
            None,
            self.mock_configs,
            self.mock_session_state,
            "FollowingList",
            self.cognitive_stack,
        )

        assert result == "BOREDOM_CHANGE_FEED"
        self.mock_device.dump_hierarchy.assert_not_called()
        self.mock_device.click.assert_not_called()
        self.mock_session_state.totalUnfollowed = 0
