"""
Tests for research_service functions.

Tests cover:
- Citation formatter retrieval
- Report export functions
- Research strategy management
- Research process management
- Cleanup functions
"""

import hashlib
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch


class TestGetCitationFormatter:
    """Tests for get_citation_formatter function."""

    @patch("local_deep_research.config.search_config.get_setting_from_snapshot")
    def test_get_citation_formatter_number_hyperlinks(self, mock_get_setting):
        """Returns formatter with NUMBER_HYPERLINKS mode."""
        from local_deep_research.web.services.research_service import (
            get_citation_formatter,
        )
        from local_deep_research.text_optimization import CitationMode

        mock_get_setting.return_value = "number_hyperlinks"

        formatter = get_citation_formatter()

        assert formatter.mode == CitationMode.NUMBER_HYPERLINKS

    @patch("local_deep_research.config.search_config.get_setting_from_snapshot")
    def test_get_citation_formatter_domain_hyperlinks(self, mock_get_setting):
        """Returns formatter with DOMAIN_HYPERLINKS mode."""
        from local_deep_research.web.services.research_service import (
            get_citation_formatter,
        )
        from local_deep_research.text_optimization import CitationMode

        mock_get_setting.return_value = "domain_hyperlinks"

        formatter = get_citation_formatter()

        assert formatter.mode == CitationMode.DOMAIN_HYPERLINKS

    @patch("local_deep_research.config.search_config.get_setting_from_snapshot")
    def test_get_citation_formatter_no_hyperlinks(self, mock_get_setting):
        """Returns formatter with NO_HYPERLINKS mode."""
        from local_deep_research.web.services.research_service import (
            get_citation_formatter,
        )
        from local_deep_research.text_optimization import CitationMode

        mock_get_setting.return_value = "no_hyperlinks"

        formatter = get_citation_formatter()

        assert formatter.mode == CitationMode.NO_HYPERLINKS

    @patch("local_deep_research.config.search_config.get_setting_from_snapshot")
    def test_get_citation_formatter_default(self, mock_get_setting):
        """Returns formatter with default mode when unknown format."""
        from local_deep_research.web.services.research_service import (
            get_citation_formatter,
        )
        from local_deep_research.text_optimization import CitationMode

        mock_get_setting.return_value = "unknown_format"

        formatter = get_citation_formatter()

        assert formatter.mode == CitationMode.NUMBER_HYPERLINKS


class TestExportReportToMemory:
    """Tests for export_report_to_memory function."""

    def test_export_latex_format(self):
        """export_report_to_memory generates LaTeX content."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report\n\nThis is test content."

        content, filename, mimetype = export_report_to_memory(
            markdown_content, "latex", title="Test Report"
        )

        assert filename.endswith(".tex")
        assert mimetype == "text/plain"
        assert isinstance(content, bytes)

    def test_export_ris_format(self):
        """export_report_to_memory generates RIS content."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report\n\nThis is test content."

        content, filename, mimetype = export_report_to_memory(
            markdown_content, "ris", title="Test Report"
        )

        assert filename.endswith(".ris")
        assert mimetype == "text/plain"
        assert isinstance(content, bytes)

    def test_export_unsupported_format_raises(self):
        """export_report_to_memory raises for unsupported format."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report"

        try:
            export_report_to_memory(markdown_content, "unsupported")
            assert False, "Should have raised ValueError"
        except ValueError as e:
            assert "Unsupported export format" in str(e)

    def test_export_quarto_format(self):
        """export_report_to_memory generates Quarto zip content."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report\n\nThis is test content."

        content, filename, mimetype = export_report_to_memory(
            markdown_content, "quarto", title="Test Report"
        )

        assert filename.endswith(".zip")
        assert mimetype == "application/zip"
        assert isinstance(content, bytes)

    @patch("local_deep_research.web.services.pdf_service.get_pdf_service")
    def test_export_pdf_format(self, mock_get_pdf):
        """export_report_to_memory generates PDF content."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        mock_pdf_service = Mock()
        mock_pdf_service.markdown_to_pdf.return_value = b"PDF content"
        mock_get_pdf.return_value = mock_pdf_service

        markdown_content = "# Test Report\n\nThis is test content."

        content, filename, mimetype = export_report_to_memory(
            markdown_content, "pdf", title="Test Report"
        )

        assert filename.endswith(".pdf")
        assert mimetype == "application/pdf"
        assert content == b"PDF content"


class TestSaveResearchStrategy:
    """Tests for save_research_strategy function."""

    @patch(
        "local_deep_research.web.services.research_service.get_user_db_session"
    )
    def test_save_research_strategy_creates_new(self, mock_get_session):
        """save_research_strategy creates new strategy record."""
        from local_deep_research.web.services.research_service import (
            save_research_strategy,
        )

        mock_session = MagicMock()
        mock_query = Mock()
        mock_query.filter_by.return_value.first.return_value = None
        mock_session.query.return_value = mock_query
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        save_research_strategy(123, "standard", username="testuser")

        mock_session.add.assert_called_once()
        mock_session.commit.assert_called_once()

    @patch(
        "local_deep_research.web.services.research_service.get_user_db_session"
    )
    def test_save_research_strategy_updates_existing(self, mock_get_session):
        """save_research_strategy updates existing strategy."""
        from local_deep_research.web.services.research_service import (
            save_research_strategy,
        )

        mock_strategy = Mock()
        mock_strategy.strategy_name = "old_strategy"

        mock_session = MagicMock()
        mock_query = Mock()
        mock_query.filter_by.return_value.first.return_value = mock_strategy
        mock_session.query.return_value = mock_query
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        save_research_strategy(123, "new_strategy", username="testuser")

        assert mock_strategy.strategy_name == "new_strategy"
        mock_session.commit.assert_called_once()


class TestGetResearchStrategy:
    """Tests for get_research_strategy function."""

    @patch(
        "local_deep_research.web.services.research_service.get_user_db_session"
    )
    def test_get_research_strategy_found(self, mock_get_session):
        """get_research_strategy returns strategy name when found."""
        from local_deep_research.web.services.research_service import (
            get_research_strategy,
        )

        mock_strategy = Mock()
        mock_strategy.strategy_name = "standard"

        mock_session = MagicMock()
        mock_query = Mock()
        mock_query.filter_by.return_value.first.return_value = mock_strategy
        mock_session.query.return_value = mock_query
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        result = get_research_strategy(123, username="testuser")

        assert result == "standard"

    @patch(
        "local_deep_research.web.services.research_service.get_user_db_session"
    )
    def test_get_research_strategy_not_found(self, mock_get_session):
        """get_research_strategy returns None when not found."""
        from local_deep_research.web.services.research_service import (
            get_research_strategy,
        )

        mock_session = MagicMock()
        mock_query = Mock()
        mock_query.filter_by.return_value.first.return_value = None
        mock_session.query.return_value = mock_query
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        result = get_research_strategy(123, username="testuser")

        assert result is None


class TestGenerateReportPath:
    """Tests for _generate_report_path function."""

    @patch("local_deep_research.web.services.research_service.OUTPUT_DIR")
    def test_generate_report_path_creates_unique_path(self, mock_output_dir):
        """_generate_report_path creates unique path from query."""
        from local_deep_research.web.services.research_service import (
            _generate_report_path,
        )

        mock_output_dir.__truediv__ = lambda self, x: Path(f"/test/output/{x}")

        query = "test research query"

        result = _generate_report_path(query)

        # Path should contain hash of query
        query_hash = hashlib.md5(  # DevSkim: ignore DS126858
            query.encode("utf-8"), usedforsecurity=False
        ).hexdigest()[:10]
        assert query_hash in str(result)
        assert "research_report" in str(result)


class TestStartResearchProcess:
    """Tests for start_research_process function."""

    @patch(
        "local_deep_research.web.services.research_service.thread_with_app_context"
    )
    @patch("local_deep_research.web.services.research_service.thread_context")
    def test_start_research_process_creates_thread(
        self, mock_thread_context, mock_thread_with_context
    ):
        """start_research_process creates and starts a thread."""
        from local_deep_research.web.services.research_service import (
            start_research_process,
        )

        mock_callback = Mock()
        mock_thread_with_context.return_value = mock_callback
        mock_thread_context.return_value = {}

        active_research = {}
        termination_flags = {}

        # Mock threading.Thread to not actually start
        with patch(
            "local_deep_research.web.services.research_service.threading.Thread"
        ) as mock_thread_class:
            mock_thread = Mock()
            mock_thread_class.return_value = mock_thread

            start_research_process(
                research_id=123,
                query="test query",
                mode="quick",
                active_research=active_research,
                termination_flags=termination_flags,
                run_research_callback=mock_callback,
            )

            mock_thread.start.assert_called_once()
            assert 123 in active_research
            assert active_research[123]["status"] == "in_progress"

    @patch(
        "local_deep_research.web.services.research_service.thread_with_app_context"
    )
    @patch("local_deep_research.web.services.research_service.thread_context")
    def test_start_research_process_stores_settings(
        self, mock_thread_context, mock_thread_with_context
    ):
        """start_research_process stores settings in active_research."""
        from local_deep_research.web.services.research_service import (
            start_research_process,
        )

        mock_callback = Mock()
        mock_thread_with_context.return_value = mock_callback
        mock_thread_context.return_value = {}

        active_research = {}
        termination_flags = {}

        with patch(
            "local_deep_research.web.services.research_service.threading.Thread"
        ) as mock_thread_class:
            mock_thread = Mock()
            mock_thread_class.return_value = mock_thread

            start_research_process(
                research_id=123,
                query="test query",
                mode="detailed",
                active_research=active_research,
                termination_flags=termination_flags,
                run_research_callback=mock_callback,
                model="gpt-4",
                search_engine="google",
            )

            assert active_research[123]["settings"]["model"] == "gpt-4"
            assert active_research[123]["settings"]["search_engine"] == "google"


class TestCleanupResearchResources:
    """Tests for cleanup_research_resources function."""

    @patch("local_deep_research.settings.env_registry.is_test_mode")
    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch("local_deep_research.web.routes.globals.get_globals")
    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_cleanup_removes_from_active_research(
        self, mock_socket, mock_get_globals, mock_queue, mock_test_mode
    ):
        """cleanup_research_resources removes from active_research."""
        from local_deep_research.web.services.research_service import (
            cleanup_research_resources,
        )

        mock_test_mode.return_value = False
        mock_get_globals.return_value = {"socket_subscriptions": {}}

        active_research = {123: {"thread": Mock(), "progress": 100}}
        termination_flags = {123: False}

        cleanup_research_resources(
            123, active_research, termination_flags, username="testuser"
        )

        assert 123 not in active_research
        assert 123 not in termination_flags

    @patch("local_deep_research.settings.env_registry.is_test_mode")
    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch("local_deep_research.web.routes.globals.get_globals")
    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_cleanup_notifies_queue_processor(
        self, mock_socket, mock_get_globals, mock_queue, mock_test_mode
    ):
        """cleanup_research_resources notifies queue processor."""
        from local_deep_research.web.services.research_service import (
            cleanup_research_resources,
        )

        mock_test_mode.return_value = False
        mock_get_globals.return_value = {"socket_subscriptions": {}}

        active_research = {}
        termination_flags = {}

        cleanup_research_resources(
            123, active_research, termination_flags, username="testuser"
        )

        mock_queue.notify_research_completed.assert_called_once_with(
            "testuser", 123
        )


class TestCancelResearch:
    """Tests for cancel_research function."""

    @patch("local_deep_research.web.routes.globals.get_globals")
    @patch(
        "local_deep_research.web.services.research_service.handle_termination"
    )
    def test_cancel_research_sets_termination_flag(
        self, mock_handle_termination, mock_get_globals
    ):
        """cancel_research sets termination flag."""
        from local_deep_research.web.services.research_service import (
            cancel_research,
        )

        active_research = {123: {"thread": Mock()}}
        termination_flags = {}
        mock_get_globals.return_value = {
            "active_research": active_research,
            "termination_flags": termination_flags,
        }

        result = cancel_research(123, username="testuser")

        assert result is True
        assert termination_flags[123] is True
        mock_handle_termination.assert_called_once()

    @patch("local_deep_research.web.routes.globals.get_globals")
    @patch(
        "local_deep_research.web.services.research_service.get_user_db_session"
    )
    def test_cancel_research_updates_db_for_inactive(
        self, mock_get_session, mock_get_globals
    ):
        """cancel_research updates database for inactive research."""
        from local_deep_research.web.services.research_service import (
            cancel_research,
        )

        mock_get_globals.return_value = {
            "active_research": {},
            "termination_flags": {},
        }

        mock_research = Mock()
        mock_research.status = "in_progress"

        mock_session = MagicMock()
        mock_query = Mock()
        mock_query.filter_by.return_value.first.return_value = mock_research
        mock_session.query.return_value = mock_query
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        result = cancel_research(123, username="testuser")

        assert result is True
        assert mock_research.status == "suspended"

    @patch("local_deep_research.web.routes.globals.get_globals")
    @patch(
        "local_deep_research.web.services.research_service.get_user_db_session"
    )
    def test_cancel_research_already_completed(
        self, mock_get_session, mock_get_globals
    ):
        """cancel_research returns True for already completed research."""
        from local_deep_research.web.services.research_service import (
            cancel_research,
        )

        mock_get_globals.return_value = {
            "active_research": {},
            "termination_flags": {},
        }

        mock_research = Mock()
        mock_research.status = "completed"

        mock_session = MagicMock()
        mock_query = Mock()
        mock_query.filter_by.return_value.first.return_value = mock_research
        mock_session.query.return_value = mock_query
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        result = cancel_research(123, username="testuser")

        assert result is True


class TestHandleTermination:
    """Tests for handle_termination function."""

    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch(
        "local_deep_research.web.services.research_service.cleanup_research_resources"
    )
    def test_handle_termination_queues_update(self, mock_cleanup, mock_queue):
        """handle_termination queues suspension update."""
        from local_deep_research.web.services.research_service import (
            handle_termination,
        )

        active_research = {123: {"thread": Mock()}}
        termination_flags = {123: True}

        handle_termination(
            123, active_research, termination_flags, username="testuser"
        )

        mock_queue.queue_error_update.assert_called_once()
        call_kwargs = mock_queue.queue_error_update.call_args[1]
        assert call_kwargs["status"] == "suspended"
        assert call_kwargs["research_id"] == 123

    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch(
        "local_deep_research.web.services.research_service.cleanup_research_resources"
    )
    def test_handle_termination_calls_cleanup(self, mock_cleanup, mock_queue):
        """handle_termination calls cleanup function."""
        from local_deep_research.web.services.research_service import (
            handle_termination,
        )

        active_research = {}
        termination_flags = {}

        handle_termination(
            123, active_research, termination_flags, username="testuser"
        )

        mock_cleanup.assert_called_once_with(
            123, active_research, termination_flags, "testuser"
        )


class TestExportQuartoFormat:
    """Tests for quarto export format."""

    def test_export_quarto_creates_zip(self):
        """export_report_to_memory creates zip for quarto format."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report\n\nThis is test content."

        content, filename, mimetype = export_report_to_memory(
            markdown_content, "quarto", title="Test Report"
        )

        assert filename.endswith(".zip")
        assert mimetype == "application/zip"
        assert isinstance(content, bytes)
        # Verify it's a valid zip file by checking magic bytes
        assert content[:2] == b"PK"


class TestExportLatexFormat:
    """Tests for latex export format."""

    def test_export_latex_format_via_memory(self):
        """export_report_to_memory handles latex format."""
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report\n\nThis is test content."

        content, filename, mimetype = export_report_to_memory(
            markdown_content, "latex", title="Test Report"
        )

        assert filename.endswith(".tex")
        assert mimetype == "text/plain"  # LaTeX uses text/plain mimetype
        assert isinstance(content, bytes)


class TestGenerateReportPathUniqueHash:
    """Tests for _generate_report_path unique hash generation."""

    @patch("local_deep_research.web.services.research_service.OUTPUT_DIR")
    def test_different_queries_different_paths(self, mock_output_dir):
        """Different queries should generate different paths."""
        from local_deep_research.web.services.research_service import (
            _generate_report_path,
        )

        mock_output_dir.__truediv__ = lambda self, x: Path(f"/test/output/{x}")

        path1 = _generate_report_path("query one")
        path2 = _generate_report_path("query two")

        # Paths should be different
        assert str(path1) != str(path2)

    @patch("local_deep_research.web.services.research_service.OUTPUT_DIR")
    def test_same_query_same_path(self, mock_output_dir):
        """Same query should generate same path."""
        from local_deep_research.web.services.research_service import (
            _generate_report_path,
        )

        mock_output_dir.__truediv__ = lambda self, x: Path(f"/test/output/{x}")

        path1 = _generate_report_path("test query")
        path2 = _generate_report_path("test query")

        # Paths should be the same
        assert str(path1) == str(path2)


class TestStartResearchProcessWithOptions:
    """Tests for start_research_process with various options."""

    @patch(
        "local_deep_research.web.services.research_service.thread_with_app_context"
    )
    @patch("local_deep_research.web.services.research_service.thread_context")
    def test_start_research_with_local_collections(
        self, mock_thread_context, mock_thread_with_context
    ):
        """start_research_process handles local_collections option."""
        from local_deep_research.web.services.research_service import (
            start_research_process,
        )

        mock_callback = Mock()
        mock_thread_with_context.return_value = mock_callback
        mock_thread_context.return_value = {}

        active_research = {}
        termination_flags = {}

        with patch(
            "local_deep_research.web.services.research_service.threading.Thread"
        ) as mock_thread_class:
            mock_thread = Mock()
            mock_thread_class.return_value = mock_thread

            start_research_process(
                research_id=123,
                query="test query",
                mode="detailed",
                active_research=active_research,
                termination_flags=termination_flags,
                run_research_callback=mock_callback,
                local_collections=["collection1", "collection2"],
            )

            assert 123 in active_research
            assert active_research[123]["settings"]["local_collections"] == [
                "collection1",
                "collection2",
            ]

    @patch(
        "local_deep_research.web.services.research_service.thread_with_app_context"
    )
    @patch("local_deep_research.web.services.research_service.thread_context")
    def test_start_research_stores_knowledge_graph_option(
        self, mock_thread_context, mock_thread_with_context
    ):
        """start_research_process stores knowledge_graph option."""
        from local_deep_research.web.services.research_service import (
            start_research_process,
        )

        mock_callback = Mock()
        mock_thread_with_context.return_value = mock_callback
        mock_thread_context.return_value = {}

        active_research = {}
        termination_flags = {}

        with patch(
            "local_deep_research.web.services.research_service.threading.Thread"
        ) as mock_thread_class:
            mock_thread = Mock()
            mock_thread_class.return_value = mock_thread

            start_research_process(
                research_id=456,
                query="test query",
                mode="quick",
                active_research=active_research,
                termination_flags=termination_flags,
                run_research_callback=mock_callback,
                enable_knowledge_graph=True,
            )

            assert 456 in active_research
            assert (
                active_research[456]["settings"]["enable_knowledge_graph"]
                is True
            )


class TestResearchServiceExportFormats:
    """Tests for export format handling."""

    def test_export_unsupported_format_returns_error(self):
        """export_report_to_memory raises for unsupported format."""
        import pytest
        from local_deep_research.web.services.research_service import (
            export_report_to_memory,
        )

        markdown_content = "# Test Report\n\nThis is test content."

        # Test unsupported format raises ValueError
        with pytest.raises(ValueError) as exc_info:
            export_report_to_memory(
                markdown_content, "unsupported_format", title="Test"
            )

        assert "Unsupported export format" in str(exc_info.value)
