"""Tests for knowledge ingestion orchestrator."""

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

from genesis.knowledge.distillation import DistillationPipeline, KnowledgeUnit
from genesis.knowledge.manifest import ManifestManager
from genesis.knowledge.orchestrator import KnowledgeOrchestrator
from genesis.knowledge.processors.registry import ContentProcessorRegistry
from genesis.knowledge.processors.text import TextProcessor


def _make_orchestrator(tmp_path: Path, mock_distill_result: list[KnowledgeUnit] | None = None):
    """Build an orchestrator with a mock distillation pipeline."""
    registry = ContentProcessorRegistry()
    text = TextProcessor()
    registry.register_extensions(text, [".txt", ".md"])

    mock_router = MagicMock()
    distillation = DistillationPipeline(router=mock_router)

    # Mock the distill method
    if mock_distill_result is not None:
        distillation.distill = AsyncMock(return_value=mock_distill_result)

    manifest = ManifestManager(root=tmp_path / "knowledge")

    return KnowledgeOrchestrator(
        registry=registry,
        distillation=distillation,
        manifest=manifest,
    )


async def test_ingest_unknown_source(tmp_path: Path):
    """Unknown source type returns error."""
    orch = _make_orchestrator(tmp_path)
    result = await orch.ingest_source("file.xyz", project_type="test")
    assert result.error is not None
    assert "No processor" in result.error


async def test_ingest_missing_file(tmp_path: Path):
    """Missing file returns processing error."""
    orch = _make_orchestrator(tmp_path)
    result = await orch.ingest_source("/nonexistent/file.txt", project_type="test")
    assert result.error is not None
    assert "Processing failed" in result.error


async def test_ingest_empty_content(tmp_path: Path):
    """Empty file returns quality flag."""
    orch = _make_orchestrator(tmp_path)
    empty_file = tmp_path / "empty.txt"
    empty_file.write_text("")
    result = await orch.ingest_source(str(empty_file), project_type="test")
    assert "empty_content" in result.quality_flags


async def test_ingest_duplicate_detection(tmp_path: Path):
    """Second ingestion of same source returns cached result."""
    units = [KnowledgeUnit(concept="Test", body="Test body", domain="test")]
    orch = _make_orchestrator(tmp_path, mock_distill_result=units)

    # Mock the storage
    with patch("genesis.knowledge.orchestrator.KnowledgeOrchestrator._store_units",
               new_callable=AsyncMock, return_value=["unit-1"]):
        file = tmp_path / "doc.txt"
        file.write_text("Some meaningful content here.")

        r1 = await orch.ingest_source(str(file), project_type="test")
        assert r1.units_created == 1

        r2 = await orch.ingest_source(str(file), project_type="test")
        assert r2.units_created == 0
        assert "duplicate_source" in r2.quality_flags


async def test_ingest_no_units_extracted(tmp_path: Path):
    """Distillation producing zero units flags appropriately."""
    orch = _make_orchestrator(tmp_path, mock_distill_result=[])
    file = tmp_path / "notes.txt"
    file.write_text("Some content that produces nothing meaningful.")
    result = await orch.ingest_source(str(file), project_type="test")
    assert result.units_created == 0
    assert "no_units_extracted" in result.quality_flags


async def test_batch_ingest(tmp_path: Path):
    """Batch ingestion processes all supported files."""
    orch = _make_orchestrator(tmp_path, mock_distill_result=[])

    # Create test files
    (tmp_path / "a.txt").write_text("File A content")
    (tmp_path / "b.md").write_text("File B content")
    (tmp_path / "c.xyz").write_text("Unsupported")

    results = await orch.ingest_batch(str(tmp_path), project_type="test")
    # Should process a.txt and b.md but skip c.xyz
    assert len(results) == 2


async def test_thin_extraction_quality_flag(tmp_path: Path):
    """Thin extraction should produce a quality flag."""
    units = [KnowledgeUnit(concept="Thin", body="Short.", domain="test")]
    orch = _make_orchestrator(tmp_path, mock_distill_result=units)

    # Simulate a low extraction ratio on the distillation pipeline
    orch._distillation.last_extraction_ratio = 0.02  # 2% — below 10% floor

    with patch("genesis.knowledge.orchestrator.KnowledgeOrchestrator._store_units",
               new_callable=AsyncMock, return_value=["unit-1"]):
        file = tmp_path / "big_doc.txt"
        file.write_text("A" * 10000)  # Large input

        result = await orch.ingest_source(str(file), project_type="test")
        assert result.units_created == 1
        assert "thin_extraction" in result.quality_flags


async def test_store_units_rollback_on_failure(tmp_path: Path):
    """When _store_units fails mid-batch, SQLite is rolled back and Qdrant vectors are cleaned up."""
    units = [
        KnowledgeUnit(
            domain="test", concept=f"concept_{i}", body=f"body {i}",
            tags=["t"], confidence=0.9,
        )
        for i in range(3)
    ]
    orch = _make_orchestrator(tmp_path, mock_distill_result=units)

    # Mock the memory module internals that _store_units uses
    mock_db = AsyncMock()
    mock_store = MagicMock()
    # store() succeeds for first 2 calls, then the 3rd SQLite insert fails
    mock_store.store = AsyncMock(side_effect=["qid-0", "qid-1", "qid-2"])
    mock_store._qdrant = MagicMock()
    mock_store._embeddings = MagicMock(model_name="test-model")

    mock_knowledge = MagicMock()
    # find_by_unique_key returns None (no existing unit) for all calls
    mock_knowledge.find_by_unique_key = AsyncMock(return_value=None)
    # SQLite upsert succeeds twice, then raises on the 3rd
    mock_knowledge.upsert = AsyncMock(
        side_effect=[("uid-0", True), ("uid-1", True), Exception("DB locked")]
    )

    with patch("genesis.mcp.memory_mcp._require_init"), \
         patch("genesis.mcp.memory_mcp._store", mock_store), \
         patch("genesis.mcp.memory_mcp._db", mock_db), \
         patch("genesis.mcp.memory_mcp.knowledge", mock_knowledge), \
         patch("genesis.qdrant.collections.delete_point") as mock_delete_point:

        file = tmp_path / "test.txt"
        file.write_text("some content")

        result = await orch.ingest_source(str(file), project_type="test")

        # Storage failed — should return error result (S2 fix)
        assert result.error is not None
        assert "Storage failed" in result.error
        assert result.units_created == 0

        # SQLite should have been rolled back
        mock_db.rollback.assert_awaited_once()
        # commit should NOT have been called (failed before reaching it)
        mock_db.commit.assert_not_awaited()

        # All 3 Qdrant vectors should be compensation-deleted
        assert mock_delete_point.call_count == 3
        deleted_ids = [call.kwargs["point_id"] for call in mock_delete_point.call_args_list]
        assert deleted_ids == ["qid-0", "qid-1", "qid-2"]
