"""Tests for log_utils module."""

import logging
import queue
import threading
from datetime import datetime
from unittest.mock import Mock, patch, MagicMock


class TestInterceptHandler:
    """Tests for InterceptHandler class."""

    def test_emit_forwards_to_loguru(self):
        """Should forward log records to loguru."""
        from local_deep_research.utilities.log_utils import InterceptHandler

        handler = InterceptHandler()

        # Create a mock log record
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="test.py",
            lineno=1,
            msg="Test message",
            args=(),
            exc_info=None,
        )

        with patch(
            "local_deep_research.utilities.log_utils.logger"
        ) as mock_logger:
            mock_logger.level.return_value = Mock(name="INFO")
            mock_opt = Mock()
            mock_logger.opt.return_value = mock_opt

            handler.emit(record)

            mock_logger.opt.assert_called()
            mock_opt.log.assert_called()

    def test_handles_unknown_level(self):
        """Should handle unknown log levels by using levelno."""
        from local_deep_research.utilities.log_utils import InterceptHandler

        handler = InterceptHandler()

        record = logging.LogRecord(
            name="test",
            level=35,  # Non-standard level
            pathname="test.py",
            lineno=1,
            msg="Test",
            args=(),
            exc_info=None,
        )
        record.levelname = "CUSTOM"

        with patch(
            "local_deep_research.utilities.log_utils.logger"
        ) as mock_logger:
            mock_logger.level.side_effect = ValueError("Unknown level")
            mock_opt = Mock()
            mock_logger.opt.return_value = mock_opt

            handler.emit(record)

            mock_opt.log.assert_called()


class TestLogForResearch:
    """Tests for log_for_research decorator."""

    def test_sets_research_id_in_g(self):
        """Should set research_id in Flask g object."""
        from local_deep_research.utilities.log_utils import log_for_research

        mock_g = MagicMock()

        @log_for_research
        def test_func(research_id):
            return "done"

        with patch("local_deep_research.utilities.log_utils.g", mock_g):
            test_func("test-uuid-123")

            # Check that research_id was set
            assert mock_g.research_id == "test-uuid-123"

    def test_removes_research_id_after_function(self):
        """Should remove research_id from g after function completes."""
        from local_deep_research.utilities.log_utils import log_for_research

        @log_for_research
        def test_func(research_id):
            return "result"

        mock_g = MagicMock()
        with patch("local_deep_research.utilities.log_utils.g", mock_g):
            test_func("uuid")

            mock_g.pop.assert_called_with("research_id")

    def test_preserves_function_metadata(self):
        """Should preserve function name and docstring."""
        from local_deep_research.utilities.log_utils import log_for_research

        @log_for_research
        def documented_func(research_id):
            """My documentation."""
            pass

        assert documented_func.__name__ == "documented_func"
        assert documented_func.__doc__ == "My documentation."

    def test_passes_args_and_kwargs(self):
        """Should pass arguments correctly."""
        from local_deep_research.utilities.log_utils import log_for_research

        @log_for_research
        def test_func(research_id, arg1, kwarg1=None):
            return (research_id, arg1, kwarg1)

        mock_g = MagicMock()
        with patch("local_deep_research.utilities.log_utils.g", mock_g):
            result = test_func("uuid", "value1", kwarg1="value2")

            assert result == ("uuid", "value1", "value2")


class TestDatabaseSink:
    """Tests for database_sink function."""

    def test_creates_log_entry_dict(self):
        """Should create log entry dictionary from message."""
        from local_deep_research.utilities.log_utils import database_sink

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test log message",
            "name": "test_module",
            "function": "test_function",
            "line": 42,
            "level": Mock(name="INFO"),
            "extra": {},
        }

        with patch(
            "local_deep_research.utilities.log_utils.has_app_context",
            return_value=False,
        ):
            with patch(
                "local_deep_research.utilities.log_utils._log_queue"
            ) as mock_queue:
                database_sink(mock_message)

                # Should queue the log since we're not in app context
                mock_queue.put_nowait.assert_called_once()

    def test_queues_log_from_non_main_thread(self):
        """Should queue log when not in main thread."""
        from local_deep_research.utilities.log_utils import database_sink
        import local_deep_research.utilities.log_utils as module

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test message",
            "name": "module",
            "function": "func",
            "line": 1,
            "level": Mock(name="DEBUG"),
            "extra": {"research_id": "test-uuid"},
        }

        # Mock has_app_context to return True but thread name is not MainThread
        mock_thread = Mock()
        mock_thread.name = "WorkerThread"

        with patch.object(module, "has_app_context", return_value=True):
            with patch.object(
                module, "_get_research_id", return_value="test-uuid"
            ):
                with patch.object(
                    threading, "current_thread", return_value=mock_thread
                ):
                    with patch.object(module, "_log_queue") as mock_queue:
                        database_sink(mock_message)

                        # Should queue since not MainThread
                        mock_queue.put_nowait.assert_called_once()

    def test_handles_full_queue_gracefully(self):
        """Should not raise when queue is full."""
        from local_deep_research.utilities.log_utils import database_sink

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test",
            "name": "mod",
            "function": "f",
            "line": 1,
            "level": Mock(name="INFO"),
            "extra": {},
        }

        with patch(
            "local_deep_research.utilities.log_utils.has_app_context",
            return_value=False,
        ):
            with patch(
                "local_deep_research.utilities.log_utils._log_queue"
            ) as mock_queue:
                mock_queue.put_nowait.side_effect = queue.Full()

                # Should not raise
                database_sink(mock_message)

    def test_writes_to_database_in_main_thread(self):
        """Should write to database when in main thread with app context."""
        from local_deep_research.utilities.log_utils import database_sink
        import local_deep_research.utilities.log_utils as module

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test message",
            "name": "test_module",
            "function": "test_func",
            "line": 42,
            "level": Mock(name="INFO"),
            "extra": {"username": "testuser", "research_id": "test-uuid"},
        }

        mock_thread = Mock()
        mock_thread.name = "MainThread"

        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        mock_g = Mock()
        mock_g.get.return_value = None

        with patch.object(module, "has_app_context", return_value=True):
            with patch.object(module, "g", mock_g):
                with patch.object(
                    threading, "current_thread", return_value=mock_thread
                ):
                    with patch(
                        "local_deep_research.database.session_context.get_user_db_session",
                        return_value=mock_cm,
                    ):
                        database_sink(mock_message)

                        # Should write to database
                        mock_session.add.assert_called_once()
                        mock_session.commit.assert_called_once()

    def test_handles_database_error_gracefully(self):
        """Should not raise on database errors when writing."""
        from local_deep_research.utilities.log_utils import database_sink
        import local_deep_research.utilities.log_utils as module

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test",
            "name": "mod",
            "function": "f",
            "line": 1,
            "level": Mock(name="INFO"),
            "extra": {"research_id": "test-uuid"},
        }

        mock_thread = Mock()
        mock_thread.name = "MainThread"

        mock_g = Mock()
        mock_g.get.return_value = None

        with patch.object(module, "has_app_context", return_value=True):
            with patch.object(module, "g", mock_g):
                with patch.object(
                    threading, "current_thread", return_value=mock_thread
                ):
                    with patch(
                        "local_deep_research.database.session_context.get_user_db_session",
                        side_effect=Exception("DB error"),
                    ):
                        # Should not raise
                        database_sink(mock_message)

    def test_extracts_research_id_from_record_extra(self):
        """Should extract research_id from record extra."""
        from local_deep_research.utilities.log_utils import database_sink
        import local_deep_research.utilities.log_utils as module

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test",
            "name": "mod",
            "function": "f",
            "line": 1,
            "level": Mock(name="INFO"),
            "extra": {"research_id": "record-uuid"},
        }

        with patch.object(module, "has_app_context", return_value=False):
            with patch.object(module, "_log_queue") as mock_queue:
                database_sink(mock_message)

                # Verify the queued log entry contains the research_id
                call_args = mock_queue.put_nowait.call_args[0][0]
                assert call_args["research_id"] == "record-uuid"

    def test_extracts_research_id_from_flask_g(self):
        """Should extract research_id from Flask g when not in record."""
        from local_deep_research.utilities.log_utils import database_sink
        import local_deep_research.utilities.log_utils as module

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test",
            "name": "mod",
            "function": "f",
            "line": 1,
            "level": Mock(name="INFO"),
            "extra": {},
        }

        mock_g = Mock()
        mock_g.get.return_value = "flask-uuid"

        with patch.object(module, "has_app_context", return_value=True):
            with patch.object(module, "g", mock_g):
                with patch.object(module, "_log_queue") as mock_queue:
                    # Use non-main thread to queue instead of write
                    mock_thread = Mock()
                    mock_thread.name = "WorkerThread"
                    with patch.object(
                        threading, "current_thread", return_value=mock_thread
                    ):
                        database_sink(mock_message)

                        # Verify the queued log entry contains the research_id
                        call_args = mock_queue.put_nowait.call_args[0][0]
                        assert call_args["research_id"] == "flask-uuid"

    def test_record_research_id_takes_priority_over_flask(self):
        """Record research_id should take priority over Flask g."""
        from local_deep_research.utilities.log_utils import database_sink
        import local_deep_research.utilities.log_utils as module

        mock_message = Mock()
        mock_message.record = {
            "time": datetime.now(),
            "message": "Test",
            "name": "mod",
            "function": "f",
            "line": 1,
            "level": Mock(name="INFO"),
            "extra": {"research_id": "record-uuid"},
        }

        mock_g = Mock()
        mock_g.get.return_value = "flask-uuid"

        with patch.object(module, "has_app_context", return_value=True):
            with patch.object(module, "g", mock_g):
                with patch.object(module, "_log_queue") as mock_queue:
                    # Use non-main thread to queue instead of write
                    mock_thread = Mock()
                    mock_thread.name = "WorkerThread"
                    with patch.object(
                        threading, "current_thread", return_value=mock_thread
                    ):
                        database_sink(mock_message)

                        # Record research_id should win
                        call_args = mock_queue.put_nowait.call_args[0][0]
                        assert call_args["research_id"] == "record-uuid"


class TestFrontendProgressSink:
    """Tests for frontend_progress_sink function."""

    def test_skips_when_no_research_id(self):
        """Should skip when no research_id is available."""
        from local_deep_research.utilities.log_utils import (
            frontend_progress_sink,
        )

        mock_message = Mock()
        mock_message.record = {
            "message": "Test",
            "level": Mock(name="INFO"),
            "time": Mock(isoformat=Mock(return_value="2024-01-01T00:00:00")),
            "extra": {},
        }

        with patch(
            "local_deep_research.utilities.log_utils._get_research_id",
            return_value=None,
        ):
            with patch(
                "local_deep_research.utilities.log_utils.SocketIOService"
            ) as mock_socket:
                frontend_progress_sink(mock_message)

                # Should not emit anything
                mock_socket.return_value.emit_to_subscribers.assert_not_called()

    def test_emits_to_subscribers_with_research_id(self):
        """Should emit to subscribers when research_id is present."""
        from local_deep_research.utilities.log_utils import (
            frontend_progress_sink,
        )

        mock_message = Mock()
        mock_message.record = {
            "message": "Progress update",
            "level": Mock(name="INFO"),
            "time": Mock(isoformat=Mock(return_value="2024-01-01T12:00:00")),
            "extra": {"research_id": "test-uuid"},
        }

        with patch(
            "local_deep_research.utilities.log_utils._get_research_id",
            return_value="test-uuid",
        ):
            with patch(
                "local_deep_research.utilities.log_utils.SocketIOService"
            ) as mock_socket:
                frontend_progress_sink(mock_message)

                mock_socket.return_value.emit_to_subscribers.assert_called_once()
                call_args = (
                    mock_socket.return_value.emit_to_subscribers.call_args
                )
                assert call_args[0][0] == "progress"
                assert call_args[0][1] == "test-uuid"


class TestFlushLogQueue:
    """Tests for flush_log_queue function."""

    def test_flushes_all_queued_logs(self):
        """Should flush all logs from queue."""
        from local_deep_research.utilities.log_utils import flush_log_queue
        import local_deep_research.utilities.log_utils as module

        log_entries = [
            {
                "timestamp": datetime.now(),
                "message": "Log 1",
                "module": "mod",
                "function": "f",
                "line_no": 1,
                "level": "INFO",
                "research_id": None,
                "username": None,
            },
            {
                "timestamp": datetime.now(),
                "message": "Log 2",
                "module": "mod",
                "function": "f",
                "line_no": 2,
                "level": "INFO",
                "research_id": None,
                "username": None,
            },
        ]

        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch.object(module, "_log_queue") as mock_queue:
            mock_queue.empty.side_effect = [False, False, True]
            mock_queue.get_nowait.side_effect = log_entries + [queue.Empty()]

            with patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=mock_cm,
            ):
                flush_log_queue()

                # Should have written 2 logs
                assert mock_session.add.call_count == 2
                assert mock_session.commit.call_count == 2

    def test_handles_empty_queue(self):
        """Should handle empty queue gracefully."""
        from local_deep_research.utilities.log_utils import flush_log_queue

        with patch(
            "local_deep_research.utilities.log_utils._log_queue"
        ) as mock_queue:
            mock_queue.empty.return_value = True

            # Should not raise
            flush_log_queue()


class TestConfigLogger:
    """Tests for config_logger function."""

    def test_configures_logger(self):
        """Should configure logger with sinks."""
        from local_deep_research.utilities.log_utils import config_logger

        with patch(
            "local_deep_research.utilities.log_utils.logger"
        ) as mock_logger:
            config_logger("test_app")

            mock_logger.enable.assert_called_with("local_deep_research")
            mock_logger.remove.assert_called_once()
            # Should add multiple sinks
            assert mock_logger.add.call_count >= 3

    def test_adds_file_logging_when_enabled(self):
        """Should add file logging when environment variable is set."""
        from local_deep_research.utilities.log_utils import config_logger

        with patch.dict("os.environ", {"LDR_ENABLE_FILE_LOGGING": "true"}):
            with patch(
                "local_deep_research.utilities.log_utils.logger"
            ) as mock_logger:
                config_logger("test_app")

                # Should add 4 sinks (stderr, database, frontend, file)
                assert mock_logger.add.call_count >= 4

    def test_creates_milestone_level(self):
        """Should create MILESTONE log level."""
        from local_deep_research.utilities.log_utils import config_logger

        with patch(
            "local_deep_research.utilities.log_utils.logger"
        ) as mock_logger:
            config_logger("test_app")

            mock_logger.level.assert_called()

    def test_handles_existing_milestone_level(self):
        """Should handle case where MILESTONE level already exists."""
        from local_deep_research.utilities.log_utils import config_logger

        with patch(
            "local_deep_research.utilities.log_utils.logger"
        ) as mock_logger:
            mock_logger.level.side_effect = ValueError("Level already exists")

            # Should not raise
            config_logger("test_app")


class TestWriteLogToDatabase:
    """Tests for _write_log_to_database function."""

    def test_writes_research_log(self):
        """Should write ResearchLog to database."""
        from local_deep_research.utilities.log_utils import (
            _write_log_to_database,
        )

        log_entry = {
            "timestamp": datetime.now(),
            "message": "Test message",
            "module": "test_module",
            "function": "test_func",
            "line_no": 42,
            "level": "INFO",
            "research_id": "test-uuid",
            "username": "testuser",
        }

        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)

        with patch(
            "local_deep_research.database.session_context.get_user_db_session",
            return_value=mock_cm,
        ):
            _write_log_to_database(log_entry)

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

    def test_handles_database_error_gracefully(self):
        """Should not raise on database errors."""
        from local_deep_research.utilities.log_utils import (
            _write_log_to_database,
        )

        log_entry = {
            "timestamp": datetime.now(),
            "message": "Test",
            "module": "mod",
            "function": "f",
            "line_no": 1,
            "level": "INFO",
            "research_id": None,
            "username": None,
        }

        with patch(
            "local_deep_research.database.session_context.get_user_db_session",
            side_effect=Exception("DB error"),
        ):
            # Should not raise
            _write_log_to_database(log_entry)
