# #region Test.Core.TaskManager.Manager [C:3] [TYPE Module] [SEMANTICS test,manager,cancel,tracking,delegates]
# @BRIEF Verify TaskManager facade — cancellation, _async_tasks tracking, delegate plumbing,
#        get_task_logs wrapper, RuntimeError on event_bus.start, and edge cases.
# @RELATION BINDS_TO -> [TaskManager]
# @TEST_EDGE: cancel_running -> Running task is cancelled and removed from _async_tasks
# @TEST_EDGE: cancel_done -> Done task returns False
# @TEST_EDGE: cancel_not_found -> Nonexistent task returns False
# @TEST_EDGE: add_log_no_task -> _add_log callback returns early when task not found
# @TEST_EDGE: init_no_event_loop -> __init__ handles RuntimeError from event_bus.start()
# @TEST_EDGE: delegate_resolve -> resolve_task delegates to lifecycle
# @TEST_EDGE: delegate_dataset -> subscribe/unsubscribe dataset delegates to lifecycle
# @TEST_EDGE: get_task_logs_wrapper -> TaskManager.get_task_logs wraps event_bus with task status
import sys
from pathlib import Path

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

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
import pytest

from src.core.task_manager.models import LogFilter, LogStats, Task, TaskStatus


# ── Helpers ───────────────────────────────────────────────────

def _make_manager_with_mocks():
    """Create a TaskManager with all dependencies mocked.
    
    Returns (manager, mock_plugin_loader, mock_persistence, mock_log_persistence).
    Patches TaskPersistenceService and TaskLogPersistenceService at import time.
    Patches RuntimeError from event_bus.start() by default.
    """
    mock_plugin_loader = MagicMock()
    mock_plugin_loader.has_plugin.return_value = True
    mock_plugin = MagicMock()
    mock_plugin.name = "test_plugin"
    mock_plugin.execute = MagicMock(return_value={"status": "ok"})
    mock_plugin_loader.get_plugin.return_value = mock_plugin

    with patch("src.core.task_manager.manager.TaskPersistenceService") as MockPersist, \
         patch("src.core.task_manager.manager.TaskLogPersistenceService") as MockLogPersist:
        mock_persist = MockPersist.return_value
        mock_persist.load_tasks.return_value = []
        mock_persist.persist_task = MagicMock()
        mock_persist.delete_tasks = MagicMock(return_value=None)

        mock_log_persist = MockLogPersist.return_value
        mock_log_persist.add_logs = MagicMock()
        mock_log_persist.get_logs = MagicMock(return_value=[])
        mock_log_persist.get_log_stats = MagicMock(return_value=LogStats(total_count=0, by_level={}, by_source={}))
        mock_log_persist.get_sources = MagicMock(return_value=[])
        mock_log_persist.delete_logs_for_tasks = MagicMock()

        # Ensure a running event loop
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)

        with patch(
            "src.core.task_manager.event_bus.EventBus.start",
            side_effect=RuntimeError("No event loop"),
        ):
            from src.core.task_manager.manager import TaskManager
            manager = TaskManager(mock_plugin_loader)
            return manager, mock_plugin_loader, mock_persist, mock_log_persist

    raise RuntimeError("unreachable")


@pytest.fixture
def mgr():
    """Hardcoded fixture: create TaskManager, clean up after test."""
    manager, _, _, _ = _make_manager_with_mocks()
    yield manager
    # Cleanup: try to stop event_bus flusher
    try:
        loop = asyncio.get_running_loop()
        if loop.is_running():
            loop.create_task(manager.event_bus.stop())
    except RuntimeError:
        pass
    if manager.event_bus._flusher_task and not manager.event_bus._flusher_task.done():
        try:
            loop = asyncio.get_running_loop()
            loop.create_task(manager.event_bus.stop())
        except RuntimeError:
            pass


# ═══════════════════════════════════════════════════════════════
# Init
# ═══════════════════════════════════════════════════════════════

class TestManagerInit:
    """Verify __init__ handles RuntimeError from event_bus.start()."""

    def test_init_handles_no_event_loop(self):
        """When event_bus.start() raises RuntimeError (no event loop), it's caught.
        The TaskManager is still operational."""
        with patch("src.core.task_manager.manager.TaskPersistenceService") as MockPersist, \
             patch("src.core.task_manager.manager.TaskLogPersistenceService") as MockLogPersist, \
             patch("src.core.task_manager.event_bus.EventBus.start", side_effect=RuntimeError("No loop")):
            MockPersist.return_value.load_tasks.return_value = []
            MockLogPersist.return_value.add_logs = MagicMock()
            from src.core.task_manager.manager import TaskManager
            loader = MagicMock()
            manager = TaskManager(loader)
            assert manager.plugin_loader is loader
            assert manager._async_tasks == {}
            assert manager.graph is not None
            assert manager.event_bus is not None
            assert manager.lifecycle is not None


# ═══════════════════════════════════════════════════════════════
# _make_add_log_callback
# ═══════════════════════════════════════════════════════════════

class TestManagerAddLogCallback:
    """Verify _add_log async closure returns early when task not found."""

    @pytest.mark.asyncio
    async def test_add_log_returns_early_if_no_task(self, mgr):
        """When task_id not in graph, _add_log should return without calling event_bus."""
        with patch.object(mgr.event_bus, "add_log", AsyncMock()) as mock_add_log:
            await mgr._add_log("nonexistent", "INFO", "msg")
            mock_add_log.assert_not_called()

    @pytest.mark.asyncio
    async def test_add_log_delegates_to_event_bus(self, mgr):
        """When task exists, _add_log delegates to event_bus.add_log."""
        from src.core.task_manager.models import Task
        task = Task(plugin_id="test", params={})
        mgr.tasks[task.id] = task

        with patch.object(mgr.event_bus, "add_log", AsyncMock()) as mock_add_log:
            await mgr._add_log(task.id, "INFO", "msg", source="test")
            mock_add_log.assert_called_once()
            args = mock_add_log.call_args
            assert args[0][0] == task.id  # task_id
            assert args[0][1] == "INFO"
            assert args[0][2] == "msg"


# ═══════════════════════════════════════════════════════════════
# create_task — _async_tasks tracking
# ═══════════════════════════════════════════════════════════════

class TestManagerCreateTask:
    """Verify create_task tracks asyncio task in _async_tasks."""

    @pytest.mark.asyncio
    async def test_create_task_tracks_async_task(self, mgr):
        mgr.lifecycle.create_task = AsyncMock()
        mgr.lifecycle._run_task = AsyncMock()
        mock_task = MagicMock()
        mock_task.id = "task-track-1"
        mgr.lifecycle.create_task.return_value = mock_task

        result = await mgr.create_task("test_plugin", {})

        assert result.id == "task-track-1"
        assert "task-track-1" in mgr._async_tasks

    @pytest.mark.asyncio
    async def test_create_task_schedules_lifecycle_run(self, mgr):
        mgr.lifecycle.create_task = AsyncMock()
        mgr.lifecycle._run_task = AsyncMock()
        mock_task = MagicMock()
        mock_task.id = "task-sched-1"
        mgr.lifecycle.create_task.return_value = mock_task

        await mgr.create_task("test_plugin", {"k": "v"}, user_id="u1")

        mgr.lifecycle.create_task.assert_called_with(
            "test_plugin", {"k": "v"}, "u1", add_log_callback=mgr._add_log,
        )


# ═══════════════════════════════════════════════════════════════
# _run_task wrapper — tracking + cleanup
# ═══════════════════════════════════════════════════════════════

class TestManagerRunTask:
    """Verify _run_task wrapper tracks and cleans up _async_tasks."""

    @pytest.mark.asyncio
    async def test_run_task_tracks_and_cleans_up(self, mgr):
        mgr.lifecycle._run_task = AsyncMock()
        task_id = "task-clean-1"

        await mgr._run_task(task_id)

        assert task_id not in mgr._async_tasks  # cleaned up after await

    @pytest.mark.asyncio
    async def test_run_task_removes_on_exception(self, mgr):
        mgr.lifecycle._run_task = AsyncMock(side_effect=Exception("crash"))

        with pytest.raises(Exception, match="crash"):
            await mgr._run_task("task-exc-1")

        assert "task-exc-1" not in mgr._async_tasks  # cleaned up


# ═══════════════════════════════════════════════════════════════
# cancel_task
# ═══════════════════════════════════════════════════════════════

class TestManagerCancelTask:
    """Verify cancel_task @POST for running, done, and nonexistent tasks."""

    @pytest.mark.asyncio
    async def test_cancel_running_task(self, mgr):
        """Running task should be cancelled and removed from tracking."""
        async def dummy():
            try:
                await asyncio.sleep(10)
            except asyncio.CancelledError:
                raise

        async_task = asyncio.create_task(dummy())
        mgr._async_tasks["task-cancel-1"] = async_task
        await asyncio.sleep(0.01)  # let the task start

        result = await mgr.cancel_task("task-cancel-1")
        assert result is True
        assert "task-cancel-1" not in mgr._async_tasks
        # After cancel_task returns, the task may be cancelling but not yet
        # fully cancelled. Give it a yield to complete cancellation.
        await asyncio.sleep(0.01)
        assert async_task.cancelled()

    @pytest.mark.asyncio
    async def test_cancel_done_task_returns_false(self, mgr):
        """Completed task returns False."""
        async def quick():
            return "done"

        async_task = asyncio.create_task(quick())
        await async_task  # wait for completion
        mgr._async_tasks["task-done-1"] = async_task
        assert async_task.done()

        result = await mgr.cancel_task("task-done-1")
        assert result is False
        assert "task-done-1" in mgr._async_tasks  # not removed

    @pytest.mark.asyncio
    async def test_cancel_nonexistent_returns_false(self, mgr):
        """Task not in _async_tracks returns False."""
        result = await mgr.cancel_task("no-such")
        assert result is False


# ═══════════════════════════════════════════════════════════════
# get_task_logs — wrapper
# ═══════════════════════════════════════════════════════════════

class TestManagerGetTaskLogs:
    """Verify TaskManager.get_task_logs wraps event_bus with task status."""

    def test_get_task_logs_with_existing_task(self, mgr):
        from src.core.task_manager.models import Task, TaskStatus
        task = Task(plugin_id="p1", params={})
        task.status = TaskStatus.SUCCESS
        mgr.tasks[task.id] = task

        with patch.object(mgr.event_bus, "get_task_logs", return_value=[]) as mock_get:
            result = mgr.get_task_logs(task.id)
            mock_get.assert_called_once()
            args = mock_get.call_args
            assert args[0][0] == task.id
            assert args[1]["task_status"] == TaskStatus.SUCCESS

    def test_get_task_logs_with_nonexistent_task(self, mgr):
        with patch.object(mgr.event_bus, "get_task_logs", return_value=[]) as mock_get:
            result = mgr.get_task_logs("no-such")
            mock_get.assert_called_once()
            args = mock_get.call_args
            assert args[0][0] == "no-such"
            assert args[1]["task_status"] is None
            assert args[1]["task_logs"] == []

    def test_get_task_logs_with_log_filter(self, mgr):
        from src.core.task_manager.models import Task, TaskStatus
        task = Task(plugin_id="p1", params={})
        task.status = TaskStatus.RUNNING
        mgr.tasks[task.id] = task
        lf = LogFilter(level="ERROR")

        with patch.object(mgr.event_bus, "get_task_logs", return_value=[]) as mock_get:
            mgr.get_task_logs(task.id, log_filter=lf)
            mock_get.assert_called_with(task.id, lf, task_status=TaskStatus.RUNNING, task_logs=task.logs)


# ═══════════════════════════════════════════════════════════════
# get_task_log_stats / get_task_log_sources
# ═══════════════════════════════════════════════════════════════

class TestManagerLogDelegates:
    """Verify direct delegates to event_bus log methods."""

    def test_get_task_log_stats(self, mgr):
        expected = LogStats(total_count=3, by_level={}, by_source={})
        mgr.event_bus.get_task_log_stats = MagicMock(return_value=expected)
        result = mgr.get_task_log_stats("task-1")
        assert result.total_count == 3
        mgr.event_bus.get_task_log_stats.assert_called_with("task-1")

    def test_get_task_log_sources(self, mgr):
        mgr.event_bus.get_task_log_sources = MagicMock(return_value=["sys", "plugin"])
        result = mgr.get_task_log_sources("task-1")
        assert result == ["sys", "plugin"]
        mgr.event_bus.get_task_log_sources.assert_called_with("task-1")


# ═══════════════════════════════════════════════════════════════
# Legacy flusher aliases
# ═══════════════════════════════════════════════════════════════

class TestManagerFlushAliases:
    """Verify legacy _flusher_loop, _flush_logs, _flush_task_logs delegates."""

    @pytest.mark.asyncio
    async def test_flusher_loop_delegates(self, mgr):
        original = mgr.event_bus.async_flusher_loop
        mgr.event_bus.async_flusher_loop = AsyncMock()
        await mgr._flusher_loop()
        mgr.event_bus.async_flusher_loop.assert_called_once()
        mgr.event_bus.async_flusher_loop = original

    @pytest.mark.asyncio
    async def test_flush_logs_delegates(self, mgr):
        original = mgr.event_bus._flush_logs
        mgr.event_bus._flush_logs = AsyncMock()
        await mgr._flush_logs()
        mgr.event_bus._flush_logs.assert_called_once()
        mgr.event_bus._flush_logs = original

    @pytest.mark.asyncio
    async def test_flush_task_logs_delegates(self, mgr):
        original = mgr.event_bus.flush_task_logs
        mgr.event_bus.flush_task_logs = AsyncMock()
        await mgr._flush_task_logs("task-1")
        mgr.event_bus.flush_task_logs.assert_called_with("task-1")
        mgr.event_bus.flush_task_logs = original


# ═══════════════════════════════════════════════════════════════
# Subscribe delegates (status + task events)
# ═══════════════════════════════════════════════════════════════

class TestManagerSubscribeDelegates:
    """Verify subscription delegates pass through to EventBus."""

    @pytest.mark.asyncio
    async def test_subscribe_logs_delegates(self, mgr):
        mgr.event_bus.subscribe_logs = AsyncMock(return_value="q")
        result = await mgr.subscribe_logs("t1")
        assert result == "q"
        mgr.event_bus.subscribe_logs.assert_called_with("t1")

    def test_unsubscribe_logs_delegates(self, mgr):
        """Covers manager.py line 267."""
        queue = asyncio.Queue()
        mgr.event_bus.unsubscribe_logs = MagicMock()
        mgr.unsubscribe_logs("t1", queue)
        mgr.event_bus.unsubscribe_logs.assert_called_with("t1", queue)

    @pytest.mark.asyncio
    async def test_subscribe_status(self, mgr):
        mgr.event_bus.subscribe_status = AsyncMock(return_value="q")
        result = await mgr.subscribe_status("t1")
        assert result == "q"
        mgr.event_bus.subscribe_status.assert_called_with("t1")

    def test_unsubscribe_status(self, mgr):
        queue = asyncio.Queue()
        mgr.event_bus.unsubscribe_status = MagicMock()
        mgr.unsubscribe_status("t1", queue)
        mgr.event_bus.unsubscribe_status.assert_called_with("t1", queue)

    @pytest.mark.asyncio
    async def test_subscribe_task_events(self, mgr):
        mgr.event_bus.subscribe_task_events = AsyncMock(return_value="q")
        result = await mgr.subscribe_task_events()
        assert result == "q"

    def test_unsubscribe_task_events(self, mgr):
        queue = asyncio.Queue()
        mgr.event_bus.unsubscribe_task_events = MagicMock()
        mgr.unsubscribe_task_events(queue)
        mgr.event_bus.unsubscribe_task_events.assert_called_with(queue)


# ═══════════════════════════════════════════════════════════════
# Lifecycle delegates
# ═══════════════════════════════════════════════════════════════

class TestManagerLifecycleDelegates:
    """Verify lifecycle delegates pass through to JobLifecycle."""

    @pytest.mark.asyncio
    async def test_resolve_task_delegates(self, mgr):
        mgr.lifecycle.resolve_task = AsyncMock()
        await mgr.resolve_task("t1", {"mapping": "done"})
        mgr.lifecycle.resolve_task.assert_called_with("t1", {"mapping": "done"})

    @pytest.mark.asyncio
    async def test_wait_for_resolution_delegates(self, mgr):
        mgr.lifecycle.wait_for_resolution = AsyncMock()
        await mgr.wait_for_resolution("t1")
        mgr.lifecycle.wait_for_resolution.assert_called_with("t1")

    @pytest.mark.asyncio
    async def test_wait_for_input_delegates(self, mgr):
        mgr.lifecycle.wait_for_input = AsyncMock()
        await mgr.wait_for_input("t1")
        mgr.lifecycle.wait_for_input.assert_called_with("t1")

    @pytest.mark.asyncio
    async def test_await_input_delegates(self, mgr):
        mgr.lifecycle.await_input = AsyncMock()
        mgr._add_log = AsyncMock()
        await mgr.await_input("t1", {"prompt": "pwd"})
        mgr.lifecycle.await_input.assert_called_with("t1", {"prompt": "pwd"}, add_log_callback=mgr._add_log)

    @pytest.mark.asyncio
    async def test_resume_task_with_password_delegates(self, mgr):
        mgr.lifecycle.resume_task_with_password = AsyncMock()
        mgr._add_log = AsyncMock()
        await mgr.resume_task_with_password("t1", {"db": "pass"})
        mgr.lifecycle.resume_task_with_password.assert_called_with("t1", {"db": "pass"}, add_log_callback=mgr._add_log)


# ═══════════════════════════════════════════════════════════════
# Maintenance event delegates
# ═══════════════════════════════════════════════════════════════

class TestManagerMaintenanceDelegates:
    """Verify maintenance event delegates pass through to EventBus."""

    @pytest.mark.asyncio
    async def test_subscribe_maintenance_events(self, mgr):
        mgr.event_bus.subscribe_maintenance_events = AsyncMock(return_value="q")
        result = await mgr.subscribe_maintenance_events()
        assert result == "q"
        mgr.event_bus.subscribe_maintenance_events.assert_called_once()

    def test_unsubscribe_maintenance_events(self, mgr):
        queue = asyncio.Queue()
        mgr.event_bus.unsubscribe_maintenance_events = MagicMock()
        mgr.unsubscribe_maintenance_events(queue)
        mgr.event_bus.unsubscribe_maintenance_events.assert_called_with(queue)

    @pytest.mark.asyncio
    async def test_broadcast_maintenance_event(self, mgr):
        mgr.event_bus.broadcast_maintenance_event = AsyncMock()
        await mgr.broadcast_maintenance_event({"type": "test"})
        mgr.event_bus.broadcast_maintenance_event.assert_called_with({"type": "test"})


# ═══════════════════════════════════════════════════════════════
# Dataset event delegates
# ═══════════════════════════════════════════════════════════════

class TestManagerDatasetDelegates:
    """Verify dataset event delegates pass through to JobLifecycle."""

    @pytest.mark.asyncio
    async def test_subscribe_dataset_events(self, mgr):
        mgr.lifecycle.subscribe_dataset_events = AsyncMock(return_value="q")
        result = await mgr.subscribe_dataset_events("env-1")
        assert result == "q"
        mgr.lifecycle.subscribe_dataset_events.assert_called_with("env-1")

    def test_unsubscribe_dataset_events(self, mgr):
        queue = asyncio.Queue()
        mgr.lifecycle.unsubscribe_dataset_events = MagicMock()
        mgr.unsubscribe_dataset_events("env-1", queue)
        mgr.lifecycle.unsubscribe_dataset_events.assert_called_with("env-1", queue)


# ═══════════════════════════════════════════════════════════════
# load_persisted_tasks
# ═══════════════════════════════════════════════════════════════

class TestManagerLoadPersistedTasks:
    """Verify load_persisted_tasks delegates to graph."""

    def test_load_persisted_tasks_delegates(self, mgr):
        mgr.graph.load_persisted_tasks = MagicMock()
        mgr.load_persisted_tasks()
        mgr.graph.load_persisted_tasks.assert_called_with(limit=100)

# #endregion Test.Core.TaskManager.Manager
