import xml.etree.ElementTree as ET
from unittest.mock import MagicMock, patch

import pytest

from GramAddict.core.device_facade import DeviceFacade
from GramAddict.core.goap import GoalExecutor
from GramAddict.core.qdrant_memory import QdrantBase
from GramAddict.core.telepathic_engine import TelepathicEngine


class AndroidEnvironmentSimulator(DeviceFacade):
    def __init__(self, device_id="sim", app_id="com.instagram.android", args=None):
        self.device_id = device_id
        self.app_id = app_id
        self.args = args
        self.deviceV2 = MagicMock()
        self.deviceV2.info = {"displayWidth": 1080, "displayHeight": 2400, "screenOn": True}

        self.state_stack = ["home_feed"]
        self.state_files = {
            "home_feed": "tests/fixtures/home_feed_with_ad.xml",
            "explore_grid": "tests/fixtures/explore_feed_dump.xml",
            "post_detail": "tests/fixtures/organic_post.xml",
            "user_profile": "tests/fixtures/user_profile_dump.xml",
        }

    def _current_state(self):
        return self.state_stack[-1]

    def dump_hierarchy(self):
        current = self._current_state()
        filepath = self.state_files[current]
        with open(filepath, "r", encoding="utf-8") as f:
            data = f.read()
        print(f"📱 [Simulator] dump_hierarchy returning state: {current} (length: {len(data)})")
        return data

    def _parse_bounds(self, bounds_str):
        import re

        match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds_str)
        if match:
            return [int(x) for x in match.groups()]
        return None

    def human_click(self, x, y):
        # Simulate click translation to next state
        xml_data = self.dump_hierarchy()
        root = ET.fromstring(xml_data)

        clicked_nodes = []
        for node in root.iter("node"):
            bounds_str = node.attrib.get("bounds", "")
            bounds = self._parse_bounds(bounds_str)
            if bounds:
                x1, y1, x2, y2 = bounds
                if x1 <= x <= x2 and y1 <= y <= y2:
                    area = (x2 - x1) * (y2 - y1)
                    clicked_nodes.append((area, node))

        if not clicked_nodes:
            return

        clicked_nodes.sort(key=lambda item: item[0])

        for _, target in clicked_nodes:
            content_desc = target.attrib.get("content-desc", "") or ""
            res_id = target.attrib.get("resource-id", "") or ""
            target.attrib.get("text", "") or ""

            current = self._current_state()
            if current == "home_feed":
                if "Search and explore" in content_desc or "search_tab" in res_id:
                    print(f"📱 [Simulator] Clicked ({x}, {y}) on {res_id}. Transition: home_feed -> explore_grid")
                    self.state_stack.append("explore_grid")
                    return
            elif current == "explore_grid":
                # In explore, anything the VLM clicks that has an image or button is likely a post
                if "image_button" in res_id or "container" in res_id or target.attrib.get("clickable") == "true":
                    print(f"📱 [Simulator] Clicked ({x}, {y}) on {res_id}. Transition: explore_grid -> post_detail")
                    self.state_stack.append("post_detail")
                    return
            elif current == "post_detail":
                # Allow clicking either the post author or the comment author (both go to user_profile)
                if 100 < x < 800 and 300 < y < 900:
                    print(f"📱 [Simulator] Clicked ({x}, {y}) on {res_id}. Transition: post_detail -> user_profile")
                    self.state_stack.append("user_profile")
                    return

        # If we get here, no transition happened
        for _, target in clicked_nodes:
            print(
                f"📱 [Simulator] Click ({x}, {y}) fell through on: {target.attrib.get('resource-id')} / text={target.attrib.get('text')}"
            )
        if not clicked_nodes:
            print(f"📱 [Simulator] Click ({x}, {y}) fell outside ALL elements!")

    def click(self, x=None, y=None, obj=None):
        if x is not None and y is not None:
            self.human_click(x, y)
        elif obj and isinstance(obj, dict) and "x" in obj:
            self.human_click(obj["x"], obj["y"])

    def press(self, key):
        if key == "back":
            if len(self.state_stack) > 1:
                old_state = self.state_stack.pop()
                print(f"📱 [Simulator] Back pressed. State Transition: {old_state} -> {self._current_state()}")
            else:
                print("📱 [Simulator] Back pressed at root state.")

    def _get_current_app(self):
        return self.app_id

    def get_info(self):
        return self.deviceV2.info

    def wake_up(self):
        pass

    def unlock(self):
        pass

    def shell(self, cmd):
        return ""

    def swipe(self, sx, sy, ex, ey, duration=None):
        print(f"📱 [Simulator] Swiped ({sx}, {sy}) -> ({ex}, {ey})")

    def human_swipe(self, sx, sy, ex, ey, duration=None):
        print(f"📱 [Simulator] Swiped ({sx}, {sy}) -> ({ex}, {ey})")

    @property
    def info(self):
        return self.deviceV2.info


@pytest.fixture(autouse=True)
def e2e_qdrant_mock(request, monkeypatch):
    """Override the global e2e_qdrant_mock fixture to allow REAL Qdrant in this module."""
    yield None


@pytest.fixture(autouse=True)
def setup_qdrant_isolation():
    """Prefix all Qdrant collections with test_sim_ so we don't pollute live data."""
    original_init = QdrantBase.__init__

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

    with patch.object(QdrantBase, "__init__", new=mocked_init):
        # We aggressively wipe ALL test collections before running the test!
        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_sim_"):
                    client.delete_collection(c.name)
        except Exception:
            pass
        yield


def test_full_autonomous_sim_loop(monkeypatch):
    """
    This test runs the real GoalExecutor with the real TelepathicEngine (VLM)
    and real Qdrant (sandboxed via prefix) against a simulated Android environment.
    """
    import urllib.request

    try:
        urllib.request.urlopen("http://localhost:11434/", timeout=2)
    except Exception:
        pytest.skip("Ollama is not running. Live E2E sim requires LLM backend.")

    # 1. Create Simulator
    sim_device = AndroidEnvironmentSimulator()

    # 2. Patch TelepathicEngine to NOT be mocked by conftest
    engine = TelepathicEngine()
    monkeypatch.setattr(TelepathicEngine, "get_instance", lambda: engine)

    # 3. Create context and GoalExecutor
    from GramAddict.core.config import Config

    if not hasattr(Config(), "args"):
        Config().args = MagicMock()
    Config().args.use_nav_memory = True
    Config().args.use_semantic_memory = True

    executor = GoalExecutor(sim_device, bot_username="testbot")

    # 4. Start an autonomous loop: We want to reach an organic post from the home feed
    assert sim_device._current_state() == "home_feed"

    success = executor.achieve("open post", max_steps=10)
    assert success is True

    # The VLM should have figured out:
    # 1. Tap explore tab -> switches to "explore_grid"
    # 2. Tap grid item -> switches to "post_detail"
    assert sim_device._current_state() == "post_detail"

    # 5. Let's do another intent: view the user profile
    success = executor.achieve("open post author profile", max_steps=5)
    assert success is True
    assert sim_device._current_state() == "user_profile"

    # 6. Now go back to the post
    success = executor.achieve("open post", max_steps=5)
    assert success is True
    assert sim_device._current_state() == "post_detail"

    # 7. Check Qdrant Memory is actually populated
    # We should have stored the state transitions in the goap_paths collection
    from GramAddict.core.goap import PathMemory

    nav_db = PathMemory("testbot")
    # verify at least some nodes exist
    count = nav_db._db.client.count(nav_db._db.collection_name).count
    assert count > 0, "Qdrant memory should have learned the paths!"
