"""Unit tests for LangChain integration — mocked, no DB required."""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from piggie._integrations.langchain import (
    PiggieGraphStore,
    PiggieVectorStore,
    _validate_label,
)
from piggie.exceptions import IntegrationError, LabelError
from piggie.result import Result


@pytest.fixture()
def mock_driver():
    driver = MagicMock()
    driver.graph = "test_graph"
    return driver


@pytest.fixture()
def mock_embedding():
    emb = MagicMock()
    emb.embed_documents.return_value = [[0.1, 0.2, 0.3]]
    emb.embed_query.return_value = [0.1, 0.2, 0.3]
    return emb


# ---------------------------------------------------------------------------
# _validate_label
# ---------------------------------------------------------------------------


class TestValidateLabel:
    def test_valid_labels(self):
        assert _validate_label("Person") == "Person"
        assert _validate_label("_private") == "_private"
        assert _validate_label("Label123") == "Label123"

    def test_invalid_labels(self):
        with pytest.raises(LabelError, match="Invalid label"):
            _validate_label("123bad")
        with pytest.raises(LabelError, match="Invalid label"):
            _validate_label("has space")
        with pytest.raises(LabelError, match="Invalid label"):
            _validate_label("drop;--")
        with pytest.raises(LabelError, match="Invalid label"):
            _validate_label("")


# ---------------------------------------------------------------------------
# PiggieVectorStore
# ---------------------------------------------------------------------------


class TestPiggieVectorStore:
    def test_add_texts(self, mock_driver, mock_embedding):
        vs = PiggieVectorStore(mock_driver, mock_embedding, table="docs")
        ids = vs.add_texts(["hello world"], metadatas=[{"source": "test"}])
        assert len(ids) == 1
        mock_driver.sql.assert_called_once()
        mock_driver.commit.assert_called_once()

    def test_add_texts_custom_ids(self, mock_driver, mock_embedding):
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        ids = vs.add_texts(["hello"], ids=["my-id-1"])
        assert ids == ["my-id-1"]

    def test_similarity_search(self, mock_driver, mock_embedding):
        mock_driver.vector_search.return_value = Result(
            [("hello world", '{"source": "test"}', 0.1)],
            ["content", "metadata", "_distance"],
        )
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        docs = vs.similarity_search("hello", k=1)
        assert len(docs) == 1
        assert docs[0].page_content == "hello world"

    def test_similarity_search_with_score(self, mock_driver, mock_embedding):
        mock_driver.vector_search.return_value = Result(
            [("hello world", '{}', 0.15)],
            ["content", "metadata", "_distance"],
        )
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        results = vs.similarity_search_with_score("hello", k=1)
        assert len(results) == 1
        doc, score = results[0]
        assert doc.page_content == "hello world"
        assert score == pytest.approx(0.15)

    def test_similarity_search_by_vector(self, mock_driver, mock_embedding):
        mock_driver.vector_search.return_value = Result(
            [("hello world", '{}', 0.1)],
            ["content", "metadata", "_distance"],
        )
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        docs = vs.similarity_search_by_vector([0.1, 0.2, 0.3], k=1)
        assert len(docs) == 1
        assert docs[0].page_content == "hello world"
        # Should NOT call embed_query — vector is pre-computed
        mock_embedding.embed_query.assert_not_called()

    def test_similarity_search_by_vector_with_score(self, mock_driver, mock_embedding):
        mock_driver.vector_search.return_value = Result(
            [("found it", '{"k": "v"}', 0.05)],
            ["content", "metadata", "_distance"],
        )
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        results = vs.similarity_search_by_vector_with_score([0.1, 0.2], k=2)
        assert len(results) == 1
        doc, score = results[0]
        assert doc.page_content == "found it"
        assert doc.metadata == {"k": "v"}
        assert score == pytest.approx(0.05)

    def test_embeddings_property(self, mock_driver, mock_embedding):
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        assert vs.embeddings is mock_embedding

    def test_from_texts(self, mock_driver, mock_embedding):
        vs = PiggieVectorStore.from_texts(
            ["hello"], mock_embedding, driver=mock_driver
        )
        assert isinstance(vs, PiggieVectorStore)

    def test_from_texts_no_driver(self, mock_embedding):
        with pytest.raises(IntegrationError, match="driver is required"):
            PiggieVectorStore.from_texts(["hello"], mock_embedding)

    def test_delete_documents(self, mock_driver, mock_embedding):
        vs = PiggieVectorStore(mock_driver, mock_embedding, table="docs")
        result = vs.delete(ids=["id1", "id2"])
        assert result is True
        mock_driver.sql.assert_called_once()
        call_args = mock_driver.sql.call_args
        assert "DELETE FROM docs WHERE id IN" in call_args[0][0]
        assert call_args[0][1] == ("id1", "id2")
        mock_driver.commit.assert_called_once()

    def test_delete_no_ids(self, mock_driver, mock_embedding):
        vs = PiggieVectorStore(mock_driver, mock_embedding)
        result = vs.delete(ids=None)
        assert result is False
        mock_driver.sql.assert_not_called()

    def test_ensure_table(self, mock_driver, mock_embedding):
        PiggieVectorStore(
            mock_driver, mock_embedding,
            table="my_docs", create_table=True, embedding_dimensions=1536,
        )
        mock_driver.sql.assert_called_once()
        call_args = mock_driver.sql.call_args[0][0]
        assert "CREATE TABLE IF NOT EXISTS my_docs" in call_args
        assert "vector(1536)" in call_args
        mock_driver.commit.assert_called_once()

    def test_ensure_table_no_dimensions(self, mock_driver, mock_embedding):
        PiggieVectorStore(
            mock_driver, mock_embedding,
            table="my_docs", create_table=True,
        )
        call_args = mock_driver.sql.call_args[0][0]
        # Should have vector without dimensions
        assert "embedding vector," in call_args


# ---------------------------------------------------------------------------
# PiggieGraphStore (LangChain)
# ---------------------------------------------------------------------------


class TestPiggieGraphStore:
    def test_query(self, mock_driver):
        mock_driver.cypher.return_value = Result(
            [("Alice",)], ["name"]
        )
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        result = gs.query("MATCH (n) RETURN n.name")
        assert len(result) == 1

    def test_refresh_schema(self, mock_driver):
        mock_driver.get_schema.return_value = {
            "vertex_labels": [{"name": "Person"}],
            "edge_labels": [{"name": "KNOWS"}],
        }
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        gs.refresh_schema()
        assert "Person" in gs.get_schema
        assert "KNOWS" in gs.get_schema

    def test_get_structured_schema(self, mock_driver):
        mock_driver.get_schema.return_value = {
            "vertex_labels": [{"name": "Person", "properties": {"name": "str", "age": "int"}}],
            "edge_labels": [{"name": "KNOWS", "properties": {"since": "str"}}],
        }
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        ss = gs.get_structured_schema
        assert "Person" in ss["node_props"]
        assert "name" in ss["node_props"]["Person"]
        assert "age" in ss["node_props"]["Person"]
        assert "KNOWS" in ss["rel_props"]
        assert "KNOWS" in ss["relationships"]

    def test_get_structured_schema_no_properties(self, mock_driver):
        mock_driver.get_schema.return_value = {
            "vertex_labels": [{"name": "Thing"}],
            "edge_labels": [],
        }
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        ss = gs.get_structured_schema
        assert ss["node_props"]["Thing"] == []

    def test_add_graph_documents_parameterized(self, mock_driver):
        node = SimpleNamespace(id="alice", type="Person", properties={"name": "Alice"})
        rel = SimpleNamespace(
            source=SimpleNamespace(id="alice", type="Person"),
            target=SimpleNamespace(id="bob", type="Person"),
            type="KNOWS",
        )
        doc = SimpleNamespace(nodes=[node], relationships=[rel])
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        gs.add_graph_documents([doc])

        # Verify node MERGE uses parameterized query
        node_call = mock_driver.cypher.call_args_list[0]
        cypher_str = node_call[0][0]
        assert "%s" in cypher_str
        assert "'{" not in cypher_str  # no string interpolation of values
        params = node_call[1]["params"]
        assert "alice" in params
        assert "Alice" in params

        # Verify relationship MERGE uses parameterized query
        rel_call = mock_driver.cypher.call_args_list[1]
        cypher_str = rel_call[0][0]
        assert "%s" in cypher_str
        params = rel_call[1]["params"]
        assert params == ("alice", "bob")

        mock_driver.commit.assert_called_once()

    def test_add_graph_documents_label_validation(self, mock_driver):
        """Reject nodes with injection-like label names."""
        node = SimpleNamespace(id="x", type="Bad;Label", properties={})
        doc = SimpleNamespace(nodes=[node], relationships=[])
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        with pytest.raises(IntegrationError, match="Invalid label"):
            gs.add_graph_documents([doc])

    def test_add_graph_documents_rel_label_validation(self, mock_driver):
        """Reject relationships with injection-like type names."""
        src = SimpleNamespace(id="a", type="Entity")
        tgt = SimpleNamespace(id="b", type="Entity")
        rel = SimpleNamespace(source=src, target=tgt, type="DROP TABLE")
        doc = SimpleNamespace(nodes=[], relationships=[rel])
        gs = PiggieGraphStore(mock_driver, graph="test_graph")
        with pytest.raises(IntegrationError, match="Invalid label"):
            gs.add_graph_documents([doc])
