# #region Test.TaskLogger [C:3] [TYPE Module] [SEMANTICS test,task,logger]
# @BRIEF Tests for core/task_manager/task_logger.py — TaskLogger class.
# @RELATION BINDS_TO -> [TaskLoggerModule]
# @TEST_EDGE: no_event_loop -> _log drops gracefully
# @TEST_EDGE: with_source_creates_independent_logger

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

import asyncio
from unittest.mock import MagicMock, AsyncMock, patch as mock_patch
import pytest


class TestTaskLoggerInit:
    """Verify TaskLogger.__init__."""

    def test_initializes_with_required_params(self):
        from src.core.task_manager.task_logger import TaskLogger

        add_log = MagicMock()
        logger = TaskLogger(task_id="abc123", add_log_fn=add_log)
        assert logger._task_id == "abc123"
        assert logger._add_log is add_log
        assert logger._default_source == "plugin"

    def test_initializes_with_custom_source(self):
        from src.core.task_manager.task_logger import TaskLogger

        logger = TaskLogger(task_id="abc123", add_log_fn=MagicMock(), source="custom")
        assert logger._default_source == "custom"


class TestTaskLoggerWithSource:
    """Verify with_source creates sub-logger."""

    def test_with_source_returns_new_logger(self):
        from src.core.task_manager.task_logger import TaskLogger

        add_log = MagicMock()
        logger = TaskLogger(task_id="abc123", add_log_fn=add_log)
        sub = logger.with_source("sub-source")
        assert isinstance(sub, TaskLogger)
        assert sub._task_id == "abc123"
        assert sub._add_log is add_log
        assert sub._default_source == "sub-source"
        assert sub is not logger


class TestTaskLoggerLog:
    """Verify _log behavior including fire-and-forget."""

    @pytest.fixture
    def logger(self):
        from src.core.task_manager.task_logger import TaskLogger

        # Use MagicMock (not AsyncMock) in sync context to avoid unawaited coroutine warnings
        return TaskLogger(task_id="abc123", add_log_fn=MagicMock(), source="test")

    def test_log_calls_add_log_fn(self, logger):
        logger._log("INFO", "test message")
        logger._add_log.assert_called_once_with(
            task_id="abc123",
            level="INFO",
            message="test message",
            source="test",
            metadata=None,
        )

    def test_log_with_override_source(self, logger):
        logger._log("ERROR", "msg", source="override")
        logger._add_log.assert_called_once_with(
            task_id="abc123",
            level="ERROR",
            message="msg",
            source="override",
            metadata=None,
        )

    def test_log_with_metadata(self, logger):
        logger._log("INFO", "msg", metadata={"key": "val"})
        logger._add_log.assert_called_once_with(
            task_id="abc123",
            level="INFO",
            message="msg",
            source="test",
            metadata={"key": "val"},
        )

    def test_log_handles_no_running_loop(self, logger):
        """In sync context with no event loop, _log should not raise."""
        # No event loop running — should drop gracefully
        logger._log("INFO", "msg")
        # Should not raise

    async def test_log_schedules_future_when_loop_running(self):
        """When event loop is running, _log schedules ensure_future."""
        from src.core.task_manager.task_logger import TaskLogger

        add_log = AsyncMock()
        logger = TaskLogger(task_id="abc123", add_log_fn=add_log)

        with mock_patch.object(asyncio, "ensure_future") as mock_ef:
            logger._log("INFO", "msg")
            # In async context, ensure_future should be called
            assert mock_ef.called


class TestTaskLoggerLevelMethods:
    """Verify each level method delegates to _log."""

    @pytest.fixture
    def logger(self):
        from src.core.task_manager.task_logger import TaskLogger

        # Use MagicMock since _log is patched — no coroutine needed
        return TaskLogger(task_id="abc123", add_log_fn=MagicMock(), source="test")

    def test_debug(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.debug("debug msg", source="src", metadata={"a": 1})
            mock_log.assert_called_once_with("DEBUG", "debug msg", "src", {"a": 1})

    def test_info(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.info("info msg")
            mock_log.assert_called_once_with("INFO", "info msg", None, None)

    def test_warning(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.warning("warn msg")
            mock_log.assert_called_once_with("WARNING", "warn msg", None, None)

    def test_error(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.error("error msg")
            mock_log.assert_called_once_with("ERROR", "error msg", None, None)

    def test_progress_clamps_percent(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.progress("progress", 150)
            mock_log.assert_called_once()
            args = mock_log.call_args[0]
            assert args[0] == "INFO"
            assert args[2] is None  # source
            assert args[3]["progress"] == 100  # clamped

    def test_progress_lower_bound(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.progress("progress", -10)
            args = mock_log.call_args[0]
            assert args[3]["progress"] == 0  # clamped

    def test_progress_valid(self, logger):
        with pytest.MonkeyPatch().context() as mp:
            mock_log = MagicMock()
            mp.setattr(logger, "_log", mock_log)
            logger.progress("halfway", 50)
            args = mock_log.call_args[0]
            assert args[3]["progress"] == 50


class TestTaskLoggerIntegration:
    """Integration tests with real async add_log callback."""

    async def test_async_add_log_receives_correct_params(self):
        from src.core.task_manager.task_logger import TaskLogger

        received = {}

        async def add_log_fn(task_id, level, message, source, metadata):
            received.update(task_id=task_id, level=level, message=message, source=source, metadata=metadata)

        logger = TaskLogger(task_id="task-1", add_log_fn=add_log_fn, source="plugin")
        logger.info("integration test")

        # Allow the scheduled coroutine to run
        await asyncio.sleep(0.01)
        assert received.get("task_id") == "task-1"
        assert received.get("level") == "INFO"
        assert received.get("message") == "integration test"
        assert received.get("source") == "plugin"
# #endregion Test.TaskLogger
