"""Integration tests: write-path consistency for semantic search, vector store,
observation categorizer, and document import.

Fills coverage gaps identified in WRITE_INTEGRATION_TEST_REQUIREMENTS.md:
- Gap 1: SemanticSearch.index_row / index_table write persistence (HIGH)
- Gap 2: SqliteVecSearch.upsert / delete write persistence (MEDIUM)
- Gap 3: ObservationCategorizer.categorize_and_apply / categorize_batch (MEDIUM)
- Gap 4: DocumentStore.import_file write persistence (LOW)
- Gap 5: Cross-module search consistency (LOW)
- Gap 6: Semantic write idempotency (LOW)
"""

import sqlite3
from unittest.mock import MagicMock

import pytest
import sqlite_vec

from greymatter_core.categorizer import ObservationCategorizer
from greymatter_core.db import SQLiteDatabase
from greymatter_core.documents import DocumentStore
from greymatter_core.knowledge import KnowledgeStore
from greymatter_core.semantic import SemanticSearch
from greymatter_core.vector import SqliteVecSearch


@pytest.fixture
def db():
    """Shared in-memory database for write integration tests."""
    database = SQLiteDatabase(":memory:")
    yield database
    database.close()


@pytest.fixture
def raw_conn(db):
    """Raw sqlite3 connection for verification queries bypassing the DB wrapper."""
    return db._conn


@pytest.fixture
def mock_embedder():
    """Mock embedding provider with deterministic vectors."""
    embedder = MagicMock()
    embedder.dimensions = 768

    def fake_embed(text):
        vec = [0.0] * 768
        lower = text.lower()
        if "network" in lower:
            vec[0] = 1.0
        if "database" in lower:
            vec[1] = 1.0
        if "security" in lower:
            vec[2] = 1.0
        if "python" in lower:
            vec[3] = 1.0
        return vec

    embedder.embed = MagicMock(side_effect=fake_embed)
    embedder.embed_batch = MagicMock(
        side_effect=lambda texts: [fake_embed(t) for t in texts]
    )
    return embedder


@pytest.fixture
def search(db, mock_embedder):
    return SemanticSearch(db, mock_embedder)


@pytest.fixture
def knowledge(db):
    return KnowledgeStore(db)


@pytest.fixture
def doc_store(db):
    return DocumentStore(db)


@pytest.fixture
def vec_conn():
    conn = sqlite3.connect(":memory:")
    conn.enable_load_extension(True)
    sqlite_vec.load(conn)
    conn.enable_load_extension(False)
    conn.execute("""
        CREATE VIRTUAL TABLE vec_tests USING vec0(
            test_id TEXT PRIMARY KEY,
            embedding float[4]
        )
    """)
    conn.commit()
    return conn


@pytest.fixture
def sqlite_vs(vec_conn):
    return SqliteVecSearch(vec_conn)


@pytest.fixture
def mock_llm():
    llm = MagicMock()
    llm.generate_json.return_value = {
        "domains": ["python", "testing"],
        "suggested_category": "pattern",
        "category_changed": False,
        "confidence": 0.9,
    }
    return llm


class TestSemanticWriteIntegration:
    """Verify SemanticSearch write operations persist to vec, FTS, and chunk tables."""

    def test_index_row_persists_to_vec_and_fts(self, db, search):
        """index_row() writes to both vec table and FTS atomically."""
        db.execute(
            "INSERT INTO observations (id, session_id, trigger, category, title, content) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            ("obs-1", "sess-1", "compaction", "insight",
             "Network Design", "Network hub-and-spoke topology"),
        )
        search.index_row("observations", "obs-1", "Network Design Network hub-and-spoke topology")

        vec_row = db.execute_one(
            "SELECT COUNT(*) as c FROM vec_observations WHERE observation_id = ?",
            ("obs-1",),
        )
        assert vec_row["c"] == 1

        fts_rows = db.execute(
            "SELECT * FROM observations_fts WHERE observations_fts MATCH 'Network'"
        )
        assert len(fts_rows) >= 1

    def test_index_row_idempotent_reindex(self, db, search):
        """Re-indexing the same row replaces without duplicating."""
        db.execute(
            "INSERT INTO knowledge_entries (id, entry_type, title, content) "
            "VALUES (?, ?, ?, ?)",
            ("k-1", "expertise", "WAL Mode", "SQLite WAL for concurrency"),
        )
        search.index_row("knowledge", "k-1", "WAL Mode SQLite WAL for concurrency")
        search.index_row("knowledge", "k-1", "WAL Mode Updated content about WAL")

        count = db.execute_one(
            "SELECT COUNT(*) as c FROM vec_knowledge WHERE entry_id = ?", ("k-1",)
        )
        assert count["c"] == 1

    def test_index_table_batch_writes_all_rows(self, db, search):
        """index_table() indexes every unindexed row in the source table."""
        for i in range(5):
            db.execute(
                "INSERT INTO knowledge_entries (id, entry_type, title, content) "
                "VALUES (?, ?, ?, ?)",
                (f"k-{i}", "expertise", f"Topic {i}", f"Content about topic {i}"),
            )

        count = search.index_table("knowledge")
        assert count == 5

        total = db.execute_one("SELECT COUNT(*) as c FROM vec_knowledge")
        assert total["c"] == 5

    def test_index_row_after_knowledge_create(self, db, knowledge, search):
        """Cross-module: knowledge.create() followed by index_row() persists searchable entry."""
        entry = knowledge.create(
            entry_type="gotcha",
            title="Network Timeout",
            content="Always set network timeout to 30s",
            domain="infrastructure",
        )
        search.index_row(
            "knowledge", entry["id"],
            f"{entry['title']} {entry['content']}",
        )

        ke_row = db.execute_one(
            "SELECT * FROM knowledge_entries WHERE id = ?", (entry["id"],)
        )
        assert ke_row is not None
        assert ke_row["title"] == "Network Timeout"

        vec_count = db.execute_one(
            "SELECT COUNT(*) as c FROM vec_knowledge WHERE entry_id = ?",
            (entry["id"],),
        )
        assert vec_count["c"] == 1

        results = search.search("network timeout", tables=["knowledge"], limit=5)
        assert len(results) >= 1
        assert any(r["id"] == entry["id"] for r in results)


def _vec4(x, y, z, w):
    return [x, y, z, w]


class TestVectorWriteIntegration:
    """Verify SqliteVecSearch upsert/delete write persistence with real sqlite-vec."""

    def test_upsert_replaces_existing_vector(self, vec_conn, sqlite_vs):
        """upsert() on existing ID replaces the embedding without duplicating."""
        sqlite_vs.upsert("vec_tests", "id-1", _vec4(1.0, 0.0, 0.0, 0.0))
        assert sqlite_vs.count("vec_tests") == 1

        sqlite_vs.upsert("vec_tests", "id-1", _vec4(0.0, 1.0, 0.0, 0.0))
        assert sqlite_vs.count("vec_tests") == 1

        results = sqlite_vs.search("vec_tests", _vec4(0.0, 1.0, 0.0, 0.0), limit=1)
        assert len(results) == 1
        assert results[0]["id"] == "id-1"
        assert results[0]["distance"] < 0.01

        results_old = sqlite_vs.search("vec_tests", _vec4(1.0, 0.0, 0.0, 0.0), limit=1)
        assert results_old[0]["distance"] > 0.5

    def test_delete_removes_from_search(self, vec_conn, sqlite_vs):
        """delete() followed by search returns no results for that ID."""
        sqlite_vs.upsert("vec_tests", "id-1", _vec4(1.0, 0.0, 0.0, 0.0))
        sqlite_vs.upsert("vec_tests", "id-2", _vec4(0.0, 1.0, 0.0, 0.0))
        assert sqlite_vs.count("vec_tests") == 2

        sqlite_vs.delete("vec_tests", "id-1")
        assert sqlite_vs.count("vec_tests") == 1

        results = sqlite_vs.search("vec_tests", _vec4(1.0, 0.0, 0.0, 0.0), limit=5)
        ids = [r["id"] for r in results]
        assert "id-1" not in ids
        assert "id-2" in ids

    def test_upsert_then_delete_clean_state(self, vec_conn, sqlite_vs):
        """Full lifecycle: upsert -> verify -> delete -> verify gone."""
        sqlite_vs.upsert("vec_tests", "lifecycle-1", _vec4(1.0, 0.0, 0.0, 0.0))
        assert sqlite_vs.count("vec_tests") == 1

        results = sqlite_vs.search("vec_tests", _vec4(1.0, 0.0, 0.0, 0.0), limit=1)
        assert results[0]["id"] == "lifecycle-1"

        sqlite_vs.delete("vec_tests", "lifecycle-1")
        assert sqlite_vs.count("vec_tests") == 0

        results = sqlite_vs.search("vec_tests", _vec4(1.0, 0.0, 0.0, 0.0), limit=5)
        assert results == []


class TestCategorizerWriteIntegration:
    """Verify ObservationCategorizer writes domains to the observations table."""

    def test_categorize_and_apply_persists_domains(self, db, mock_llm):
        """categorize_and_apply() writes domains to observations table."""
        db.execute(
            "INSERT INTO observations (id, session_id, trigger, category, title, content, pruned) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            ("obs-cat-1", "s1", "compaction", "insight",
             "SQLite WAL", "WAL mode enables concurrent reads", 0),
        )

        categorizer = ObservationCategorizer(mock_llm)
        obs = {"id": "obs-cat-1", "title": "SQLite WAL",
               "content": "WAL mode enables concurrent reads", "category": "insight"}

        result = categorizer.categorize_and_apply(obs, db)
        assert result["domains"] == ["python", "testing"]

        row = db.execute_one(
            "SELECT domains, updated_at FROM observations WHERE id = ?",
            ("obs-cat-1",),
        )
        assert row is not None
        assert row["domains"] == "python,testing"
        assert row["updated_at"] is not None

    def test_categorize_batch_updates_all(self, db, mock_llm):
        """categorize_batch() updates all untagged observations."""
        for i in range(5):
            db.execute(
                "INSERT INTO observations "
                "(id, session_id, trigger, category, title, content, domains, pruned) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
                (f"obs-batch-{i}", "s1", "compaction", "insight",
                 f"Topic {i}", f"Content about topic {i}", "", 0),
            )

        categorizer = ObservationCategorizer(mock_llm)
        stats = categorizer.categorize_batch(db, batch_size=10, untagged_only=True)

        assert stats["processed"] == 5
        assert stats["tagged"] == 5
        assert stats["failed"] == 0

        rows = db.execute(
            "SELECT id, domains FROM observations WHERE domains != '' ORDER BY id"
        )
        assert len(rows) == 5
        for row in rows:
            assert row["domains"] == "python,testing"

    def test_categorize_and_apply_with_recategorize(self, db):
        """categorize_and_apply() with auto_recategorize updates category too."""
        mock_llm = MagicMock()
        mock_llm.generate_json.return_value = {
            "domains": ["database"],
            "suggested_category": "decision",
            "category_changed": True,
            "confidence": 0.95,
        }

        db.execute(
            "INSERT INTO observations (id, session_id, trigger, category, title, content, pruned) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            ("obs-recat-1", "s1", "compaction", "insight",
             "Schema Choice", "Chose SQLite over Postgres", 0),
        )

        categorizer = ObservationCategorizer(mock_llm, auto_recategorize=True)
        obs = {"id": "obs-recat-1", "title": "Schema Choice",
               "content": "Chose SQLite over Postgres", "category": "insight"}

        result = categorizer.categorize_and_apply(obs, db)
        assert result["category_changed"] is True

        row = db.execute_one(
            "SELECT category, domains FROM observations WHERE id = ?",
            ("obs-recat-1",),
        )
        assert row["category"] == "decision"
        assert row["domains"] == "database"


class TestDocumentImportWriteIntegration:
    """Verify DocumentStore.import_file() persists content and metadata."""

    def test_import_file_persists_content_and_metadata(self, db, doc_store, tmp_path):
        """import_file() reads file and persists document with correct metadata."""
        test_file = tmp_path / "design-doc.md"
        test_file.write_text("# Architecture\n\nThis is the design document.\n")

        doc = doc_store.import_file(
            file_path=str(test_file),
            doc_type="design",
            project="greymatter",
            tags="architecture,v2",
        )
        assert doc is not None
        assert doc["doc_type"] == "design"
        assert doc["title"] == "design-doc"
        assert doc["project"] == "greymatter"
        assert doc["tags"] == "architecture,v2"
        assert "# Architecture" in doc["content"]

        row = db.execute_one(
            "SELECT * FROM documents WHERE id = ?", (doc["id"],)
        )
        assert row is not None
        assert row["content"] == "# Architecture\n\nThis is the design document.\n"
        assert row["doc_type"] == "design"

    def test_import_file_computes_token_estimate(self, db, doc_store, tmp_path):
        """import_file() computes accurate token_estimate from file content."""
        content = "Word " * 400
        test_file = tmp_path / "long-doc.md"
        test_file.write_text(content)

        doc = doc_store.import_file(
            file_path=str(test_file), doc_type="research"
        )

        row = db.execute_one(
            "SELECT token_estimate FROM documents WHERE id = ?", (doc["id"],)
        )
        assert row is not None
        assert 400 <= row["token_estimate"] <= 600


class TestCrossModuleSearchWriteConsistency:
    """Verify that writes via managers + index_row produce consistent search results."""

    def test_knowledge_create_then_index_then_search(self, db, knowledge, search):
        """Full pipeline: knowledge.create() -> index_row() -> search() returns result."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Database Indexing",
            content="B-tree indexes speed up WHERE clauses on SQLite",
            domain="database",
        )

        search.index_row(
            "knowledge", entry["id"],
            f"{entry['title']} {entry['content']}",
        )

        results = search.search("database indexing B-tree", tables=["knowledge"], limit=5)
        assert len(results) >= 1
        match = results[0]
        assert match["id"] == entry["id"]
        assert match["source_type"] == "knowledge"
        assert match["title"] == "Database Indexing"

    def test_document_create_then_index_table_then_search(self, db, doc_store, search):
        """Full pipeline: doc_store.create() -> index_table() -> search() finds it."""
        doc = doc_store.create(
            doc_type="design",
            title="Security Architecture",
            content="Zero-trust network architecture with mutual TLS between all services. "
                    "Security boundaries at every hop.",
            project="greymatter",
        )

        count = search.index_table("documents")
        assert count == 1

        results = search.search("security architecture", tables=["documents"], limit=5)
        assert len(results) >= 1
        assert any(r["id"] == doc["id"] for r in results)


class TestSemanticWriteIdempotency:
    """Verify that re-indexing and batch re-indexing are idempotent."""

    def test_reindex_same_row_does_not_duplicate(self, db, search):
        """Calling index_row() multiple times on the same row does not create duplicates."""
        db.execute(
            "INSERT INTO knowledge_entries (id, entry_type, title, content) "
            "VALUES (?, ?, ?, ?)",
            ("k-idem", "gotcha", "Idempotency Test", "Content for idempotency"),
        )

        search.index_row("knowledge", "k-idem", "Idempotency Test Content for idempotency")
        search.index_row("knowledge", "k-idem", "Idempotency Test Content for idempotency")
        search.index_row("knowledge", "k-idem", "Idempotency Test Content for idempotency")

        count = db.execute_one("SELECT COUNT(*) as c FROM vec_knowledge")
        assert count["c"] == 1

    def test_index_table_skips_already_indexed_rows(self, db, search):
        """index_table() only indexes rows that are not yet in the vec table."""
        for i in range(3):
            db.execute(
                "INSERT INTO knowledge_entries (id, entry_type, title, content) "
                "VALUES (?, ?, ?, ?)",
                (f"k-skip-{i}", "expertise", f"Topic {i}", f"Content {i}"),
            )

        count_first = search.index_table("knowledge")
        assert count_first == 3

        db.execute(
            "INSERT INTO knowledge_entries (id, entry_type, title, content) "
            "VALUES (?, ?, ?, ?)",
            ("k-skip-new", "expertise", "New Topic", "New content"),
        )

        count_second = search.index_table("knowledge")
        assert count_second == 1

        total = db.execute_one("SELECT COUNT(*) as c FROM vec_knowledge")
        assert total["c"] == 4
