"""Tests for LociDB core — node, edge, store, scorer, and db."""

import math
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pytest

from locidb.core.node import MemoryNode
from locidb.core.edge import MemoryEdge
from locidb.core.store import MemoryStore, _cosine_similarity
from locidb.core.scorer import compute_importance, batch_score


# ── MemoryNode ─────────────────────────────────────────────────────────

class TestMemoryNode:
    def test_defaults(self):
        node = MemoryNode(content="hello world")
        assert node.content == "hello world"
        assert node.importance == 0.5
        assert node.summary_level == 0
        assert node.access_count == 0
        assert node.context_scope == "global"
        assert node.embedding == []
        assert len(node.id) == 32  # hex UUID

    def test_touch(self):
        node = MemoryNode(content="test")
        assert node.access_count == 0
        assert node.last_accessed is None
        node.touch()
        assert node.access_count == 1
        assert node.last_accessed is not None

    def test_entity_tags(self):
        node = MemoryNode(content="test", entity_tags=["Dropzi", "routing"])
        assert "Dropzi" in node.entity_tags
        assert len(node.entity_tags) == 2


# ── MemoryEdge ─────────────────────────────────────────────────────────

class TestMemoryEdge:
    def test_defaults(self):
        edge = MemoryEdge(source_id="a", target_id="b", relationship_type="owns")
        assert edge.source_id == "a"
        assert edge.target_id == "b"
        assert edge.relationship_type == "owns"
        assert edge.strength == 1.0

    def test_custom_strength(self):
        edge = MemoryEdge(
            source_id="a", target_id="b",
            relationship_type="related_to", strength=0.7,
        )
        assert edge.strength == 0.7


# ── Cosine Similarity ─────────────────────────────────────────────────

class TestCosineSimilarity:
    def test_identical_vectors(self):
        import numpy as np
        v = np.array([1.0, 2.0, 3.0])
        assert abs(_cosine_similarity(v, v) - 1.0) < 1e-6

    def test_orthogonal_vectors(self):
        import numpy as np
        a = np.array([1.0, 0.0])
        b = np.array([0.0, 1.0])
        assert abs(_cosine_similarity(a, b)) < 1e-6

    def test_zero_vector(self):
        import numpy as np
        a = np.array([0.0, 0.0])
        b = np.array([1.0, 2.0])
        assert _cosine_similarity(a, b) == 0.0


# ── MemoryStore ────────────────────────────────────────────────────────

class TestMemoryStore:
    @pytest.fixture
    def store(self, tmp_path):
        s = MemoryStore(tmp_path / "test.loci")
        yield s
        s.close()

    def test_save_and_get_node(self, store):
        node = MemoryNode(
            content="test content",
            embedding=[0.1, 0.2, 0.3],
            entity_tags=["A", "B"],
        )
        store.save_node(node)
        retrieved = store.get_node(node.id)
        assert retrieved is not None
        assert retrieved.content == "test content"
        assert retrieved.embedding == [0.1, 0.2, 0.3]
        assert retrieved.entity_tags == ["A", "B"]

    def test_delete_node(self, store):
        node = MemoryNode(content="to delete", embedding=[1.0])
        store.save_node(node)
        store.delete_node(node.id)
        assert store.get_node(node.id) is None

    def test_list_nodes(self, store):
        for i in range(5):
            store.save_node(MemoryNode(content=f"node {i}", embedding=[float(i)]))
        nodes = store.list_nodes(limit=3)
        assert len(nodes) == 3

    def test_count_nodes(self, store):
        assert store.count_nodes() == 0
        store.save_node(MemoryNode(content="one", embedding=[1.0]))
        assert store.count_nodes() == 1

    def test_save_and_get_edges(self, store):
        n1 = MemoryNode(content="a", embedding=[1.0])
        n2 = MemoryNode(content="b", embedding=[2.0])
        store.save_node(n1)
        store.save_node(n2)
        edge = MemoryEdge(source_id=n1.id, target_id=n2.id, relationship_type="owns")
        store.save_edge(edge)
        edges = store.get_edges(n1.id, direction="outgoing")
        assert len(edges) == 1
        assert edges[0].target_id == n2.id

    def test_vector_search(self, store):
        store.save_node(MemoryNode(content="cat", embedding=[1.0, 0.0, 0.0]))
        store.save_node(MemoryNode(content="dog", embedding=[0.9, 0.1, 0.0]))
        store.save_node(MemoryNode(content="car", embedding=[0.0, 0.0, 1.0]))
        results = store.vector_search([1.0, 0.0, 0.0], top_k=2)
        assert len(results) == 2
        assert results[0][0].content == "cat"
        assert results[0][1] > results[1][1]

    def test_stats(self, store):
        store.save_node(MemoryNode(content="x", embedding=[1.0]))
        s = store.stats()
        assert s["node_count"] == 1
        assert s["edge_count"] == 0

    def test_traverse(self, store):
        n1 = MemoryNode(content="root", embedding=[1.0], entity_tags=["start"])
        n2 = MemoryNode(content="child", embedding=[2.0])
        store.save_node(n1)
        store.save_node(n2)
        store.save_edge(MemoryEdge(source_id=n1.id, target_id=n2.id, relationship_type="owns"))
        results = store.traverse("start", max_depth=2)
        assert len(results) == 2
        assert results[0]["depth"] == 0
        assert results[1]["depth"] == 1


# ── Scorer ─────────────────────────────────────────────────────────────

class TestScorer:
    def test_fresh_node_high_recency(self):
        node = MemoryNode(content="fresh", entity_tags=["A"])
        score = compute_importance(node, query_entities=["A"])
        # Fresh node + perfect entity match → high score
        assert score > 0.7

    def test_old_node_low_recency(self):
        old_time = datetime.now(timezone.utc) - timedelta(days=60)
        node = MemoryNode(content="old", timestamp=old_time, entity_tags=["A"])
        score = compute_importance(node, query_entities=["A"])
        # Old node → lower recency component
        assert score < 0.6

    def test_no_entity_overlap(self):
        node = MemoryNode(content="test", entity_tags=["X"])
        score = compute_importance(node, query_entities=["Y"])
        # No entity match → entity_relevance = 0
        assert score < 0.5

    def test_high_usage_frequency(self):
        node = MemoryNode(content="popular", access_count=20)
        score = compute_importance(node)
        # usage_frequency = min(20/10, 1.0) = 1.0 → contributes 0.2
        assert score >= 0.2

    def test_batch_score(self):
        nodes = [MemoryNode(content=f"n{i}") for i in range(3)]
        scores = batch_score(nodes, query_entities=["test"])
        assert len(scores) == 3
        assert all(0 <= s <= 1 for s in scores)
