"""
Tests covering missing branches/lines in ResearchHistoryIndexer.

Targets lines: 94, 100, 110, 118-120, 127-129, 234-235, 242-245, 286-291
and the module-level auto_convert_research function.
"""

import uuid
from contextlib import contextmanager
from unittest.mock import patch

import pytest
from sqlalchemy.exc import IntegrityError

from local_deep_research.database.models.library import (
    Collection,
    SourceType,
)
from local_deep_research.database.models.research import ResearchHistory
from local_deep_research.research_library.search.services.research_history_indexer import (
    ResearchHistoryIndexer,
    auto_convert_research,
)

INDEXER_MODULE = "local_deep_research.research_library.search.services.research_history_indexer"


@pytest.fixture
def mock_session_ctx(library_session):
    @contextmanager
    def _mock_session(*args, **kwargs):
        yield library_session

    with patch(f"{INDEXER_MODULE}.get_user_db_session", _mock_session):
        yield library_session


@pytest.fixture
def indexer(mock_session_ctx):
    return ResearchHistoryIndexer("testuser", db_password=None)


@pytest.fixture
def report_source_type(library_session):
    st = SourceType(
        id=str(uuid.uuid4()),
        name="research_report",
        display_name="Research Report",
        description="Report",
        icon="fas fa-file",
    )
    library_session.add(st)
    library_session.commit()
    return st


@pytest.fixture
def collection(library_session):
    coll = Collection(
        id=str(uuid.uuid4()),
        name="Research History",
        description="Auto-indexed",
        is_default=False,
        collection_type="research_history",
    )
    library_session.add(coll)
    library_session.commit()
    return coll


def _make_research(
    session, status="completed", report_content="# Report\n\nContent"
):
    r = ResearchHistory(
        id=str(uuid.uuid4()),
        query="Test query",
        mode="detailed_report",
        status=status,
        created_at="2025-01-15T10:00:00",
        report_content=report_content,
        title="Test Research",
    )
    session.add(r)
    session.commit()
    return r


class TestIndexResearchNotCompleted:
    """Line 94."""

    def test_returns_error_when_not_completed(
        self, indexer, mock_session_ctx, collection
    ):
        research = _make_research(mock_session_ctx, status="in_progress")
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            result = indexer.index_research(research.id)
        assert result["status"] == "error"
        assert "not yet completed" in result["error"]


class TestIndexResearchNoContent:
    """Line 100."""

    def test_returns_error_when_content_none(
        self, indexer, mock_session_ctx, collection
    ):
        research = _make_research(mock_session_ctx, report_content=None)
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            result = indexer.index_research(research.id)
        assert result["status"] == "error"
        assert "no report content" in result["error"].lower()

    def test_returns_error_when_content_empty(
        self, indexer, mock_session_ctx, collection
    ):
        research = _make_research(mock_session_ctx, report_content="")
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            result = indexer.index_research(research.id)
        assert result["status"] == "error"
        assert "no report content" in result["error"].lower()


class TestIndexResearchSourceTypeMissing:
    """Line 110."""

    def test_returns_error_when_no_source_type(
        self, indexer, mock_session_ctx, collection
    ):
        research = _make_research(mock_session_ctx)
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            result = indexer.index_research(research.id)
        assert result["status"] == "error"
        assert "SourceType" in result["error"]


class TestIndexResearchCreateDocException:
    """Lines 118-120."""

    def test_returns_error_on_exception(
        self, indexer, mock_session_ctx, collection
    ):
        research = _make_research(mock_session_ctx)
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            with patch.object(
                indexer,
                "_create_document_from_report",
                side_effect=RuntimeError("boom"),
            ):
                result = indexer.index_research(research.id)
        assert result["status"] == "error"
        assert "Failed to create" in result["error"]


class TestIndexResearchIntegrityError:
    """Lines 127-129."""

    def test_handles_integrity_error_on_commit(
        self, indexer, mock_session_ctx, collection, report_source_type
    ):
        research = _make_research(mock_session_ctx)

        call_count = [0]
        original_commit = mock_session_ctx.commit

        def commit_side_effect():
            call_count[0] += 1
            if call_count[0] >= 1:
                raise IntegrityError("dup", {}, Exception("unique"))
            original_commit()

        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            with patch.object(
                mock_session_ctx, "commit", side_effect=commit_side_effect
            ):
                result = indexer.index_research(research.id)

        assert result["status"] == "success"
        assert result["documents_added"] == 1


class TestConvertAllDocNone:
    """Lines 234-235."""

    def test_failed_count_when_doc_none(
        self, indexer, mock_session_ctx, collection, report_source_type
    ):
        _make_research(mock_session_ctx)
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            with patch.object(
                indexer, "_create_document_from_report", return_value=None
            ):
                result = indexer.convert_all_research(force=True)
        assert result["failed"] >= 1
        assert result["converted"] == 0


class TestConvertAllException:
    """Lines 242-245."""

    def test_failed_count_on_exception(
        self, indexer, mock_session_ctx, collection, report_source_type
    ):
        _make_research(mock_session_ctx)
        with patch.object(
            indexer, "get_or_create_collection", return_value=collection.id
        ):
            with patch.object(
                indexer,
                "_create_document_from_report",
                side_effect=RuntimeError("oops"),
            ):
                result = indexer.convert_all_research(force=True)
        assert result["failed"] >= 1
        assert result["converted"] == 0


class TestCreateDocNoSourceType:
    """Lines 286-291."""

    def test_returns_none_when_source_type_missing(
        self, indexer, mock_session_ctx, collection
    ):
        research = _make_research(mock_session_ctx)
        result = indexer._create_document_from_report(
            research, collection.id, mock_session_ctx, report_type_id=None
        )
        assert result is None


class TestAutoConvertResearch:
    def test_success(self, mock_session_ctx, collection, report_source_type):
        research = _make_research(mock_session_ctx)
        with patch(
            f"{INDEXER_MODULE}.ensure_research_history_collection",
            return_value=collection.id,
        ):
            auto_convert_research("testuser", research.id)

    def test_handles_exception(self):
        with patch(
            f"{INDEXER_MODULE}.get_user_db_session",
            side_effect=RuntimeError("fail"),
        ):
            with patch(
                f"{INDEXER_MODULE}.ensure_research_history_collection",
                return_value=str(uuid.uuid4()),
            ):
                auto_convert_research("testuser", "fake-id")
