"""Tests for MemoryUnit dataclass and HU3-prime text interchange protocol.

Verifies:
    - Portable field round-trips (create → to_portable → from_portable)
    - Embedding is stripped on portable export
    - Full JSON serialization round-trip
    - Batch export/import via interchange module
    - Edge cases: None fields, empty metadata, missing optional fields
    - to_engram_store_args() mapping
    - from_search_result() factory
"""

from __future__ import annotations

from datetime import UTC, datetime

import pytest

from claude_engram.interchange import export_memories, import_memories
from claude_engram.memory_unit import MemoryUnit


class TestMemoryUnitPortable:
    """Test portable field round-trip — content preserved, embedding lost."""

    def test_round_trip_preserves_content(self) -> None:
        unit = MemoryUnit(
            content="Engram uses MiniLM-L6-v2 (384d)",
            category="decision",
            metadata={"source": "HU3-research"},
            created_at=datetime(2026, 2, 10, tzinfo=UTC),
            scope="proj:starchart",
            source_system="engram",
            embedding=[0.1, 0.2, 0.3],  # local field
            id=42,  # local field
            similarity_score=0.95,  # local field
        )

        portable = unit.to_portable()
        restored = MemoryUnit.from_portable(portable)

        assert restored.content == unit.content
        assert restored.category == unit.category
        assert restored.metadata == unit.metadata
        assert restored.created_at == unit.created_at
        assert restored.scope == unit.scope
        assert restored.source_system == unit.source_system

    def test_round_trip_strips_local_fields(self) -> None:
        unit = MemoryUnit(
            content="test",
            embedding=[0.1, 0.2, 0.3],
            id=42,
            similarity_score=0.95,
            superseded_by=99,
            token_count=5,
        )

        portable = unit.to_portable()
        restored = MemoryUnit.from_portable(portable)

        assert restored.embedding is None
        assert restored.id is None
        assert restored.similarity_score is None
        assert restored.superseded_by is None
        assert restored.token_count is None
        assert restored.is_active is True

    def test_portable_dict_has_no_local_keys(self) -> None:
        unit = MemoryUnit(
            content="test",
            embedding=[0.1],
            id=1,
            similarity_score=0.5,
        )
        portable = unit.to_portable()

        local_keys = {
            "id",
            "embedding",
            "similarity_score",
            "is_active",
            "superseded_by",
            "token_count",
        }
        assert not local_keys & portable.keys()

    def test_valid_from_valid_to_round_trip(self) -> None:
        vf = datetime(2026, 1, 1, tzinfo=UTC)
        vt = datetime(2026, 12, 31, tzinfo=UTC)
        unit = MemoryUnit(content="temporal", valid_from=vf, valid_to=vt)

        portable = unit.to_portable()
        assert "valid_from" in portable
        assert "valid_to" in portable

        restored = MemoryUnit.from_portable(portable)
        assert restored.valid_from == vf
        assert restored.valid_to == vt

    def test_none_temporal_fields_omitted_from_portable(self) -> None:
        unit = MemoryUnit(content="no temporal")
        portable = unit.to_portable()
        assert "valid_from" not in portable
        assert "valid_to" not in portable


class TestMemoryUnitJson:
    """Test full JSON serialization round-trip."""

    def test_json_round_trip_all_fields(self) -> None:
        unit = MemoryUnit(
            content="full json test",
            category="lesson",
            metadata={"key": "value", "nested": {"a": 1}},
            created_at=datetime(2026, 2, 10, 12, 0, 0, tzinfo=UTC),
            valid_from=datetime(2026, 1, 1, tzinfo=UTC),
            scope="proj:test",
            source_system="engram",
            id=7,
            embedding=[0.1, 0.2, 0.3, 0.4],
            similarity_score=0.88,
            is_active=True,
            superseded_by=None,
            token_count=10,
        )

        json_str = unit.to_json()
        restored = MemoryUnit.from_json(json_str)

        assert restored.content == unit.content
        assert restored.category == unit.category
        assert restored.metadata == unit.metadata
        assert restored.created_at == unit.created_at
        assert restored.valid_from == unit.valid_from
        assert restored.scope == unit.scope
        assert restored.id == unit.id
        assert restored.embedding == unit.embedding
        assert restored.similarity_score == unit.similarity_score
        assert restored.token_count == unit.token_count

    def test_json_round_trip_none_embedding(self) -> None:
        unit = MemoryUnit(content="no embedding")
        json_str = unit.to_json()
        restored = MemoryUnit.from_json(json_str)

        assert restored.embedding is None
        assert restored.content == "no embedding"

    def test_json_round_trip_inactive_superseded(self) -> None:
        unit = MemoryUnit(
            content="old decision",
            is_active=False,
            superseded_by=42,
        )
        json_str = unit.to_json()
        restored = MemoryUnit.from_json(json_str)

        assert restored.is_active is False
        assert restored.superseded_by == 42


class TestMemoryUnitEdgeCases:
    """Edge cases: empty metadata, missing fields, defaults."""

    def test_empty_metadata(self) -> None:
        unit = MemoryUnit(content="bare minimum")
        portable = unit.to_portable()
        assert portable["metadata"] == {}

        restored = MemoryUnit.from_portable(portable)
        assert restored.metadata == {}

    def test_from_portable_missing_optional_fields(self) -> None:
        minimal = {"content": "just content"}
        unit = MemoryUnit.from_portable(minimal)

        assert unit.content == "just content"
        assert unit.category == "general"
        assert unit.metadata == {}
        assert unit.scope == ""
        assert unit.source_system == "unknown"
        assert unit.valid_from is None
        assert unit.valid_to is None

    def test_from_portable_unknown_source_system(self) -> None:
        data = {"content": "from supermemory", "source_system": "supermemory"}
        unit = MemoryUnit.from_portable(data)
        assert unit.source_system == "supermemory"

    def test_default_source_system_is_engram(self) -> None:
        unit = MemoryUnit(content="default")
        assert unit.source_system == "engram"


class TestToEngramStoreArgs:
    """Test conversion to storage.store() kwargs."""

    def test_basic_store_args(self) -> None:
        unit = MemoryUnit(
            content="store me",
            category="decision",
            metadata={"key": "val"},
            scope="proj:test",
        )
        args = unit.to_engram_store_args()

        assert args["content"] == "store me"
        assert args["category"] == "decision"
        assert args["metadata"] == {"key": "val"}
        assert args["proj_tag"] == "proj:test"

    def test_store_args_global_scope(self) -> None:
        unit = MemoryUnit(content="global", scope="")
        args = unit.to_engram_store_args()

        assert "proj_tag" not in args

    def test_store_args_empty_metadata_is_none(self) -> None:
        unit = MemoryUnit(content="no meta", metadata={})
        args = unit.to_engram_store_args()
        assert args["metadata"] is None


class TestFromSearchResult:
    """Test factory from storage.search() result dicts."""

    def test_from_typical_search_result(self) -> None:
        result = {
            "id": 5,
            "content": "found memory",
            "category": "lesson",
            "metadata": {"tag": "test"},
            "created_at": "2026-02-10T12:00:00+00:00",
            "proj_tag": "proj:starchart",
            "similarity": 0.92,
            "token_count": 15,
            "is_active": True,
        }
        unit = MemoryUnit.from_search_result(result)

        assert unit.id == 5
        assert unit.content == "found memory"
        assert unit.category == "lesson"
        assert unit.metadata == {"tag": "test"}
        assert unit.scope == "proj:starchart"
        assert unit.similarity_score == 0.92
        assert unit.token_count == 15
        assert unit.is_active is True
        assert unit.embedding is None  # search results don't include embeddings

    def test_from_search_result_null_proj_tag(self) -> None:
        result = {
            "id": 1,
            "content": "global",
            "category": "general",
            "created_at": "2026-01-01T00:00:00+00:00",
            "proj_tag": None,
            "similarity": 0.5,
        }
        unit = MemoryUnit.from_search_result(result)
        assert unit.scope == ""


class TestInterchangeExportImport:
    """Test batch export/import via interchange module."""

    def _make_units(self) -> list[MemoryUnit]:
        return [
            MemoryUnit(
                content="First memory",
                category="decision",
                metadata={"idx": 0},
                created_at=datetime(2026, 2, 10, tzinfo=UTC),
                scope="proj:test",
                embedding=[0.1, 0.2],  # should be stripped on export
            ),
            MemoryUnit(
                content="Second memory",
                category="lesson",
                metadata={"idx": 1},
                created_at=datetime(2026, 2, 11, tzinfo=UTC),
            ),
            MemoryUnit(
                content="Third memory",
                category="general",
                created_at=datetime(2026, 2, 12, tzinfo=UTC),
                source_system="supermemory",
            ),
        ]

    def test_json_export_import_round_trip(self) -> None:
        units = self._make_units()
        exported = export_memories(units, format="json")
        imported = import_memories(exported, format="json")

        assert len(imported) == 3
        for orig, restored in zip(units, imported, strict=True):
            assert restored.content == orig.content
            assert restored.category == orig.category
            assert restored.metadata == orig.metadata
            assert restored.created_at == orig.created_at
            assert restored.embedding is None  # stripped

    def test_jsonl_export_import_round_trip(self) -> None:
        units = self._make_units()
        exported = export_memories(units, format="jsonl")

        # JSONL should have one line per memory
        lines = exported.strip().splitlines()
        assert len(lines) == 3

        imported = import_memories(exported, format="jsonl")
        assert len(imported) == 3
        assert imported[0].content == "First memory"
        assert imported[2].source_system == "supermemory"

    def test_empty_list_export(self) -> None:
        assert export_memories([], format="json") == "[]"
        assert export_memories([], format="jsonl") == ""

    def test_empty_string_import(self) -> None:
        assert import_memories("[]", format="json") == []
        assert import_memories("", format="jsonl") == []

    def test_invalid_format_raises(self) -> None:
        with pytest.raises(ValueError, match="Unsupported format"):
            export_memories([], format="csv")

        with pytest.raises(ValueError, match="Unsupported format"):
            import_memories("", format="xml")

    def test_preserves_source_system_across_systems(self) -> None:
        """Key HU3-prime scenario: import from external system, source_system preserved."""
        external_data = [
            {
                "content": "Memory from Supermemory",
                "category": "decision",
                "source_system": "supermemory",
                "created_at": "2026-02-10T00:00:00+00:00",
            }
        ]
        import json

        json_str = json.dumps(external_data)
        imported = import_memories(json_str, format="json")

        assert len(imported) == 1
        assert imported[0].source_system == "supermemory"
        assert imported[0].content == "Memory from Supermemory"

        # Re-export preserves provenance
        re_exported = export_memories(imported, format="json")
        re_imported = import_memories(re_exported, format="json")
        assert re_imported[0].source_system == "supermemory"
