# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
# Licensed under AGPL-3.0-or-later - see LICENSE file
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com

"""Tests for MemoryEngine.recall() flow — retrieval, reconsolidation, hooks.

Covers:
  - recall() returns RecallResponse
  - recall() delegates to _retrieval_engine.recall()
  - recall() uses default profile_id when none supplied
  - recall() uses override profile_id when supplied
  - recall() uses override mode when supplied
  - recall() records retrieval_time_ms > 0
  - Reconsolidation: trust_scorer.update_on_access called per result
  - Reconsolidation: access_count incremented via _db.update_fact
  - Adaptive ranking phase 1: no reranking when < 50 signals
  - Adaptive ranking: skips gracefully when learning DB missing
  - Pre-hooks invoked before recall
  - Post-hooks invoked after recall with result_count
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from superlocalmemory.core.engine import MemoryEngine
from superlocalmemory.storage.models import (
    AtomicFact,
    Mode,
    RecallResponse,
    RetrievalResult,
)


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

def _make_fact(
    fact_id: str = "f1",
    content: str = "Alice is an engineer",
    access_count: int = 0,
) -> AtomicFact:
    return AtomicFact(
        fact_id=fact_id,
        memory_id="m0",
        content=content,
        confidence=0.9,
        access_count=access_count,
    )


def _make_recall_response(
    query: str = "test query",
    facts: list[AtomicFact] | None = None,
    mode: Mode = Mode.A,
    retrieval_time_ms: float = 5.0,
) -> RecallResponse:
    results = []
    for f in (facts or []):
        results.append(RetrievalResult(
            fact=f,
            score=0.85,
            channel_scores={"semantic": 0.9},
            confidence=f.confidence,
            evidence_chain=["semantic"],
            trust_score=0.5,
        ))
    return RecallResponse(
        query=query,
        mode=mode,
        results=results,
        query_type="factual",
        channel_weights={"semantic": 1.2, "bm25": 1.0},
        total_candidates=len(results),
        retrieval_time_ms=retrieval_time_ms,
    )


# ---------------------------------------------------------------------------
# Basic recall flow
# ---------------------------------------------------------------------------

class TestRecallBasicFlow:
    """Verify recall() pipeline from query to RecallResponse."""

    def test_recall_returns_recall_response(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() returns a RecallResponse instance."""
        response = engine_with_mock_deps.recall("What does Alice do?")
        assert isinstance(response, RecallResponse)

    def test_recall_delegates_to_retrieval_engine(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() calls _retrieval_engine.recall() internally."""
        mock_response = _make_recall_response("Where is Bob?")
        re = engine_with_mock_deps._retrieval_engine
        with patch.object(re, 'recall', return_value=mock_response) as spy:
            result = engine_with_mock_deps.recall("Where is Bob?")
            spy.assert_called_once()
            assert result.query == "Where is Bob?"

    def test_recall_uses_default_profile_id(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() uses self._profile_id when profile_id param is None."""
        mock_response = _make_recall_response()
        re = engine_with_mock_deps._retrieval_engine
        with patch.object(re, 'recall', return_value=mock_response) as spy:
            engine_with_mock_deps.recall("query")
            call_args = spy.call_args
            # Second positional arg is profile_id
            assert call_args[0][1] == engine_with_mock_deps._profile_id

    def test_recall_override_profile_id(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() uses the profile_id parameter when provided."""
        mock_response = _make_recall_response()
        re = engine_with_mock_deps._retrieval_engine
        with patch.object(re, 'recall', return_value=mock_response) as spy:
            engine_with_mock_deps.recall("query", profile_id="custom-prof")
            call_args = spy.call_args
            assert call_args[0][1] == "custom-prof"

    def test_recall_override_mode(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() passes the mode parameter to retrieval engine."""
        mock_response = _make_recall_response(mode=Mode.C)
        re = engine_with_mock_deps._retrieval_engine
        with patch.object(re, 'recall', return_value=mock_response) as spy:
            engine_with_mock_deps.recall("query", mode=Mode.C)
            call_args = spy.call_args
            assert call_args[0][2] == Mode.C

    def test_recall_timing_recorded(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() returns a response with retrieval_time_ms >= 0."""
        response = engine_with_mock_deps.recall("timing test")
        assert response.retrieval_time_ms >= 0


# ---------------------------------------------------------------------------
# Reconsolidation (post-recall updates)
# ---------------------------------------------------------------------------

class TestRecallReconsolidation:
    """Verify trust and access count updates after recall."""

    def test_recall_updates_trust_on_access(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() calls trust_scorer.update_on_access for each result."""
        fact = _make_fact("trust-f1")
        mock_response = _make_recall_response(facts=[fact])
        re = engine_with_mock_deps._retrieval_engine
        ts = engine_with_mock_deps._trust_scorer

        with patch.object(re, 'recall', return_value=mock_response):
            with patch.object(ts, 'update_on_access') as trust_spy:
                engine_with_mock_deps.recall("trust query")
                trust_spy.assert_called_once_with(
                    "fact", "trust-f1", engine_with_mock_deps._profile_id,
                )

    def test_recall_increments_access_count(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() calls _db.update_fact to increment access_count."""
        fact = _make_fact("access-f1", access_count=3)
        mock_response = _make_recall_response(facts=[fact])
        re = engine_with_mock_deps._retrieval_engine
        db = engine_with_mock_deps._db

        with patch.object(re, 'recall', return_value=mock_response):
            with patch.object(db, 'update_fact') as db_spy:
                engine_with_mock_deps.recall("access query")
                db_spy.assert_called_once()
                update_dict = db_spy.call_args[0][1]
                assert update_dict["access_count"] == 4  # 3 + 1


# ---------------------------------------------------------------------------
# Adaptive ranking
# ---------------------------------------------------------------------------

class TestRecallAdaptiveRanking:
    """Verify adaptive ranking phases."""

    def test_recall_phase1_no_reranking(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """With < 50 feedback signals, adaptive ranking returns response unchanged."""
        fact = _make_fact("rank-f1")
        mock_response = _make_recall_response(facts=[fact])
        re = engine_with_mock_deps._retrieval_engine

        with patch.object(re, 'recall', return_value=mock_response):
            # Patch pathlib.Path inside the function (imported locally at line 634)
            with patch("pathlib.Path.home") as mock_home:
                mock_learning = MagicMock()
                mock_learning.exists.return_value = False
                mock_home.return_value.__truediv__ = MagicMock(return_value=mock_learning)
                mock_learning.__truediv__ = MagicMock(return_value=mock_learning)
                result = engine_with_mock_deps.recall("rank query")
                # Result should still have the fact (no crash, response unchanged)
                assert len(result.results) >= 0

    def test_recall_skips_adaptive_when_no_learning_db(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """Missing learning.db causes adaptive ranking to skip gracefully."""
        mock_response = _make_recall_response()
        re = engine_with_mock_deps._retrieval_engine

        with patch.object(re, 'recall', return_value=mock_response):
            # The actual _apply_adaptive_ranking checks learning_db.exists()
            # When it doesn't exist, it returns response unchanged
            result = engine_with_mock_deps.recall("no-learning query")
            assert isinstance(result, RecallResponse)


# ---------------------------------------------------------------------------
# Hooks
# ---------------------------------------------------------------------------

class TestRecallHooks:
    """Verify pre/post hook invocation during recall()."""

    def test_recall_runs_pre_hooks(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() calls _hooks.run_pre('recall', ...) before retrieval."""
        spy = MagicMock()
        engine_with_mock_deps._hooks.register_pre("recall", spy)
        engine_with_mock_deps.recall("hook pre test")
        spy.assert_called_once()
        ctx = spy.call_args[0][0]
        assert ctx["operation"] == "recall"

    def test_recall_runs_post_hooks(
        self, engine_with_mock_deps: MemoryEngine,
    ) -> None:
        """recall() calls _hooks.run_post('recall', ...) with result_count."""
        spy = MagicMock()
        engine_with_mock_deps._hooks.register_post("recall", spy)
        engine_with_mock_deps.recall("hook post test")
        spy.assert_called_once()
        ctx = spy.call_args[0][0]
        assert "result_count" in ctx
        assert "query_type" in ctx
