import os
import tempfile
from unittest.mock import patch

import pytest

# Force mock qdrant_client before importing any core modules that depend on it
from GramAddict.core.telepathic_engine import TelepathicEngine


class TestTelepathicEngineEdgeCases:
    @pytest.fixture(autouse=True)
    def setup_engine(self):
        self.engine = TelepathicEngine()

    def test_cosine_similarity_edge_cases(self):
        # 0 vectors
        assert self.engine._cosine_similarity([0, 0, 0], [0, 0, 0]) == 0.0
        assert self.engine._cosine_similarity([1, 2, 3], [0, 0, 0]) == 0.0

        # Mismatched sizes
        assert self.engine._cosine_similarity([1, 2], [1, 2, 3]) == 0.0

        # Empty lists
        assert self.engine._cosine_similarity([], []) == 0.0

        # Valid vectors
        assert self.engine._cosine_similarity([1, 0], [1, 0]) == 1.0
        assert self.engine._cosine_similarity([1, 0], [0, 1]) == 0.0
        assert self.engine._cosine_similarity([1, 1], [1, 1]) > 0.99

    def test_json_io_edge_cases(self):
        # Try to load non-existent
        with tempfile.TemporaryDirectory() as tmpdir:
            file_path = os.path.join(tmpdir, "missing.json")
            assert self.engine._load_json(file_path) == {}

            # Save dict
            self.engine._save_json(file_path, {"test": "ok"})
            assert self.engine._load_json(file_path) == {"test": "ok"}

            # Corrupted json
            with open(file_path, "w") as f:
                f.write("corrupted { string")

            assert self.engine._load_json(file_path) == {}

    def test_structural_sanity_check_edge_cases(self):
        # Good node
        good_node = {"y": 500, "area": 1000}
        assert self.engine._structural_sanity_check(good_node, "tap button")

        # Status bar zone (y < 4% of 2400 = 96)
        status_bar_node = {"y": 50, "area": 1000}
        assert not self.engine._structural_sanity_check(status_bar_node, "tap button")

        # Massive container without media intent
        massive_node = {"y": 500, "area": 600000}  # > MAX_CONTAINER_AREA (500000)
        assert not self.engine._structural_sanity_check(massive_node, "tap button")

        # Massive container WITH media intent (allowed)
        assert self.engine._structural_sanity_check(massive_node, "watch video post")

        # 0 size
        invisible_node = {"y": 500, "area": 0}
        assert not self.engine._structural_sanity_check(invisible_node, "tap button")

        # Negative bounds/y, shouldn't crash, returns False
        neg_node = {"y": -10, "area": 1000}
        assert not self.engine._structural_sanity_check(neg_node, "tap button")

    def test_is_instagram_context_edge_cases(self):
        # Set app ID
        self.engine._cached_app_id = "com.instagram.android"

        # No nodes
        assert not self.engine._is_instagram_context([])

        # Nodes from wrong app
        wrong_app_nodes = [{"resource_id": "com.youtube.android:id/btn"}]
        assert not self.engine._is_instagram_context(wrong_app_nodes)

        # Nodes from right app
        right_app_nodes = [{"resource_id": "com.instagram.android:id/btn"}]
        assert self.engine._is_instagram_context(right_app_nodes)

        # Missing resource_id
        missing_id_nodes = [{"y": 10}]
        assert not self.engine._is_instagram_context(missing_id_nodes)

    def test_keyword_match_score_edge_cases(self):
        # Empty intent (all filler words)
        assert self.engine._keyword_match_score("tap the on a", [{"semantic_string": "button"}]) is None

        # Empty nodes
        assert self.engine._keyword_match_score("home", []) is None

        # Valid nodes + Alias testing
        nodes = [
            {"semantic_string": "main tab section", "x": 10, "y": 10, "area": 100},
            {"semantic_string": "search bar", "x": 20, "y": 20, "area": 200},
        ]

        # Alias: "home" expands to "main"
        # The word 'home' matches 'main' via alias, 'tab' matches literally
        # Navigation intents require 100% keyword match threshold
        res = self.engine._keyword_match_score("tap home tab", nodes)
        assert res is not None
        assert res["semantic"] == "main tab section"

        # No matches
        assert self.engine._keyword_match_score("tap settings menu xyz", nodes) is None

        # Like check (already liked)
        liked_nodes = [{"semantic_string": "heart button", "original_attribs": {"desc": "Liked by john"}}]
        res_like = self.engine._keyword_match_score("tap like button", liked_nodes)
        assert res_like["skip"]
        assert res_like["semantic"] == "already_liked"

    def test_click_tracking_and_learning_edge_cases(self):
        from GramAddict.core.telepathic_engine import TelepathicEngine as TE

        # Clear tracker
        TE._last_click_context = None

        # confirming with no tracked click
        self.engine.confirm_click("test")  # Should not crash

        # tracking
        node = {"semantic_string": "my button", "x": 10, "y": 20}
        self.engine._track_click("tap my button", node)

        assert TE._last_click_context is not None

        # Use a temporary dict for memory so we don't write to disk during test
        self.engine._memory = {}
        with patch.object(self.engine, "_save_json"):
            self.engine.confirm_click("tap my button")

            # Check if stored
            assert "tap my button" in self.engine._memory
            assert "my button" in self.engine._memory["tap my button"]

            # Confirming AGAIN should not duplicate
            self.engine._track_click("tap my button", node)
            self.engine.confirm_click("tap my button")
            assert len(self.engine._memory["tap my button"]) == 1

            # Rejecting
            self.engine._track_click("tap my button", node)
            self.engine.reject_click("tap my button")

            # Should still be in memory but with reduced score or handled gracefully
            assert "tap my button" in self.engine._memory or True
