"""Tests for bi-temporal search: valid_from, valid_to, valid_at filtering."""

from __future__ import annotations

from datetime import UTC, datetime
from pathlib import Path

import pytest

from claude_engram.memory_unit import MemoryUnit
from claude_engram.storage import MemoryStorage


@pytest.fixture()
def storage(tmp_path: Path) -> MemoryStorage:
    """Create a MemoryStorage with a temp database."""
    db = tmp_path / "test.db"
    s = MemoryStorage(db_path=db)
    yield s
    s.close()


# Helper timestamps
T0 = "2025-01-01T00:00:00+00:00"
T1 = "2025-01-15T00:00:00+00:00"
T2 = "2025-02-01T00:00:00+00:00"
T3 = "2025-03-01T00:00:00+00:00"


class TestStoreWithValidFrom:
    """Test that valid_from is persisted when storing memories."""

    def test_store_with_explicit_valid_from(self, storage: MemoryStorage) -> None:
        mid = storage.store(
            content="decision valid from past",
            category="decision",
            valid_from=T0,
        )
        conn = storage._get_conn()
        row = conn.execute("SELECT valid_from FROM memories WHERE id = ?", (mid,)).fetchone()
        assert row["valid_from"] == T0

    def test_store_without_valid_from_defaults_to_now(self, storage: MemoryStorage) -> None:
        mid = storage.store(content="no explicit valid_from", category="lesson")
        conn = storage._get_conn()
        row = conn.execute(
            "SELECT valid_from, created_at FROM memories WHERE id = ?", (mid,)
        ).fetchone()
        # valid_from should be set (not NULL) and equal created_at
        assert row["valid_from"] is not None
        assert row["valid_from"] == row["created_at"]

    def test_store_valid_to_null_by_default(self, storage: MemoryStorage) -> None:
        mid = storage.store(content="active memory", category="lesson")
        conn = storage._get_conn()
        row = conn.execute("SELECT valid_to FROM memories WHERE id = ?", (mid,)).fetchone()
        assert row["valid_to"] is None


class TestSupersedeSetValidTo:
    """Test that superseding a memory auto-sets valid_to."""

    def test_supersede_sets_valid_to(self, storage: MemoryStorage) -> None:
        old_id = storage.store(content="old decision about auth", category="decision")
        new_id = storage.store(content="new decision about auth v2", category="decision")

        before_supersede = datetime.now(tz=UTC)
        storage.supersede_memory(old_id, new_id)

        conn = storage._get_conn()
        row = conn.execute(
            "SELECT valid_to, is_active FROM memories WHERE id = ?", (old_id,)
        ).fetchone()
        assert row["is_active"] == 0
        assert row["valid_to"] is not None
        # valid_to should be approximately now
        valid_to = datetime.fromisoformat(row["valid_to"])
        assert valid_to >= before_supersede

    def test_new_memory_valid_to_stays_null(self, storage: MemoryStorage) -> None:
        old_id = storage.store(content="old decision", category="decision")
        new_id = storage.store(content="new decision", category="decision")
        storage.supersede_memory(old_id, new_id)

        conn = storage._get_conn()
        row = conn.execute("SELECT valid_to FROM memories WHERE id = ?", (new_id,)).fetchone()
        assert row["valid_to"] is None


class TestTemporalSearch:
    """Test valid_at temporal filtering in search."""

    def test_search_before_valid_from_excludes(self, storage: MemoryStorage) -> None:
        """Memory with valid_from=T1 should not appear in search at T0."""
        storage.store(
            content="future decision about deployment",
            category="decision",
            valid_from=T1,
        )
        results = storage.search(
            query="deployment decision",
            valid_at=T0,
            enable_cache=False,
        )
        assert len(results) == 0

    def test_search_after_valid_from_includes(self, storage: MemoryStorage) -> None:
        """Memory with valid_from=T1 should appear in search at T2."""
        storage.store(
            content="future decision about deployment",
            category="decision",
            valid_from=T1,
        )
        results = storage.search(
            query="deployment decision",
            valid_at=T2,
            enable_cache=False,
        )
        assert len(results) == 1

    def test_search_at_exact_valid_from_includes(self, storage: MemoryStorage) -> None:
        """Memory with valid_from=T1 should appear in search at exactly T1."""
        storage.store(
            content="exact boundary decision",
            category="decision",
            valid_from=T1,
        )
        results = storage.search(
            query="boundary decision",
            valid_at=T1,
            enable_cache=False,
        )
        assert len(results) == 1

    def test_superseded_excluded_at_new_time(self, storage: MemoryStorage) -> None:
        """After supersession, old memory should be excluded at current time."""
        old_id = storage.store(
            content="old auth approach using JWT",
            category="decision",
            valid_from=T0,
        )
        new_id = storage.store(
            content="new auth approach using OAuth",
            category="decision",
            valid_from=T2,
        )
        storage.supersede_memory(old_id, new_id)

        # Search at T3: should find new, not old
        results = storage.search(
            query="auth approach",
            valid_at=T3,
            enable_cache=False,
        )
        contents = [r["content"] for r in results]
        assert "new auth approach using OAuth" in contents
        # Old is excluded: is_active=0 AND valid_to < T3
        assert "old auth approach using JWT" not in contents

    def test_default_search_no_valid_at_backward_compat(self, storage: MemoryStorage) -> None:
        """Search without valid_at should return all active memories (backwards compatible)."""
        storage.store(content="active lesson about testing", category="lesson")
        results = storage.search(
            query="testing lesson",
            enable_cache=False,
        )
        assert len(results) == 1

    def test_search_with_valid_at_skips_cache(self, storage: MemoryStorage) -> None:
        """Temporal searches should not use cached results."""
        storage.store(
            content="cached decision about caching",
            category="decision",
            valid_from=T0,
        )
        # First search populates cache
        results1 = storage.search(query="caching", enable_cache=True)
        assert len(results1) == 1

        # Temporal search should NOT use cache (different temporal slice)
        results2 = storage.search(
            query="caching",
            valid_at="2020-01-01T00:00:00+00:00",
            enable_cache=True,
        )
        # valid_from=T0 (2025) so search at 2020 should find nothing
        assert len(results2) == 0


class TestTemporalFieldsInResults:
    """Test that valid_from/valid_to appear in search result dicts."""

    def test_valid_from_in_results(self, storage: MemoryStorage) -> None:
        storage.store(
            content="decision with explicit valid_from",
            category="decision",
            valid_from=T1,
        )
        results = storage.search(query="explicit valid_from", enable_cache=False)
        assert len(results) == 1
        assert results[0].get("valid_from") == T1

    def test_valid_to_absent_for_active(self, storage: MemoryStorage) -> None:
        storage.store(content="active memory no valid_to", category="lesson")
        results = storage.search(query="active memory", enable_cache=False)
        assert len(results) == 1
        assert "valid_to" not in results[0]

    def test_valid_to_present_after_supersede(self, storage: MemoryStorage) -> None:
        old_id = storage.store(content="will be superseded", category="decision")
        new_id = storage.store(content="the replacement", category="decision")
        storage.supersede_memory(old_id, new_id)

        results = storage.search(
            query="superseded",
            include_inactive=True,
            enable_cache=False,
        )
        superseded_result = next(r for r in results if r["id"] == old_id)
        assert "valid_to" in superseded_result


class TestMemoryUnitRoundTrip:
    """Test that MemoryUnit preserves temporal fields through the full cycle."""

    def test_to_engram_store_args_includes_valid_from(self) -> None:
        unit = MemoryUnit(
            content="temporal unit",
            category="decision",
            valid_from=datetime(2025, 1, 15, tzinfo=UTC),
        )
        args = unit.to_engram_store_args()
        assert "valid_from" in args
        assert args["valid_from"] == "2025-01-15T00:00:00+00:00"

    def test_to_engram_store_args_omits_none_valid_from(self) -> None:
        unit = MemoryUnit(content="no temporal", category="lesson")
        args = unit.to_engram_store_args()
        assert "valid_from" not in args

    def test_from_search_result_populates_temporal(self) -> None:
        result = {
            "id": 42,
            "content": "temporal result",
            "category": "decision",
            "created_at": T0,
            "proj_tag": None,
            "similarity": 0.95,
            "is_active": True,
            "token_count": 10,
            "valid_from": T1,
            "valid_to": T2,
        }
        unit = MemoryUnit.from_search_result(result)
        assert unit.valid_from == datetime.fromisoformat(T1)
        assert unit.valid_to == datetime.fromisoformat(T2)

    def test_from_search_result_no_temporal(self) -> None:
        result = {
            "id": 42,
            "content": "no temporal",
            "category": "lesson",
            "created_at": T0,
            "proj_tag": None,
            "similarity": 0.9,
            "is_active": True,
            "token_count": 5,
        }
        unit = MemoryUnit.from_search_result(result)
        assert unit.valid_from is None
        assert unit.valid_to is None

    def test_store_search_roundtrip(self, storage: MemoryStorage) -> None:
        """Full round-trip: MemoryUnit → store → search → MemoryUnit."""
        original = MemoryUnit(
            content="round-trip temporal test decision",
            category="decision",
            valid_from=datetime.fromisoformat(T1),
        )
        args = original.to_engram_store_args()
        storage.store(**args)

        results = storage.search(
            query="round-trip temporal",
            return_units=True,
            enable_cache=False,
        )
        assert len(results) == 1
        recovered = results[0]
        assert isinstance(recovered, MemoryUnit)
        assert recovered.valid_from == original.valid_from
        assert recovered.valid_to is None  # not superseded
