"""Tests for the FactQuery builder.

Verifies that the builder produces the expected SQLAlchemy ``Select``
statements for every factory method, filter, ordering, and combination.
No database is involved — tests inspect the compiled SQL string or the
ORM-level clauses on the Select object.

Source: ``.vibeflow/specs/fact-query-abstraction-part-1.md``.
"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime

from sqlalchemy import Select
from sqlalchemy.dialects import postgresql

from arandu._fact_query import FactQuery

AGENT = "agent-1"


def _compile(stmt: Select) -> str:  # type: ignore[type-arg]
    """Compile a Select against the postgresql dialect for string inspection."""
    return str(stmt.compile(dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}))


# ---------------------------------------------------------------------------
# Factory methods
# ---------------------------------------------------------------------------


class TestFactoryMethods:
    """Factory methods establish the base filter."""

    def test_active_applies_valid_to_null(self) -> None:
        stmt = FactQuery.active(AGENT).build()
        sql = _compile(stmt)
        assert "memory_facts.valid_to IS NULL" in sql
        assert f"memory_facts.agent_id = '{AGENT}'" in sql

    def test_all_versions_does_not_apply_valid_to(self) -> None:
        stmt = FactQuery.all_versions(AGENT).build()
        sql = _compile(stmt)
        assert "valid_to IS NULL" not in sql
        assert f"memory_facts.agent_id = '{AGENT}'" in sql

    def test_active_produces_select_of_memory_fact(self) -> None:
        stmt = FactQuery.active(AGENT).build()
        assert isinstance(stmt, Select)
        # Selected columns belong to the MemoryFact table
        sql = _compile(stmt)
        assert "FROM memory_facts" in sql


# ---------------------------------------------------------------------------
# Filters
# ---------------------------------------------------------------------------


class TestFilters:
    """Each filter method adds the expected WHERE clause."""

    def test_with_min_confidence(self) -> None:
        stmt = FactQuery.active(AGENT).with_min_confidence(0.7).build()
        sql = _compile(stmt)
        assert "memory_facts.confidence >= 0.7" in sql

    def test_with_embedding(self) -> None:
        stmt = FactQuery.active(AGENT).with_embedding().build()
        sql = _compile(stmt)
        assert "memory_facts.embedding_vec IS NOT NULL" in sql

    def test_for_entity_single(self) -> None:
        stmt = FactQuery.active(AGENT).for_entity("person:pedro").build()
        sql = _compile(stmt)
        assert "EXISTS" in sql
        assert "memory_fact_entity_links" in sql
        assert "'person:pedro'" in sql

    def test_for_entities_multiple(self) -> None:
        stmt = FactQuery.active(AGENT).for_entities(["person:pedro", "person:ana"]).build()
        sql = _compile(stmt)
        assert "EXISTS" in sql
        assert "memory_fact_entity_links" in sql
        assert "'person:pedro'" in sql
        assert "'person:ana'" in sql

    def test_for_entities_empty_list_matches_nothing(self) -> None:
        """Empty entity list produces a filter that matches no rows."""
        stmt = FactQuery.active(AGENT).for_entities([]).build()
        sql = _compile(stmt)
        # No EXISTS subquery — a sentinel zero-UUID id filter is applied
        assert "EXISTS" not in sql
        assert "00000000-0000-0000-0000-000000000000" in sql

    def test_for_cluster(self) -> None:
        cluster_id = uuid.UUID("11111111-2222-3333-4444-555555555555")
        stmt = FactQuery.active(AGENT).for_cluster(cluster_id).build()
        sql = _compile(stmt)
        assert str(cluster_id) in sql
        assert "cluster_id" in sql

    def test_within_temporal_window_both_bounds(self) -> None:
        start = datetime(2026, 1, 1, tzinfo=UTC)
        end = datetime(2026, 6, 1, tzinfo=UTC)
        stmt = FactQuery.active(AGENT).within_temporal_window(start, end).build()
        sql = _compile(stmt)
        assert "valid_from >=" in sql
        assert "valid_from <=" in sql

    def test_within_temporal_window_only_start(self) -> None:
        start = datetime(2026, 1, 1, tzinfo=UTC)
        stmt = FactQuery.active(AGENT).within_temporal_window(start, None).build()
        sql = _compile(stmt)
        assert "valid_from >=" in sql
        assert "valid_from <=" not in sql

    def test_within_temporal_window_none_none_is_noop(self) -> None:
        stmt = FactQuery.active(AGENT).within_temporal_window(None, None).build()
        sql = _compile(stmt)
        assert "valid_from >=" not in sql
        assert "valid_from <=" not in sql

    def test_excluding_ids(self) -> None:
        fact_id_1 = uuid.UUID("aaaaaaaa-0000-0000-0000-000000000001")
        fact_id_2 = uuid.UUID("bbbbbbbb-0000-0000-0000-000000000002")
        stmt = FactQuery.active(AGENT).excluding_ids([fact_id_1, fact_id_2]).build()
        sql = _compile(stmt)
        assert "NOT" in sql
        assert str(fact_id_1) in sql
        assert str(fact_id_2) in sql

    def test_matching_text(self) -> None:
        stmt = FactQuery.active(AGENT).matching_text(["%pedro%", "%brasil%"]).build()
        sql = _compile(stmt)
        assert "ILIKE" in sql
        # SQL literal rendering escapes single %% as %%%% — match pattern contents loosely
        assert "pedro" in sql
        assert "brasil" in sql

    def test_matching_text_empty_list_is_noop(self) -> None:
        stmt = FactQuery.active(AGENT).matching_text([]).build()
        sql = _compile(stmt)
        assert "ILIKE" not in sql

    def test_by_ids(self) -> None:
        fact_id = uuid.UUID("cccccccc-0000-0000-0000-000000000003")
        stmt = FactQuery.active(AGENT).by_ids([fact_id]).build()
        sql = _compile(stmt)
        assert str(fact_id) in sql
        assert "memory_facts.id IN" in sql

    def test_with_entity_text(self) -> None:
        stmt = FactQuery.active(AGENT).with_entity_text().build()
        sql = _compile(stmt)
        assert "entity_name IS NOT NULL" in sql


# ---------------------------------------------------------------------------
# Ordering — tiebreaker always embedded
# ---------------------------------------------------------------------------


class TestOrdering:
    """Every ordering method includes MemoryFact.id as the final tiebreaker."""

    def test_order_by_recency_includes_id_tiebreaker(self) -> None:
        stmt = FactQuery.active(AGENT).order_by_recency().build()
        sql = _compile(stmt)
        assert "ORDER BY memory_facts.valid_from DESC, memory_facts.id" in sql

    def test_order_by_created_includes_id_tiebreaker(self) -> None:
        stmt = FactQuery.active(AGENT).order_by_created().build()
        sql = _compile(stmt)
        assert "ORDER BY memory_facts.created_at DESC, memory_facts.id" in sql

    def test_order_by_confidence_includes_id_tiebreaker(self) -> None:
        stmt = FactQuery.active(AGENT).order_by_confidence().build()
        sql = _compile(stmt)
        assert "ORDER BY memory_facts.confidence DESC, memory_facts.id" in sql

    def test_order_by_importance_includes_id_tiebreaker(self) -> None:
        stmt = FactQuery.active(AGENT).order_by_importance().build()
        sql = _compile(stmt)
        assert "ORDER BY memory_facts.importance DESC, memory_facts.id" in sql

    def test_order_by_similarity_includes_id_tiebreaker(self) -> None:
        embedding = [0.1] * 1536
        stmt = FactQuery.active(AGENT).order_by_similarity(embedding).build()
        sql = _compile(stmt)
        # Cosine distance expression is rendered; id is the second ORDER BY
        assert "embedding_vec <=>" in sql
        assert "memory_facts.id" in sql

    def test_no_raw_order_by_method_exposed(self) -> None:
        """The quality gate: constructing an unstable query is impossible.

        ``FactQuery`` does not expose a generic ``order_by()`` method.
        The only way to add ordering is via the four ``order_by_*`` helpers,
        each of which embeds ``MemoryFact.id`` as the tiebreaker.
        """
        query = FactQuery.active(AGENT)
        assert not hasattr(query, "order_by"), (
            "FactQuery must NOT expose a raw `order_by` method — it would "
            "allow callers to bypass the tiebreaker guarantee."
        )


# ---------------------------------------------------------------------------
# Similarity — 2-column select
# ---------------------------------------------------------------------------


class TestSimilarity:
    """``order_by_similarity`` returns a 2-column select with labeled similarity."""

    def test_similarity_select_has_two_columns(self) -> None:
        embedding = [0.0] * 1536
        stmt = FactQuery.active(AGENT).with_embedding().order_by_similarity(embedding).build()
        # Selected columns should include MemoryFact + the similarity label
        columns = list(stmt.selected_columns)
        assert len(columns) >= 2
        labels = [c.key if hasattr(c, "key") else str(c) for c in columns]
        assert any("similarity" in str(label) for label in labels)

    def test_similarity_default_select_has_one_column(self) -> None:
        """Without order_by_similarity, build() selects MemoryFact only."""
        stmt = FactQuery.active(AGENT).order_by_recency().build()
        columns = list(stmt.selected_columns)
        # Only MemoryFact columns (no extra similarity column)
        labels = [c.key if hasattr(c, "key") else str(c) for c in columns]
        assert not any("similarity" in str(label) for label in labels)


# ---------------------------------------------------------------------------
# Chaining — fluent composition
# ---------------------------------------------------------------------------


class TestChaining:
    """Fluent chaining works and produces the expected composite query."""

    def test_full_chain(self) -> None:
        stmt = (
            FactQuery.active(AGENT)
            .with_min_confidence(0.5)
            .with_embedding()
            .for_entity("person:pedro")
            .order_by_recency()
            .limit(20)
            .offset(10)
            .build()
        )
        sql = _compile(stmt)
        assert "valid_to IS NULL" in sql
        assert "confidence >= 0.5" in sql
        assert "embedding_vec IS NOT NULL" in sql
        assert "EXISTS" in sql
        assert "'person:pedro'" in sql
        assert "ORDER BY memory_facts.valid_from DESC, memory_facts.id" in sql
        assert "LIMIT 20" in sql
        assert "OFFSET 10" in sql

    def test_filters_are_composable(self) -> None:
        stmt = FactQuery.active(AGENT).with_min_confidence(0.3).with_entity_text().matching_text(["%work%"]).build()
        sql = _compile(stmt)
        assert "confidence >= 0.3" in sql
        assert "entity_name IS NOT NULL" in sql
        # SQL literal rendering escapes % as %% — match pattern content loosely
        assert "work" in sql
        assert "ILIKE" in sql

    def test_builder_returns_self_from_chain_methods(self) -> None:
        """Every chain method returns the same instance for composition."""
        q = FactQuery.active(AGENT)
        assert q.with_min_confidence(0.5) is q
        assert q.with_embedding() is q
        assert q.for_entity("x") is q
        assert q.for_entities(["x"]) is q
        assert q.for_cluster(uuid.uuid4()) is q
        assert q.within_temporal_window(None, None) is q
        assert q.excluding_ids([]) is q
        assert q.matching_text([]) is q
        assert q.by_ids([]) is q
        assert q.with_entity_text() is q
        assert q.order_by_recency() is q
        assert q.order_by_created() is q
        assert q.order_by_confidence() is q
        assert q.order_by_importance() is q
        assert q.order_by_similarity([0.0] * 1536) is q
        assert q.limit(10) is q
        assert q.offset(5) is q
        assert q.with_deferred_embedding() is q


# ---------------------------------------------------------------------------
# Point lookup pattern
# ---------------------------------------------------------------------------


class TestPointLookup:
    """``by_ids`` + ``all_versions`` replicates MemoryClient.get() semantics."""

    def test_get_by_id_includes_soft_deleted(self) -> None:
        fact_id = uuid.UUID("dddddddd-0000-0000-0000-000000000004")
        stmt = FactQuery.all_versions(AGENT).by_ids([fact_id]).build()
        sql = _compile(stmt)
        assert str(fact_id) in sql
        assert "valid_to IS NULL" not in sql

    def test_get_by_id_active_only(self) -> None:
        fact_id = uuid.UUID("eeeeeeee-0000-0000-0000-000000000005")
        stmt = FactQuery.active(AGENT).by_ids([fact_id]).build()
        sql = _compile(stmt)
        assert str(fact_id) in sql
        assert "valid_to IS NULL" in sql


# ---------------------------------------------------------------------------
# Pagination + deferred embedding
# ---------------------------------------------------------------------------


class TestPagination:
    """Limit, offset and deferred embedding."""

    def test_limit(self) -> None:
        stmt = FactQuery.active(AGENT).limit(50).build()
        sql = _compile(stmt)
        assert "LIMIT 50" in sql

    def test_offset(self) -> None:
        stmt = FactQuery.active(AGENT).order_by_created().limit(10).offset(20).build()
        sql = _compile(stmt)
        assert "OFFSET 20" in sql

    def test_with_deferred_embedding(self) -> None:
        """Deferred embedding option is applied to the returned Select."""
        stmt = FactQuery.active(AGENT).with_deferred_embedding().build()
        # The defer option modifies loader options on the compiled statement
        # We inspect the statement execution options indirectly
        sql = _compile(stmt)
        # Smoke check: statement still compiles and selects from memory_facts
        assert "memory_facts" in sql
