import time
from unittest.mock import MagicMock, patch

import pytest

# Inject mock qdrant_client
from GramAddict.core.qdrant_memory import (
    BannedPathsDB,
    CommentMemoryDB,
    ContentMemoryDB,
    DMMemoryDB,
    HeuristicMemoryDB,
    NavigationMemoryDB,
    ParasocialCRMDB,
    PersonaMemoryDB,
    QdrantBase,
    UIMemoryDB,
)


@pytest.fixture(autouse=True)
def mock_qdrant():
    with patch("GramAddict.core.qdrant_memory.QdrantClient") as mq:
        yield mq


def test_qdrant_base(mock_qdrant):
    mock_client = MagicMock()
    mock_qdrant.return_value = mock_client

    # Missing collection creation
    mock_client.collection_exists.return_value = False
    base = QdrantBase("test_collection", vector_size=4)
    mock_client.create_collection.assert_called()

    # Dimension mismatch
    dim_mock = MagicMock()
    dim_mock.config.params.vectors.size = 2
    mock_client.get_collection.return_value = dim_mock
    mock_client.collection_exists.return_value = True
    base = QdrantBase("test_collection", vector_size=4)
    # Should delete and recreate
    mock_client.delete_collection.assert_called()
    assert mock_client.create_collection.call_count == 2

    # Upsert & Search
    base.upsert_point("seed", {"a": 1})
    mock_client.upsert.assert_called()
    base.search_points([0.0] * 4)
    mock_client.search.assert_called()


def test_qdrant_base_embeddings(mock_qdrant):
    base = QdrantBase("x", 4)
    with patch("requests.post") as mock_post, patch("GramAddict.core.config.Config"):
        # Ollama style
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"embedding": [0.1, 0.2]}
        mock_post.return_value = resp
        assert base._get_embedding("hi") == [0.1, 0.2]

        # OpenAI style
        resp.json.return_value = {"data": [{"embedding": [0.3]}]}
        assert base._get_embedding("hi") == [0.3]

        # Failure
        mock_post.side_effect = Exception("failed")
        assert base._get_embedding("hi") is None


def test_heuristic_memory(mock_qdrant):
    with patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb:
        m_emb.return_value = [0.0] * 1536
        db = HeuristicMemoryDB()

        # learn heuristics
        db.cache_heuristic("find_button", {"bounds": [0, 0, 10, 10]})

        # mock query_points
        pt = MagicMock()
        pt.payload = {"rule": "{'bounds': [0, 0, 10, 10]}", "rule_type": "regex"}
        pt.score = 1.0
        mock_result = MagicMock()
        mock_result.points = [pt]
        db.client.query_points.return_value = mock_result

        res = db.fetch_heuristic("find_button")
        assert res is not None


def test_ui_memory_db(mock_qdrant):
    with patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb:
        m_emb.return_value = [0.0] * 1536
        db = UIMemoryDB()
        db.store_memory("home", "<xml/>", {"res": 1})

        pt = MagicMock()
        pt.payload = {
            "solution": {"res": 1},
            "structural_signature": db._create_structural_signature("<xml/>"),
            "confidence": 0.8,
        }
        pt.score = 1.0
        mock_result = MagicMock()
        mock_result.points = [pt]
        db.client.query_points.return_value = mock_result

        assert db.retrieve_memory("home", "<xml/>") == {"res": 1}

        # confidence
        db.client.query_points.return_value = mock_result
        db.boost_confidence("home")
        db.decay_confidence("home")
        db.purge_stale_entries()


def test_content_and_comments(mock_qdrant):
    with patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb:
        m_emb.return_value = [0.0] * 1536

        # Comments
        cdb = CommentMemoryDB()
        cdb.store_comment("nice", "positive", "user")
        cdb.client.upsert.assert_called()

        pt = MagicMock()
        pt.payload = {"text": "nice"}
        pt.score = 1.0
        mock_result = MagicMock()
        mock_result.points = [pt]
        cdb.client.query_points.return_value = mock_result
        res = cdb.get_relevant_comments("post")
        assert len(res) == 1

        # Content
        cndb = ContentMemoryDB()
        cndb.store_evaluation("nice pic", "POSITIVE", "good vibe")
        # pt payload
        pt.payload = {"classification": "POSITIVE", "reason": "ok"}
        pt.score = 1.0
        mock_result = MagicMock()
        mock_result.points = [pt]
        cndb.client.query_points.return_value = mock_result
        assert cndb.get_cached_evaluation("nice pic") is not None


def test_banned_paths_db(mock_qdrant):
    mock_client = MagicMock()
    mock_qdrant.return_value = mock_client

    # Mocking scroll to return some expired and some active
    exp_pt = MagicMock()
    exp_pt.id = "exp"
    exp_pt.payload = {"banned_at": 100, "goal_hash": "a", "element_id": "e1"}  # VERY OLD

    act_pt = MagicMock()
    act_pt.id = "act"
    act_pt.payload = {"banned_at": time.time(), "goal_hash": "b", "element_id": "e2"}

    mock_client.scroll.return_value = ([exp_pt, act_pt], None)

    db = BannedPathsDB()

    # Should have run clean up for exp_pt, and loaded act_pt
    mock_client.delete.assert_called()
    assert len(db._banned) == 1

    # ban new
    db.ban("My Goal", "ui_123", "Not working")
    mock_client.upsert.assert_called()

    # check
    assert db.is_banned("My Goal", "ui_123")


def test_navigation_memory_db(mock_qdrant):
    db = NavigationMemoryDB()
    with patch("GramAddict.core.qdrant_memory.uuid.uuid4", return_value="1234"):
        db.store_transition("Feed", "click_home", "Home")
        db.client.upsert.assert_called()

    pt = MagicMock()
    pt.payload = {"from": "Feed", "action": "click_home", "to": "Home"}
    db.client.scroll.return_value = ([pt], None)
    res = db.get_all_transitions()
    assert res.get("Feed") == {"transitions": {"click_home": "Home"}}


def test_persona_memory_db(mock_qdrant):
    with patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb:
        m_emb.return_value = [0.0] * 1536
        db = PersonaMemoryDB()

        db.store_persona_insight("likes", "Loves tech")
        pt = MagicMock()
        pt.payload = {"category": "likes", "insight": "Loves tech"}
        db.client.scroll.return_value = ([pt], None)
        assert "Loves tech" in db.get_persona_context("likes")


def test_crm_db(mock_qdrant):
    with (
        patch("GramAddict.core.qdrant_memory.ParasocialCRMDB.is_connected", new_callable=MagicMock, return_value=True),
        patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb,
    ):
        m_emb.return_value = [0.0] * 1536
        db = ParasocialCRMDB()
        pt = MagicMock()
        pt.payload = {"stage": 1, "intent_history": ["LIKE"], "last_interaction": 100}
        # ParasocialCRMDB uses scroll
        db.client.scroll.return_value = ([pt], None)

        res = db.get_relationship_stage("user")
        assert res["stage"] == 1

        db.log_interaction("user", "COMMENT")
        db.client.upsert.assert_called()

        db.log_generated_comment("user", "hi")
        db.log_profile_context("user", "Tech dev")

        # Simulate DB state updated
        pt.payload["bio"] = "Tech dev"
        assert "Tech dev" in db.get_conversation_context("user")


def test_dm_history_db(mock_qdrant):
    with patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb:
        m_emb.return_value = [0.0] * 1536
        db = DMMemoryDB()
        db.log_sent_dm("user", "hi", "bio", [])
        db.client.upsert.assert_called()

        pt = MagicMock()
        pt.payload = {"target_username": "user", "message": "hi", "score": 0.9}

        mock_result = MagicMock()
        mock_result.points = [pt]
        db.client.query_points.return_value = mock_result
        db.client.scroll.return_value = ([pt], None)

        pending = db.get_pending_dms()
        assert len(pending) == 1

        db.update_dm_score("123", 1.0)
        db.client.set_payload.assert_called()

        best = db.get_best_performing_dms()
        assert len(best) == 1


def test_unhappy_paths(mock_qdrant):
    mock_client = MagicMock()
    mock_qdrant.return_value = mock_client
    with patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb:
        m_emb.return_value = [0.0] * 1536

        # Test 1: Exception on query_points
        db = UIMemoryDB()
        db.client.query_points.side_effect = Exception("failed")
        assert db.retrieve_memory("home", "<xml/>") is None

        # Test 2: Exception on upsert
        db.client.upsert.side_effect = Exception("failed")
        db.store_memory("home", "<xml/>", {"res": 1})  # shouldn't crash

        # _adjust_confidence coverage
        db.client.retrieve.return_value = []
        db.boost_confidence("home")  # handles empty retrieve

        pt = MagicMock()
        pt.payload = {"confidence": 0.5}
        db.client.retrieve.return_value = [pt]
        db.decay_confidence("home", amount=1.0)  # falls below 0.1, calls delete
        db.client.delete.assert_called()

        # purge stale entries
        stale_pt = MagicMock()
        stale_pt.payload = {"confidence": 0.4, "stored_at": 100}
        db.client.scroll.return_value = ([stale_pt], None)
        db.purge_stale_entries()
        db.client.delete.assert_called()

        # fetch heuristic fail
        db = HeuristicMemoryDB()
        db.client.query_points.side_effect = Exception("failed")
        assert db.fetch_heuristic("button") is None

        cndb = ContentMemoryDB()
        cndb.client.query_points.side_effect = Exception("failed")
        assert cndb.get_cached_evaluation("pic") is None

        # get_similar_examples
        db.client.query_points.side_effect = None
        pt.payload = {"description": "hello", "classification": "A", "reason": "B"}
        mock_result = MagicMock()
        mock_result.points = [pt]
        cndb.client.query_points.return_value = mock_result
        res = cndb.get_similar_examples("pic")
        assert len(res) == 1
        assert res[0]["classification"] == "A"


def test_disconnected_state(mock_qdrant):
    with (
        patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=MagicMock, return_value=False),
        patch("GramAddict.core.qdrant_memory.QdrantBase._get_embedding") as m_emb,
    ):
        m_emb.return_value = None
        db = UIMemoryDB()
        assert db.retrieve_memory("home", "<xml/>") is None
        db.store_memory("home", "<xml/>", {})
        db._adjust_confidence("home", 0.1)

        cdb = CommentMemoryDB()
        assert cdb.get_relevant_comments("post") == []
        cdb.store_comment("p", "a", "u")

        crm = ParasocialCRMDB()
        assert crm.get_relationship_stage("user")["stage"] == 0
        crm.log_profile_context("u", "b")
        crm.log_interaction("u", "intent")
        crm.log_generated_comment("u", "t")

        dm = DMMemoryDB()
        dm.update_dm_score("123", 1.0)
        assert dm.get_pending_dms() == []
        assert dm.get_best_performing_dms() == []
        dm.log_sent_dm("a", "b", "c", [])

        cndb = ContentMemoryDB()
        assert cndb.get_similar_examples("hello") == []
        assert cndb.get_cached_evaluation("hi") is None
        cndb.store_evaluation("a", "b", "c")

        ndb = NavigationMemoryDB()
        assert ndb.get_all_transitions() == {}
        ndb.store_transition("a", "b", "c")

        pdb = PersonaMemoryDB()
        assert pdb.get_persona_context("C") == ""
        pdb.store_persona_insight("a", "b")

        hdb = HeuristicMemoryDB()
        assert hdb.fetch_heuristic("H") is None
        hdb.cache_heuristic("a", {})
