import urllib.request
from unittest.mock import MagicMock, create_autospec, patch

import pytest

from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin
from GramAddict.core.behaviors.comment import CommentPlugin
from GramAddict.core.behaviors.follow import FollowPlugin
from GramAddict.core.behaviors.grid_like import GridLikePlugin
from GramAddict.core.behaviors.like import LikePlugin
from GramAddict.core.behaviors.profile_guard import ProfileGuardPlugin
from GramAddict.core.behaviors.story_view import StoryViewPlugin
from GramAddict.core.q_nav_graph import QNavGraph
from GramAddict.core.session_state import SessionState
from GramAddict.core.telepathic_engine import TelepathicEngine
from tests.e2e.test_sim_full_lifecycle import AndroidEnvironmentSimulator

# ==============================================================================
# Stateful Simulator for Behaviors
# ==============================================================================


class BehaviorSimulator(AndroidEnvironmentSimulator):
    def __init__(self, start_state="user_profile"):
        super().__init__()
        self.state_stack = [start_state]
        self.state_files.update(
            {
                "private_profile": "tests/fixtures/user_profile_dump.xml",  # We will mock the content dynamically if needed, or use a real private profile xml
            }
        )
        self.actions_taken = []

    def human_click(self, x, y):
        super().human_click(x, y)
        self.actions_taken.append(("click", x, y))

    def swipe(self, sx, sy, ex, ey, duration=None):
        super().swipe(sx, sy, ex, ey, duration)
        self.actions_taken.append(("swipe", sx, sy, ex, ey))
        # If we are using a mock_xml, simulate state changes based on coordinates
        if hasattr(self, "mock_xml") and self.mock_xml:
            # Simulate Follow button
            if 100 <= sx <= 400 and 800 <= sy <= 950:
                self.mock_xml = self.mock_xml.replace('text="Follow"', 'text="Following"')
            # Simulate Like button
            elif 50 <= sx <= 150 and 1500 <= sy <= 1600:
                self.mock_xml = self.mock_xml.replace('content-desc="Like"', 'content-desc="Liked"')

    def dump_hierarchy(self):
        # Allow dynamic override of the XML for guard tests
        if hasattr(self, "mock_xml") and self.mock_xml:
            return self.mock_xml
        return super().dump_hierarchy()


# ==============================================================================
# Fixtures
# ==============================================================================


@pytest.fixture(autouse=True)
def setup_qdrant_isolation():
    """Prefix all Qdrant collections with test_behaviors_ so we don't pollute live data."""
    from GramAddict.core.qdrant_memory import QdrantBase

    original_init = QdrantBase.__init__

    def mocked_init(self, collection_name, *args, **kwargs):
        test_collection = f"test_behaviors_{collection_name}"
        original_init(self, test_collection, *args, **kwargs)

    with patch.object(QdrantBase, "__init__", new=mocked_init):
        from qdrant_client import QdrantClient

        try:
            client = QdrantClient(url="http://localhost:6344", timeout=5.0)
            collections = client.get_collections().collections
            for c in collections:
                if c.name.startswith("test_behaviors_"):
                    client.delete_collection(c.name)
        except Exception:
            pass
        yield


@pytest.fixture
def real_telepathic_engine(monkeypatch):
    """Ensure we use the real LLM."""
    try:
        urllib.request.urlopen("http://localhost:11434/", timeout=2)
    except Exception:
        pytest.skip("Ollama is not running. Live E2E sim requires LLM backend.")

    engine = TelepathicEngine()
    monkeypatch.setattr(TelepathicEngine, "get_instance", lambda: engine)
    return engine


@pytest.fixture
def base_ctx(real_telepathic_engine):
    configs = MagicMock()
    configs.args.follow_percentage = "100"
    configs.args.likes_percentage = "100"
    configs.args.ignore_close_friends = True
    configs.args.scrape_profiles = False

    session_state = MagicMock(spec=SessionState)
    session_state.my_username = "testbot"
    session_state.check_limit.return_value = False

    return configs, session_state


# ==============================================================================
# E2E Tests for Plugins using REAL LLM & REAL Qdrant
# ==============================================================================


def test_e2e_profile_guard_blocks_private(base_ctx):
    """
    Testet, ob das echte LLM ein privates Profil in der XML erkennt
    und das ProfileGuardPlugin die Ausführung blockiert.
    """
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    # We load a real profile XML and inject the private account text
    with open("tests/fixtures/user_profile_dump.xml", "r") as f:
        real_xml = f.read()

    # Inject private account text near the bio
    sim.mock_xml = real_xml.replace(
        '<node index="1" text="Felix Schreiner / Content Creator"',
        '<node text="This account is private" resource-id="com.instagram.android:id/row_profile_header_empty_profile_notice_title" bounds="[100,500][980,600]" /><node index="1" text="Felix Schreiner / Content Creator"',
    )

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={"nav_graph": MagicMock()},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="target_user",
    )

    plugin = ProfileGuardPlugin()
    result = plugin.execute(ctx)

    assert result.should_skip is True
    assert result.metadata["reason"] == "private"


def test_e2e_follow_plugin_execution(base_ctx):
    """
    Testet den FollowPlugin, indem das echte LLM (TelepathicEngine)
    den "Follow" Button in der XML findet und klickt.
    """
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    # Load real profile XML
    with open("tests/fixtures/user_profile_dump.xml", "r") as f:
        real_xml = f.read()

    # Ensure it has a Follow button (replace Following with Follow if needed)
    real_xml = real_xml.replace('text="Following"', 'text="Follow"').replace(
        'content-desc="Following"', 'content-desc="Follow"'
    )
    sim.mock_xml = real_xml

    # Override human_click to modify state dynamically
    def dynamic_click(x, y):
        sim.actions_taken.append(("click", x, y))
        # Simulate Follow button
        if 32 <= x <= 326 and 950 <= y <= 1034:
            sim.mock_xml = sim.mock_xml.replace(
                'text="Follow"',
                'text="Following" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
            ).replace(
                'content-desc="Follow Felix Schreiner / Content Creator"',
                'content-desc="Following" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
            )

    sim.human_click = dynamic_click

    nav_graph = QNavGraph(sim)

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={"nav_graph": nav_graph},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="target_user",
    )

    plugin = FollowPlugin()

    # We patch sleep so the test runs fast
    with patch("GramAddict.core.behaviors.follow.sleep", autospec=True):
        result = plugin.execute(ctx)

    assert result.executed is True
    assert result.metadata["followed"] == "target_user"
    assert len(sim.actions_taken) > 0

    # Verify the LLM clicked within the bounds of the Follow button [32,950][326,1034]
    action, cx, cy = sim.actions_taken[-1]
    assert action == "click"
    assert 32 <= cx <= 326
    assert 950 <= cy <= 1034


def test_e2e_grid_like_plugin_execution(base_ctx):
    """
    Testet das GridLikePlugin. Das LLM muss einen Post aus dem Grid öffnen,
    liken und danach prüfen, ob der Like erfolgreich war.
    """
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    # Load real organic post XML
    with open("tests/fixtures/organic_post.xml", "r") as f:
        sim.mock_xml = f.read()

    # Override human_click to modify state dynamically
    def dynamic_click(x, y):
        sim.actions_taken.append(("click", x, y))
        sim.mock_xml = sim.mock_xml.replace(
            'content-desc="Like"',
            'content-desc="Liked" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
        )

    sim.human_click = dynamic_click

    nav_graph = QNavGraph(sim)

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={"nav_graph": nav_graph},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="target_user",
    )

    plugin = GridLikePlugin()

    # We need to patch the open_first_post logic to simulate that it succeeds,
    # as our XML already represents the opened post.
    with patch("GramAddict.core.behaviors.grid_like.sleep", autospec=True):
        original_do = nav_graph.do

        def side_effect_do(action, *args, **kwargs):
            if "grid" in action.lower():
                return True
            return original_do(action, *args, **kwargs)

        with patch.object(nav_graph, "do", autospec=True, side_effect=side_effect_do):
            result = plugin.execute(ctx)

    assert result.executed is True
    assert result.metadata["posts_liked"] == 1

    assert len(sim.actions_taken) > 0

    # Check if the click coordinates match the Like button [32,339][95,460]
    action, cx, cy = sim.actions_taken[-1]
    assert action == "click"
    assert 32 <= cx <= 95
    assert 339 <= cy <= 460


def test_e2e_carousel_plugin_execution(base_ctx):
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    with open("tests/fixtures/organic_post.xml", "r") as f:
        sim.mock_xml = f.read()

    # Needs to see carousel ring indicator to proceed
    # organic_post.xml already contains carousel_media_group

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="fiona.dawson",
    )

    plugin = CarouselBrowsingPlugin()

    def mock_swipe(device, start_x, end_x, y, duration_ms):
        sim.actions_taken.append(("swipe", start_x, y, end_x, y))

    with (
        patch("GramAddict.core.behaviors.carousel_browsing.sleep", autospec=True),
        patch(
            "GramAddict.core.behaviors.carousel_browsing.humanized_horizontal_swipe",
            autospec=True,
            side_effect=mock_swipe,
        ),
    ):
        result = plugin.execute(ctx)

    assert result.executed is True

    swipes = [a for a in sim.actions_taken if a[0] == "swipe"]
    assert len(swipes) > 0


def test_e2e_like_plugin_execution(base_ctx):
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    with open("tests/fixtures/organic_post.xml", "r") as f:
        sim.mock_xml = f.read()

    # Same injection as GridLikePlugin to pass ActionMemory
    def dynamic_click(x, y):
        sim.actions_taken.append(("click", x, y))
        sim.mock_xml = sim.mock_xml.replace(
            'content-desc="Like"',
            'content-desc="Liked" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
        )

    sim.human_click = dynamic_click

    nav_graph = QNavGraph(sim)
    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={"nav_graph": nav_graph},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="target_user",
    )

    plugin = LikePlugin()

    with patch("GramAddict.core.behaviors.like.random.random", autospec=True, return_value=0.0):
        result = plugin.execute(ctx)

    assert result.executed is True

    # Check if the click coordinates match the Like button [32,339][95,460]
    clicks = [a for a in sim.actions_taken if a[0] == "click"]
    action, cx, cy = clicks[-1]
    assert 32 <= cx <= 95
    assert 339 <= cy <= 460


def test_e2e_story_view_plugin_execution(base_ctx):
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    with open("tests/fixtures/user_profile_dump.xml", "r") as f:
        sim.mock_xml = f.read().replace(
            'content-desc="felixschreiner_\'s story, 0 of 0, Seen."',
            'content-desc="story ring avatar" reel_ring="true"',
        )

    def dynamic_click(x, y):
        sim.actions_taken.append(("click", x, y))
        sim.mock_xml = sim.mock_xml.replace(
            'content-desc="story ring avatar"',
            'content-desc="story ring avatar clicked" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
        )

    sim.human_click = dynamic_click

    nav_graph = QNavGraph(sim)

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={"nav_graph": nav_graph},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="target_user",
    )

    plugin = StoryViewPlugin()

    with (
        patch("GramAddict.core.behaviors.story_view.sleep", autospec=True),
        patch("GramAddict.core.behaviors.story_view.wait_for_story_loaded", autospec=True, return_value=True),
    ):
        result = plugin.execute(ctx)

    assert result.executed is True
    assert result.metadata["stories_viewed"] >= 1


def test_e2e_comment_plugin_execution(base_ctx):
    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    with open("tests/fixtures/organic_post.xml", "r") as f:
        sim.mock_xml = f.read()

    nav_graph = QNavGraph(sim)

    mock_writer = MagicMock()
    mock_writer.generate_comment.return_value = "Great post!"

    def dynamic_click(x, y):
        sim.actions_taken.append(("click", x, y))
        # If trying to open comments, change UI state to comment screen
        if "Comment" in sim.mock_xml:
            sim.mock_xml = sim.mock_xml.replace(
                'content-desc="Comment"',
                'content-desc="Comments Screen" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
            )
        # If trying to post comment, change UI state to comment posted
        elif "Comments Screen" in sim.mock_xml:
            sim.mock_xml = sim.mock_xml.replace(
                'content-desc="Comments Screen"',
                'content-desc="Comment Posted" padding="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"',
            )

    sim.human_click = dynamic_click

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={"nav_graph": nav_graph, "writer": mock_writer},
        context_xml=sim.dump_hierarchy(),
        sleep_mod=0.0,
        username="target_user",
        post_data={"caption": "Test"},
    )

    plugin = CommentPlugin()

    with patch("GramAddict.core.behaviors.comment.random.random", autospec=True, return_value=0.0):
        result = plugin.execute(ctx)

    assert result.executed is True
    assert result.metadata["text"] == "Great post!"


# ==============================================================================
# E2E Tests for ObstacleGuard — Real SAE integration
# ==============================================================================


def test_e2e_obstacle_guard_unlearn_on_fatal(base_ctx):
    """
    Reproduces the production crash: obstacle_guard calls sae.unlearn_current_state()
    without the required xml_dump argument.

    This test uses the REAL SituationalAwarenessEngine (not MagicMock) so that
    a signature mismatch causes an immediate TypeError — exactly as in production.
    """
    from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin
    from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType

    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    # Load the survey modal XML — this is a real OBSTACLE_MODAL
    with open("tests/fixtures/survey_modal.xml", "r") as f:
        sim.mock_xml = f.read()

    session_state.job_target = "Feed"

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={},
        context_xml=sim.mock_xml,
        sleep_mod=0.0,
        username="target_user",
    )
    ctx.shared_state["consecutive_marker_misses"] = 2  # Trigger the fatal path

    plugin = ObstacleGuardPlugin()

    # We use the REAL SAE instance, but mock perceive to return OBSTACLE_MODAL
    # and mock the ScreenMemoryDB to avoid Qdrant dependency.
    # The key: unlearn_current_state is NOT mocked — it must accept xml_dump.
    real_sae = create_autospec(SituationalAwarenessEngine, instance=True)
    real_sae.perceive.return_value = SituationType.OBSTACLE_MODAL

    with (
        patch(
            "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine.get_instance",
            autospec=True,
            return_value=real_sae,
        ),
        patch("GramAddict.core.behaviors.obstacle_guard.dump_ui_state", autospec=True),
        patch("GramAddict.core.behaviors.obstacle_guard.sleep", autospec=True),
    ):
        result = plugin.execute(ctx)

    assert result.executed is True
    assert result.metadata.get("return_code") == "CONTEXT_LOST"
    # The critical assertion: unlearn_current_state MUST be called with xml_dump
    real_sae.unlearn_current_state.assert_called_once()
    call_args = real_sae.unlearn_current_state.call_args
    assert (
        call_args[0][0] == sim.mock_xml
    ), "unlearn_current_state must receive the XML dump as first positional argument"


def test_e2e_obstacle_guard_dismiss_modal(base_ctx):
    """
    Tests that the ObstacleGuard correctly dismisses a survey modal
    and resets the marker miss counter.
    """
    from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin
    from GramAddict.core.situational_awareness import SituationType

    configs, session_state = base_ctx
    sim = BehaviorSimulator()

    # Start with survey modal, after back press return to feed
    with open("tests/fixtures/survey_modal.xml", "r") as f:
        survey_xml = f.read()
    with open("tests/fixtures/organic_post.xml", "r") as f:
        feed_xml = f.read()

    sim.mock_xml = survey_xml

    # After back press, switch to feed XML
    original_press = sim.press

    def mock_press(key):
        if key == "back":
            sim.mock_xml = feed_xml
        original_press(key)

    sim.press = mock_press

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={},
        context_xml=sim.mock_xml,
        sleep_mod=0.0,
        username="target_user",
    )
    ctx.shared_state["consecutive_marker_misses"] = 0

    plugin = ObstacleGuardPlugin()

    with (
        patch(
            "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine.get_instance",
            autospec=True,
        ) as mock_sae,
        patch("GramAddict.core.behaviors.obstacle_guard.sleep", autospec=True),
    ):
        mock_instance = MagicMock()
        mock_instance.perceive.return_value = SituationType.OBSTACLE_MODAL
        mock_sae.return_value = mock_instance

        result = plugin.execute(ctx)

    assert result.executed is True
    # After recovery, consecutive_marker_misses should reset (feed has markers)
    assert ctx.shared_state["consecutive_marker_misses"] == 0


# ==============================================================================
# E2E Tests for ResonanceEvaluator — Real TelepathicEngine integration
# ==============================================================================


def test_e2e_resonance_evaluator_visual_vibe_check(base_ctx):
    """
    Reproduces the production crash: resonance_evaluator calls
    tele.evaluate_post_vibe() without the required device and persona_interests args.

    Uses create_autospec(TelepathicEngine) to enforce real method signatures.
    """
    from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin
    from GramAddict.core.telepathic_engine import TelepathicEngine

    configs, session_state = base_ctx
    configs.args.visual_vibe_check_percentage = 100
    configs.args.interact_percentage = 100
    configs.args.persona_interests = ["travel", "photography"]

    sim = BehaviorSimulator()
    with open("tests/fixtures/organic_post.xml", "r") as f:
        sim.mock_xml = f.read()

    # Create an autospec'd TelepathicEngine — enforces real signatures
    mock_tele = create_autospec(TelepathicEngine, instance=True)
    mock_tele.evaluate_post_vibe.return_value = {
        "quality_score": 8,
        "matches_niche": True,
    }

    mock_resonance = MagicMock()
    mock_resonance.calculate_resonance.return_value = 0.6

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={
            "telepathic": mock_tele,
            "resonance": mock_resonance,
            "dopamine": MagicMock(),
        },
        context_xml=sim.mock_xml,
        sleep_mod=0.0,
        username="target_user",
        post_data={"caption": "Beautiful sunset"},
    )

    plugin = ResonanceEvaluatorPlugin()

    with patch("GramAddict.core.behaviors.resonance_evaluator.random.random", autospec=True, return_value=0.0):
        result = plugin.execute(ctx)

    assert result.executed is True
    assert result.should_skip is False

    # The critical assertion: evaluate_post_vibe MUST be called with device + persona_interests
    mock_tele.evaluate_post_vibe.assert_called_once_with(sim, ["travel", "photography"])

    # Verify the vibe score was integrated into the resonance score
    res_score = ctx.shared_state["res_score"]
    assert res_score > 0.5, f"Expected resonance score > 0.5 with high vibe, got {res_score}"


def test_e2e_resonance_evaluator_no_persona_interests(base_ctx):
    """
    Ensures ResonanceEvaluator gracefully handles missing persona_interests
    by defaulting to an empty list.
    """
    from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin
    from GramAddict.core.telepathic_engine import TelepathicEngine

    configs, session_state = base_ctx
    configs.args.visual_vibe_check_percentage = 100
    configs.args.interact_percentage = 100
    # Explicitly remove persona_interests to test the getattr default
    del configs.args.persona_interests

    sim = BehaviorSimulator()
    with open("tests/fixtures/organic_post.xml", "r") as f:
        sim.mock_xml = f.read()

    mock_tele = create_autospec(TelepathicEngine, instance=True)
    mock_tele.evaluate_post_vibe.return_value = {
        "quality_score": 5,
        "matches_niche": False,
    }

    mock_resonance = MagicMock()
    mock_resonance.calculate_resonance.return_value = 0.5

    ctx = BehaviorContext(
        device=sim,
        configs=configs,
        session_state=session_state,
        cognitive_stack={
            "telepathic": mock_tele,
            "resonance": mock_resonance,
            "dopamine": MagicMock(),
        },
        context_xml=sim.mock_xml,
        sleep_mod=0.0,
        username="target_user",
        post_data={"caption": "Test"},
    )

    plugin = ResonanceEvaluatorPlugin()

    with patch("GramAddict.core.behaviors.resonance_evaluator.random.random", autospec=True, return_value=0.0):
        result = plugin.execute(ctx)

    assert result.executed is True
    # Verify that evaluate_post_vibe was called with empty list as default
    mock_tele.evaluate_post_vibe.assert_called_once_with(sim, [])
