"""Tests for TrustMem.add_raw() — direct memory insertion bypassing LLM pipeline."""
import pytest

from trustmem import TrustMem
from trustmem.exceptions import ValidationError


# ------------------------------------------------------------------
# Fixtures
# ------------------------------------------------------------------

@pytest.fixture
def tm(handler):
    """TrustMem SDK backed by in-memory handler."""
    return TrustMem._from_handler(handler)


# ------------------------------------------------------------------
# Basic functionality
# ------------------------------------------------------------------

class TestAddRawBasic:
    """Basic add_raw tests."""

    def test_add_raw_minimal(self, tm):
        """Minimal call — content only, defaults for everything else."""
        result = tm.add_raw(content="Alex likes sushi", user_id="test")
        assert result["memory_id"]
        assert result["content_hash"].startswith("mem_")
        assert result["confidence"] == "medium"
        assert result["source_trust"] == "derived"
        assert result["embedding_status"] == "success"

    def test_add_raw_with_all_params(self, tm):
        """All parameters specified."""
        result = tm.add_raw(
            content="Alex works at Acme Corp",
            embedding=[0.1] * 768,
            user_id="alex",
            tags=["crm", "import"],
            metadata={"source": "salesforce"},
            confidence="high",
            source_trust="verified",
        )
        assert result["memory_id"]
        assert result["confidence"] == "high"
        assert result["source_trust"] == "verified"
        assert result["embedding_status"] == "success"

    def test_add_raw_searchable(self, tm):
        """Memory added via add_raw is searchable."""
        tm.add_raw(
            content="Bob prefers tea over coffee",
            user_id="test",
        )
        result = tm.search("tea preferences", user_id="test")
        assert len(result["results"]) >= 1
        contents = [r["content"] for r in result["results"]]
        assert "Bob prefers tea over coffee" in contents

    def test_add_raw_with_precomputed_embedding(self, tm):
        """Pre-computed embedding is stored (no AI embed call needed)."""
        vec = [0.5] * 768
        result = tm.add_raw(
            content="Pre-embedded fact",
            embedding=vec,
            user_id="test",
        )
        assert result["embedding_status"] == "success"
        # Verify searchable
        search = tm.search("Pre-embedded", user_id="test")
        assert len(search["results"]) >= 1

    def test_add_raw_appears_in_get_all(self, tm):
        """Memories from add_raw appear in get_all."""
        tm.add_raw(content="Fact one", user_id="test")
        tm.add_raw(content="Fact two", user_id="test")
        result = tm.get_all(user_id="test")
        assert len(result["results"]) == 2

    def test_add_raw_deletable(self, tm):
        """Memories from add_raw can be deleted."""
        result = tm.add_raw(content="Temporary fact", user_id="test")
        mid = result["memory_id"]
        deleted = tm.delete(mid)
        assert deleted is True
        all_mems = tm.get_all(user_id="test")
        assert len(all_mems["results"]) == 0


# ------------------------------------------------------------------
# Audit trail
# ------------------------------------------------------------------

class TestAddRawAuditTrail:
    """add_raw creates proper audit trail entries."""

    def test_audit_trail_created(self, tm):
        """add_raw creates a memory_created audit entry."""
        result = tm.add_raw(
            content="Audited fact",
            confidence="high",
            source_trust="verified",
            user_id="test",
        )
        trail = tm.audit_trail(result["memory_id"])
        assert len(trail) == 1
        entry = trail[0]
        assert entry["event_type"] == "memory_created"
        assert entry["new_confidence"] == "high"
        details = entry.get("details", {})
        assert details.get("source_trust") == "verified"


# ------------------------------------------------------------------
# Confidence and source_trust
# ------------------------------------------------------------------

class TestAddRawConfidence:
    """Confidence and source_trust parameter handling."""

    @pytest.mark.parametrize("confidence", ["high", "medium", "low"])
    def test_valid_confidence(self, tm, confidence):
        result = tm.add_raw(content="test", confidence=confidence, user_id="test")
        assert result["confidence"] == confidence

    @pytest.mark.parametrize("trust", ["verified", "input", "derived"])
    def test_valid_source_trust(self, tm, trust):
        result = tm.add_raw(content="test", source_trust=trust, user_id="test")
        assert result["source_trust"] == trust


# ------------------------------------------------------------------
# Validation errors
# ------------------------------------------------------------------

class TestAddRawValidation:
    """Input validation for add_raw."""

    def test_empty_content_raises(self, tm):
        with pytest.raises(ValidationError, match="non-empty"):
            tm.add_raw(content="", user_id="test")

    def test_whitespace_content_raises(self, tm):
        with pytest.raises(ValidationError, match="non-empty"):
            tm.add_raw(content="   ", user_id="test")

    def test_non_string_content_raises(self, tm):
        with pytest.raises(ValidationError, match="non-empty"):
            tm.add_raw(content=123, user_id="test")  # type: ignore[arg-type]

    def test_invalid_confidence_raises(self, tm):
        with pytest.raises(ValidationError, match="confidence"):
            tm.add_raw(content="test", confidence="ultra", user_id="test")

    def test_invalid_source_trust_raises(self, tm):
        with pytest.raises(ValidationError, match="source_trust"):
            tm.add_raw(content="test", source_trust="unknown", user_id="test")

    def test_invalid_embedding_type_raises(self, tm):
        with pytest.raises(ValidationError, match="embedding"):
            tm.add_raw(content="test", embedding="not_a_list", user_id="test")  # type: ignore[arg-type]

    def test_invalid_embedding_values_raises(self, tm):
        with pytest.raises(ValidationError, match="embedding"):
            tm.add_raw(content="test", embedding=["a", "b"], user_id="test")  # type: ignore[list-item]


# ------------------------------------------------------------------
# Verify integrity
# ------------------------------------------------------------------

class TestAddRawIntegrity:
    """Memories from add_raw pass integrity verification."""

    def test_verify_passes(self, tm):
        result = tm.add_raw(content="Verifiable fact", user_id="test")
        verify = tm.verify(result["memory_id"])
        assert verify["valid"] is True

    def test_verify_all_passes(self, tm):
        tm.add_raw(content="Fact A", user_id="test")
        tm.add_raw(content="Fact B", user_id="test")
        result = tm.verify_all()
        assert result["total"] == 2
        assert result["valid"] == 2
        assert result["invalid"] == 0


# ------------------------------------------------------------------
# No LLM extraction (pipeline bypass)
# ------------------------------------------------------------------

class TestAddRawBypassesPipeline:
    """add_raw does NOT go through LLM extraction pipeline."""

    def test_no_extraction_stats(self, tm):
        """add_raw returns add_memory result, not extraction stats."""
        result = tm.add_raw(content="Direct insert", user_id="test")
        # add_raw returns add_memory format, NOT add() extraction format
        assert "memory_id" in result
        assert "content_hash" in result
        # Should NOT have extraction pipeline fields
        assert "extracted" not in result
        assert "stats" not in result
        assert "entities" not in result

    def test_content_stored_as_is(self, tm):
        """Content is stored exactly as provided, no LLM transformation."""
        original = "The quick brown fox jumps over the lazy dog"
        result = tm.add_raw(content=original, user_id="test")
        all_mems = tm.get_all(user_id="test")
        stored = all_mems["results"][0]["content"]
        assert stored == original


# ------------------------------------------------------------------
# Metadata pre-filter in search
# ------------------------------------------------------------------

class TestAddRawSearchFilters:
    """search(filters=...) pre-filters on metadata from add_raw."""

    def test_filter_by_single_metadata_key(self, tm):
        """Filter search results by a single metadata field."""
        tm.add_raw(content="CRM record A", user_id="test",
                   metadata={"source": "salesforce"})
        tm.add_raw(content="CRM record B", user_id="test",
                   metadata={"source": "hubspot"})
        tm.add_raw(content="CRM record C", user_id="test",
                   metadata={"source": "salesforce"})

        result = tm.search("CRM", user_id="test",
                           filters={"source": "salesforce"})
        assert len(result["results"]) == 2
        contents = {r["content"] for r in result["results"]}
        assert "CRM record A" in contents
        assert "CRM record C" in contents

    def test_filter_by_multiple_metadata_keys(self, tm):
        """Filter with multiple metadata fields (AND logic)."""
        tm.add_raw(content="Sales US", user_id="test",
                   metadata={"source": "salesforce", "region": "us"})
        tm.add_raw(content="Sales JP", user_id="test",
                   metadata={"source": "salesforce", "region": "jp"})
        tm.add_raw(content="Marketing US", user_id="test",
                   metadata={"source": "hubspot", "region": "us"})

        result = tm.search("data", user_id="test",
                           filters={"source": "salesforce", "region": "us"})
        assert len(result["results"]) == 1
        assert result["results"][0]["content"] == "Sales US"

    def test_filter_no_match_returns_empty(self, tm):
        """Filter that matches nothing returns empty results."""
        tm.add_raw(content="some data", user_id="test",
                   metadata={"source": "salesforce"})

        result = tm.search("data", user_id="test",
                           filters={"source": "nonexistent"})
        assert len(result["results"]) == 0

    def test_filter_with_int_value(self, tm):
        """Metadata with integer values can be filtered."""
        tm.add_raw(content="Priority high", user_id="test",
                   metadata={"priority": 1})
        tm.add_raw(content="Priority low", user_id="test",
                   metadata={"priority": 3})

        result = tm.search("priority", user_id="test",
                           filters={"priority": 1})
        assert len(result["results"]) == 1
        assert result["results"][0]["content"] == "Priority high"

    def test_filter_with_bool_value(self, tm):
        """Metadata with boolean values can be filtered."""
        tm.add_raw(content="Active record", user_id="test",
                   metadata={"active": True})
        tm.add_raw(content="Inactive record", user_id="test",
                   metadata={"active": False})

        result = tm.search("record", user_id="test",
                           filters={"active": True})
        assert len(result["results"]) == 1
        assert result["results"][0]["content"] == "Active record"

    def test_no_filter_returns_all(self, tm):
        """Without filters, all matching memories are returned."""
        tm.add_raw(content="Record A", user_id="test",
                   metadata={"source": "salesforce"})
        tm.add_raw(content="Record B", user_id="test",
                   metadata={"source": "hubspot"})

        result = tm.search("record", user_id="test")
        assert len(result["results"]) == 2

    def test_filter_does_not_affect_memories_without_metadata(self, tm):
        """Memories without metadata are excluded by filters."""
        tm.add_raw(content="With metadata", user_id="test",
                   metadata={"source": "salesforce"})
        tm.add_raw(content="No metadata", user_id="test")

        result = tm.search("data", user_id="test",
                           filters={"source": "salesforce"})
        assert len(result["results"]) == 1
        assert result["results"][0]["content"] == "With metadata"


# ------------------------------------------------------------------
# search_raw — pure cosine similarity search
# ------------------------------------------------------------------

class TestSearchRaw:
    """search_raw() returns results ranked by cosine similarity only."""

    def test_search_raw_basic(self, tm):
        """search_raw returns results with similarity ranking."""
        tm.add_raw(content="Alex likes sushi", user_id="test")
        result = tm.search_raw("sushi", user_id="test")
        assert "results" in result
        assert len(result["results"]) == 1
        r = result["results"][0]
        assert r["content"] == "Alex likes sushi"
        assert "similarity" in r
        assert "memory_id" in r

    def test_search_raw_no_relations(self, tm):
        """search_raw does not include graph relations."""
        tm.add_raw(content="Alex likes sushi", user_id="test")
        result = tm.search_raw("sushi", user_id="test")
        assert "relations" not in result

    def test_search_raw_includes_metadata(self, tm):
        """search_raw results include metadata field."""
        tm.add_raw(content="CRM data", user_id="test",
                   metadata={"source": "salesforce"})
        result = tm.search_raw("CRM", user_id="test")
        assert result["results"][0]["metadata"] == {"source": "salesforce"}

    def test_search_raw_with_filters(self, tm):
        """search_raw supports metadata pre-filters."""
        tm.add_raw(content="SF record", user_id="test",
                   metadata={"source": "salesforce"})
        tm.add_raw(content="HB record", user_id="test",
                   metadata={"source": "hubspot"})

        result = tm.search_raw("record", user_id="test",
                               filters={"source": "salesforce"})
        assert len(result["results"]) == 1
        assert result["results"][0]["content"] == "SF record"

    def test_search_raw_ranked_by_similarity_only(self, tm):
        """search_raw ignores confidence/source_trust for ranking."""
        # Low confidence but should still appear first if similarity is higher
        tm.add_raw(content="exact match query text", user_id="test",
                   confidence="low", source_trust="derived")
        tm.add_raw(content="unrelated content here", user_id="test",
                   confidence="high", source_trust="verified")

        result = tm.search_raw("exact match query text", user_id="test")
        # The exact match should be first regardless of confidence
        assert result["results"][0]["content"] == "exact match query text"

    def test_search_raw_returns_count(self, tm):
        """search_raw returns count field."""
        tm.add_raw(content="Fact A", user_id="test")
        tm.add_raw(content="Fact B", user_id="test")
        result = tm.search_raw("fact", user_id="test")
        assert result["count"] == 2
