"""Unit tests for RecallPipeline: current episode scoring and recall-time dedup."""

from __future__ import annotations

from datetime import datetime
from typing import Any

import pytest

from gleanr.core.config import GleanrConfig, RecallConfig
from gleanr.memory.recall import RecallPipeline
from gleanr.models import Fact, MarkerType, Role, Turn
from gleanr.providers.base import NullEmbedder
from gleanr.storage.memory import InMemoryBackend
from gleanr.utils import HeuristicTokenCounter


# ---------------------------------------------------------------------------
# Test doubles
# ---------------------------------------------------------------------------


class FakeEpisodeManager:
    """Minimal stand-in for EpisodeManager used by RecallPipeline."""

    def __init__(self, turns: list[Turn] | None = None) -> None:
        self._turns = turns or []
        self._episode_id = "ep_current"

    @property
    def current_episode_id(self) -> str | None:
        return self._episode_id

    async def get_current_episode_turns(self) -> list[Turn]:
        return list(self._turns)


class FixedEmbedder:
    """Embedder that returns a pre-configured vector for any input.

    Useful for controlling cosine similarity in tests.
    """

    def __init__(self, vector: list[float]) -> None:
        self._vector = vector

    async def embed(self, texts: list[str]) -> list[list[float]]:
        return [list(self._vector) for _ in texts]

    @property
    def dimension(self) -> int:
        return len(self._vector)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_turn(
    turn_id: str = "turn_1",
    session_id: str = "test_session",
    episode_id: str = "ep_current",
    content: str = "Test message",
    role: Role = Role.USER,
    embedding_id: str | None = None,
) -> Turn:
    return Turn(
        id=turn_id,
        session_id=session_id,
        episode_id=episode_id,
        role=role,
        content=content,
        created_at=datetime.utcnow(),
        embedding_id=embedding_id,
        token_count=10,
    )


def _make_fact(
    fact_id: str = "fact_1",
    session_id: str = "test_session",
    content: str = "Test fact",
    confidence: float = 0.9,
    embedding_id: str | None = None,
) -> Fact:
    return Fact(
        id=fact_id,
        session_id=session_id,
        episode_id="ep_past",
        content=content,
        created_at=datetime.utcnow(),
        fact_type=MarkerType.DECISION.value,
        confidence=confidence,
        embedding_id=embedding_id,
        token_count=10,
    )


async def _build_pipeline(
    storage: InMemoryBackend,
    episode_manager: FakeEpisodeManager,
    config: GleanrConfig | None = None,
) -> RecallPipeline:
    config = config or GleanrConfig()
    # Use a fixed embedder that returns a known vector for query embedding
    embedder = FixedEmbedder(vector=[1.0, 0.0, 0.0, 0.0])
    return RecallPipeline(
        session_id="test_session",
        storage=storage,
        embedder=embedder,
        token_counter=HeuristicTokenCounter(),
        episode_manager=episode_manager,
        config=config,
    )


# ---------------------------------------------------------------------------
# Tests: Current Episode Scoring
# ---------------------------------------------------------------------------


class TestCurrentEpisodeScoring:
    """Verify current episode turns are scored by embedding similarity, not 1.0."""

    @pytest.mark.asyncio
    async def test_current_episode_turns_scored_by_relevance(self) -> None:
        """Current episode turns with embeddings are scored by cosine similarity."""
        storage = InMemoryBackend()
        await storage.initialize()

        # Store a turn with an embedding that's orthogonal to query [1,0,0,0]
        await storage.save_embedding(
            id="emb_turn",
            embedding=[0.0, 1.0, 0.0, 0.0],  # orthogonal → cosine = 0.0
            metadata={},
        )

        turn = _make_turn(content="Unrelated topic", embedding_id="emb_turn")
        ep_mgr = FakeEpisodeManager(turns=[turn])

        config = GleanrConfig(
            recall=RecallConfig(current_episode_boost=0.2)
        )
        pipeline = await _build_pipeline(storage, ep_mgr, config)
        items = await pipeline.recall("relevant query")

        # Turn should score ~0.0 + 0.2 boost = 0.2, NOT 1.0
        turn_items = [i for i in items if i.source == "turn"]
        assert len(turn_items) == 1
        assert turn_items[0].score < 0.5  # Well below the old hardcoded 1.0

    @pytest.mark.asyncio
    async def test_current_episode_boost_applied(self) -> None:
        """The current_episode_boost is added to the embedding similarity."""
        storage = InMemoryBackend()
        await storage.initialize()

        # Store a turn with an embedding parallel to query [1,0,0,0]
        await storage.save_embedding(
            id="emb_turn",
            embedding=[1.0, 0.0, 0.0, 0.0],  # parallel → cosine = 1.0
            metadata={},
        )

        turn = _make_turn(content="Relevant topic", embedding_id="emb_turn")
        ep_mgr = FakeEpisodeManager(turns=[turn])

        config = GleanrConfig(
            recall=RecallConfig(current_episode_boost=0.3)
        )
        pipeline = await _build_pipeline(storage, ep_mgr, config)
        items = await pipeline.recall("relevant query")

        turn_items = [i for i in items if i.source == "turn"]
        assert len(turn_items) == 1
        # cosine(1,0,0,0 ; 1,0,0,0) = 1.0, plus boost 0.3 = 1.3
        assert turn_items[0].score == pytest.approx(1.3, abs=0.05)

    @pytest.mark.asyncio
    async def test_current_episode_fallback_without_embedding(self) -> None:
        """Turns without embeddings get default relevance 0.5 + boost."""
        storage = InMemoryBackend()
        await storage.initialize()

        # Turn with no embedding_id
        turn = _make_turn(content="No embedding", embedding_id=None)
        ep_mgr = FakeEpisodeManager(turns=[turn])

        config = GleanrConfig(
            recall=RecallConfig(current_episode_boost=0.2)
        )
        pipeline = await _build_pipeline(storage, ep_mgr, config)
        items = await pipeline.recall("some query")

        turn_items = [i for i in items if i.source == "turn"]
        assert len(turn_items) == 1
        # 0.5 default + 0.2 boost = 0.7
        assert turn_items[0].score == pytest.approx(0.7, abs=0.05)


# ---------------------------------------------------------------------------
# Tests: Recall-Time Dedup
# ---------------------------------------------------------------------------


class TestRecallDedup:
    """Verify near-duplicate facts are filtered at recall time."""

    @pytest.mark.asyncio
    async def test_duplicate_facts_filtered(self) -> None:
        """Two facts with identical embeddings — only highest-scored returned."""
        storage = InMemoryBackend()
        await storage.initialize()

        # Both facts have identical embeddings → cosine = 1.0
        emb = [0.8, 0.6, 0.0, 0.0]  # Has cosine > 0 with query [1,0,0,0]
        await storage.save_embedding(id="emb_f1", embedding=emb, metadata={})
        await storage.save_embedding(id="emb_f2", embedding=list(emb), metadata={})

        fact1 = _make_fact(
            fact_id="fact_1",
            content="All queries must use parameterized statements",
            embedding_id="emb_f1",
        )
        fact2 = _make_fact(
            fact_id="fact_2",
            content="All DB interactions must use parameterized statements",
            embedding_id="emb_f2",
        )
        await storage.save_fact(fact1)
        await storage.save_fact(fact2)

        ep_mgr = FakeEpisodeManager()
        config = GleanrConfig(
            recall=RecallConfig(
                recall_dedup_threshold=0.90,
                min_relevance_threshold=0.0,
            )
        )
        pipeline = await _build_pipeline(storage, ep_mgr, config)
        items = await pipeline.recall("parameterized queries", include_current_episode=False)

        fact_items = [i for i in items if i.source == "fact"]
        # Both have identical embeddings → one should be deduped
        assert len(fact_items) == 1

    @pytest.mark.asyncio
    async def test_non_duplicate_facts_kept(self) -> None:
        """Two facts with different embeddings are both returned."""
        storage = InMemoryBackend()
        await storage.initialize()

        # Different embeddings → low cosine similarity
        await storage.save_embedding(
            id="emb_f1", embedding=[0.8, 0.6, 0.0, 0.0], metadata={}
        )
        await storage.save_embedding(
            id="emb_f2", embedding=[0.0, 0.0, 0.8, 0.6], metadata={}
        )

        fact1 = _make_fact(
            fact_id="fact_1",
            content="Use PostgreSQL for database",
            embedding_id="emb_f1",
        )
        fact2 = _make_fact(
            fact_id="fact_2",
            content="Deploy on AWS ECS",
            embedding_id="emb_f2",
        )
        await storage.save_fact(fact1)
        await storage.save_fact(fact2)

        ep_mgr = FakeEpisodeManager()
        config = GleanrConfig(
            recall=RecallConfig(
                recall_dedup_threshold=0.90,
                min_relevance_threshold=0.0,
            )
        )
        pipeline = await _build_pipeline(storage, ep_mgr, config)
        items = await pipeline.recall("tech stack", include_current_episode=False)

        fact_items = [i for i in items if i.source == "fact"]
        assert len(fact_items) == 2

    @pytest.mark.asyncio
    async def test_recall_dedup_disabled_at_threshold_one(self) -> None:
        """All facts kept when recall_dedup_threshold is 1.0."""
        storage = InMemoryBackend()
        await storage.initialize()

        # Identical embeddings
        emb = [0.8, 0.6, 0.0, 0.0]
        await storage.save_embedding(id="emb_f1", embedding=emb, metadata={})
        await storage.save_embedding(id="emb_f2", embedding=list(emb), metadata={})

        fact1 = _make_fact(fact_id="fact_1", content="Fact A", embedding_id="emb_f1")
        fact2 = _make_fact(fact_id="fact_2", content="Fact B", embedding_id="emb_f2")
        await storage.save_fact(fact1)
        await storage.save_fact(fact2)

        ep_mgr = FakeEpisodeManager()
        config = GleanrConfig(
            recall=RecallConfig(
                recall_dedup_threshold=1.0,  # Disabled
                min_relevance_threshold=0.0,
            )
        )
        pipeline = await _build_pipeline(storage, ep_mgr, config)
        items = await pipeline.recall("query", include_current_episode=False)

        fact_items = [i for i in items if i.source == "fact"]
        assert len(fact_items) == 2
