"""Independent memory tests for knowledge plane components.

These tests run without external services (Ollama, LanceDB daemon). They cover:

- TextChunker: text splitting into chunks
- DeduplicationEngine: content-hash and URL duplicate detection
- KnowledgeNavigator: topic/tag navigation, clustering, keyword search
- SQLiteKnowledgeRepository: idempotent metadata persistence
- EmbeddingConfig: configuration validation
- KnowledgeDocument/KnowledgeSource contracts
"""

from __future__ import annotations

import hashlib
from pathlib import Path

import pytest

from cyberbrain26.data_plane.sqlite_store import SQLiteDatabase
from cyberbrain26.knowledge_plane import (
    DeduplicationEngine,
    DocumentRegistration,
    DocumentStatus,
    DocumentTrust,
    EmbeddingConfig,
    KnowledgeDocument,
    KnowledgeNavigator,
    KnowledgeSource,
    Sensitivity,
    SQLiteKnowledgeRepository,
    TextChunker,
)


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


def _approved_source() -> KnowledgeSource:
    return KnowledgeSource(
        source_id="local-notes",
        source_uri="file:///knowledge/notes",
        trust=DocumentTrust.REVIEWED,
        sensitivity=Sensitivity.PERSONAL,
        approved=True,
    )


def _unapproved_source() -> KnowledgeSource:
    return KnowledgeSource(
        source_id="unknown",
        source_uri="file:///unknown",
    )


# ---------------------------------------------------------------------------
# TextChunker tests
# ---------------------------------------------------------------------------


def test_chunker_splits_long_text_into_multiple_chunks() -> None:
    config = EmbeddingConfig(chunk_size_tokens=5, chunk_overlap_tokens=2)
    chunker = TextChunker(config)
    chunks = chunker.chunk_text("word " * 20, source_path="<memory>")
    assert len(chunks) > 1
    assert all(isinstance(c.text, str) for c in chunks)
    assert all(c.chunk_index == i for i, c in enumerate(chunks))


def test_chunker_overlap_preserved() -> None:
    config = EmbeddingConfig(chunk_size_tokens=4, chunk_overlap_tokens=1)
    chunker = TextChunker(config)
    chunks = chunker.chunk_text("one two three four five six", source_path="<memory>")
    assert len(chunks) > 1
    assert chunks[0].text == "one two three four"
    assert chunks[1].text == "four five six"


def test_chunker_overlap_not_exceeding_size_produces_single_chunk() -> None:
    config = EmbeddingConfig(chunk_size_tokens=5, chunk_overlap_tokens=10)
    chunker = TextChunker(config)
    chunks = chunker.chunk_text("a b c d e f g h i j", source_path="<memory>")
    assert len(chunks) == 1


def test_chunker_from_file(tmp_path: Path) -> None:
    file_path = tmp_path / "test.txt"
    file_path.write_text("word " * 20, encoding="utf-8")
    config = EmbeddingConfig(chunk_size_tokens=5, chunk_overlap_tokens=2)
    chunker = TextChunker(config)
    chunks = chunker.chunk_file(file_path)
    assert len(chunks) > 1
    assert all(c.source_path == str(file_path) for c in chunks)


# ---------------------------------------------------------------------------
# DeduplicationEngine tests
# ---------------------------------------------------------------------------


def _doc(document_id: str, content: bytes, source_uri: str) -> KnowledgeDocument:
    return KnowledgeDocument(
        document_id=document_id,
        source_id="src1",
        source_uri=source_uri,
        content_sha256=hashlib.sha256(content).hexdigest(),
        media_type="text/plain",
        trust=DocumentTrust.EXTERNAL,
        sensitivity=Sensitivity.PERSONAL,
        status=DocumentStatus.REGISTERED,
        metadata={},
        created_at=0.0,
        updated_at=0.0,
    )


def test_deduplication_exact_hash() -> None:
    engine = DeduplicationEngine()
    content = b"hello world"
    doc = _doc("doc1", content, "file:///test.txt")
    result = engine.check_duplicate(content, "file:///test.txt", [doc])
    assert result.is_duplicate is True
    assert result.recommended_action == "skip"
    assert len(result.candidates) == 1
    assert result.candidates[0].match_type == "exact_hash"


def test_deduplication_no_duplicate() -> None:
    engine = DeduplicationEngine()
    content = b"hello world"
    result = engine.check_duplicate(content, "file:///test.txt", [])
    assert result.is_duplicate is False
    assert result.recommended_action == "register"


def test_deduplication_batch() -> None:
    engine = DeduplicationEngine()
    items = [(b"hello", "url1"), (b"world", "url2"), (b"hello", "url3")]
    results = engine.deduplicate_batch(items, [])
    assert len(results) == 3
    assert results[0][1].is_duplicate is False
    assert results[1][1].is_duplicate is False
    assert results[2][1].is_duplicate is True


# ---------------------------------------------------------------------------
# KnowledgeNavigator tests
# ---------------------------------------------------------------------------


def test_navigator_topics_and_tags() -> None:
    nav = KnowledgeNavigator()
    doc = KnowledgeDocument(
        document_id="doc1",
        source_id="src1",
        source_uri="file:///test.txt",
        content_sha256="abc",
        media_type="text/plain",
        trust=DocumentTrust.EXTERNAL,
        sensitivity=Sensitivity.PERSONAL,
        status=DocumentStatus.REGISTERED,
        metadata={"topic": "research", "tags": "ai,ml"},
        created_at=0.0,
        updated_at=0.0,
    )
    nav.add_document(doc)
    assert "research" in nav.get_topics()
    assert nav.get_documents_by_topic("research") == ["doc1"]
    assert "ai" in nav.get_all_tags()
    assert "ml" in nav.get_all_tags()


def test_navigator_search_by_keyword() -> None:
    nav = KnowledgeNavigator()
    doc = KnowledgeDocument(
        document_id="doc1",
        source_id="src1",
        source_uri="file:///test.txt",
        content_sha256="abc",
        media_type="text/plain",
        trust=DocumentTrust.EXTERNAL,
        sensitivity=Sensitivity.PERSONAL,
        status=DocumentStatus.REGISTERED,
        metadata={"description": "AI research paper"},
        created_at=0.0,
        updated_at=0.0,
    )
    nav.add_document(doc)
    results = nav.search_by_keyword("ai", [doc])
    assert "doc1" in results


def test_navigator_cluster_by_source() -> None:
    nav = KnowledgeNavigator()
    docs = [
        KnowledgeDocument(
            document_id="doc1",
            source_id="src1",
            source_uri="https://example.com/page1",
            content_sha256="abc",
            media_type="text/plain",
            trust=DocumentTrust.EXTERNAL,
            sensitivity=Sensitivity.PERSONAL,
            status=DocumentStatus.REGISTERED,
            metadata={},
            created_at=0.0,
            updated_at=0.0,
        ),
        KnowledgeDocument(
            document_id="doc2",
            source_id="src2",
            source_uri="https://other.com/page2",
            content_sha256="def",
            media_type="text/plain",
            trust=DocumentTrust.EXTERNAL,
            sensitivity=Sensitivity.PERSONAL,
            status=DocumentStatus.REGISTERED,
            metadata={},
            created_at=0.0,
            updated_at=0.0,
        ),
    ]
    clusters = nav.cluster_by_source(docs)
    assert "example.com" in clusters
    assert "other.com" in clusters


def test_navigator_default_topic_uncategorized() -> None:
    nav = KnowledgeNavigator()
    doc = KnowledgeDocument(
        document_id="doc1",
        source_id="src1",
        source_uri="file:///test.txt",
        content_sha256="abc",
        media_type="text/plain",
        trust=DocumentTrust.EXTERNAL,
        sensitivity=Sensitivity.PERSONAL,
        status=DocumentStatus.REGISTERED,
        metadata={},
        created_at=0.0,
        updated_at=0.0,
    )
    nav.add_document(doc)
    assert "uncategorized" in nav.get_topics()


# ---------------------------------------------------------------------------
# SQLiteKnowledgeRepository tests
# ---------------------------------------------------------------------------


def test_repository_register_idempotent(tmp_path: Path) -> None:
    database = SQLiteDatabase(tmp_path / "knowledge.sqlite3")
    repository = SQLiteKnowledgeRepository(database)
    source = _approved_source()
    content = b"Private meeting note"
    registration = DocumentRegistration(
        source=source,
        content=content,
        media_type="text/plain",
        metadata={"filename": "meeting.txt"},
    )

    document, created = repository.register(registration)
    repeated, repeated_created = repository.register(registration)

    assert created is True
    assert repeated_created is False
    assert repeated.document_id == document.document_id
    assert document.status is DocumentStatus.REGISTERED
    assert document.content_sha256 == hashlib.sha256(content).hexdigest()


def test_repository_changed_content_is_new_version(tmp_path: Path) -> None:
    repository = SQLiteKnowledgeRepository(SQLiteDatabase(tmp_path / "knowledge.sqlite3"))
    source = _approved_source()
    first, first_created = repository.register(
        DocumentRegistration(source=source, content=b"version one", media_type="text/plain")
    )
    second, second_created = repository.register(
        DocumentRegistration(source=source, content=b"version two", media_type="text/plain")
    )

    assert first_created is True
    assert second_created is True
    assert first.document_id != second.document_id
    assert first.content_sha256 != second.content_sha256


def test_repository_unapproved_source_rejected(tmp_path: Path) -> None:
    repository = SQLiteKnowledgeRepository(SQLiteDatabase(tmp_path / "knowledge.sqlite3"))
    source = _unapproved_source()
    with pytest.raises(PermissionError, match="not approved"):
        repository.register(
            DocumentRegistration(source=source, content=b"data", media_type="text/plain")
        )


def test_repository_rejects_empty_content(tmp_path: Path) -> None:
    repository = SQLiteKnowledgeRepository(SQLiteDatabase(tmp_path / "knowledge.sqlite3"))
    source = _approved_source()
    with pytest.raises(ValueError, match="content must not be empty"):
        repository.register(
            DocumentRegistration(source=source, content=b"", media_type="text/plain")
        )


def test_repository_get_by_document_id(tmp_path: Path) -> None:
    repository = SQLiteKnowledgeRepository(SQLiteDatabase(tmp_path / "knowledge.sqlite3"))
    source = _approved_source()
    document, _ = repository.register(
        DocumentRegistration(source=source, content=b"find me", media_type="text/plain")
    )

    retrieved = repository.get(document.document_id)
    assert retrieved is not None
    assert retrieved.document_id == document.document_id
    assert retrieved.content_sha256 == hashlib.sha256(b"find me").hexdigest()


def test_repository_get_nonexistent_returns_none(tmp_path: Path) -> None:
    repository = SQLiteKnowledgeRepository(SQLiteDatabase(tmp_path / "knowledge.sqlite3"))
    assert repository.get("document_nonexistent") is None


# ---------------------------------------------------------------------------
# EmbeddingConfig tests
# ---------------------------------------------------------------------------


def test_embedding_config_defaults() -> None:
    config = EmbeddingConfig()
    assert config.model == "qwen3-embedding:0.6b"
    assert config.vector_dim == 1024
    assert config.max_context == 32_000
    assert config.chunk_size_tokens == 512
    assert config.chunk_overlap_tokens == 100
    assert config.effective_dim == 1024


def test_embedding_config_custom(tmp_path: Path) -> None:
    config = EmbeddingConfig(
        model="custom-model",
        vector_dim=512,
        chunk_size_tokens=256,
        lancedb_directory=tmp_path / "custom_lancedb",
    )
    assert config.model == "custom-model"
    assert config.vector_dim == 512
    assert config.chunk_size_tokens == 256
    assert config.effective_dim == 512


# ---------------------------------------------------------------------------
# Contracts tests
# ---------------------------------------------------------------------------


def test_knowledge_source_defaults() -> None:
    source = KnowledgeSource(source_id="s", source_uri="file:///s")
    assert source.approved is False
    assert source.trust is DocumentTrust.EXTERNAL
    assert source.sensitivity is Sensitivity.PERSONAL


def test_knowledge_document_roundtrip() -> None:
    doc = KnowledgeDocument(
        document_id="doc1",
        source_id="src1",
        source_uri="file:///test.txt",
        content_sha256="abc",
        media_type="text/plain",
        trust=DocumentTrust.TRUSTED,
        sensitivity=Sensitivity.SENSITIVE,
        status=DocumentStatus.INDEXED,
        metadata={"key": "value"},
        created_at=123.0,
        updated_at=456.0,
    )
    assert doc.document_id == "doc1"
    assert doc.trust is DocumentTrust.TRUSTED
    assert doc.sensitivity is Sensitivity.SENSITIVE
    assert doc.status is DocumentStatus.INDEXED
    assert doc.metadata == {"key": "value"}
    assert doc.created_at == 123.0
    assert doc.updated_at == 456.0

