# #region Test.TaskManager.Graph [C:3] [TYPE Module] [SEMANTICS test, task, graph, registry, crud]
# @BRIEF Tests for TaskGraph — in-memory task registry with persistence.
# @RELATION BINDS_TO -> [TaskGraphModule]

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

from datetime import UTC, datetime
from unittest.mock import MagicMock, AsyncMock

import pytest

from src.core.task_manager.graph import TaskGraph
from src.core.task_manager.models import Task, TaskStatus
from src.core.task_manager.persistence import TaskPersistenceService


def _make_task(**kwargs):
    """Helper to create a Task mock with required attributes."""
    t = MagicMock(spec=Task)
    t.started_at = kwargs.pop('started_at', None)
    for k, v in kwargs.items():
        setattr(t, k, v)
    return t


@pytest.fixture
def persistence():
    return MagicMock(spec=TaskPersistenceService)


@pytest.fixture
def graph(persistence):
    return TaskGraph(persistence)


class TestInit:
    def test_empty(self, graph):
        assert graph.tasks == {}
        assert graph.task_futures == {}
        assert graph.persistence_service is not None


class TestLoadPersistedTasks:
    def test_loads_new_tasks(self, graph, persistence):
        task1 = _make_task(id="t1")
        task2 = _make_task(id="t2")
        persistence.load_tasks.return_value = [task1, task2]
        graph.load_persisted_tasks(limit=50)
        assert len(graph.tasks) == 2
        assert graph.tasks["t1"] is task1
        assert graph.tasks["t2"] is task2
        persistence.load_tasks.assert_called_once_with(limit=50)

    def test_skips_existing_tasks(self, graph, persistence):
        existing = _make_task(id="t1")
        graph.tasks["t1"] = existing
        new_task = _make_task(id="t2")
        loaded = _make_task(id="t1")  # same ID as existing
        persistence.load_tasks.return_value = [loaded, new_task]
        graph.load_persisted_tasks()
        assert graph.tasks["t1"] is existing  # not overwritten
        assert graph.tasks["t2"] is new_task


class TestGetTask:
    def test_found(self, graph):
        task = _make_task(id="t1")
        graph.tasks["t1"] = task
        assert graph.get_task("t1") is task

    def test_not_found(self, graph):
        assert graph.get_task("nonexistent") is None


class TestGetAllTasks:
    def test_all(self, graph):
        t1 = _make_task(id="a")
        t2 = _make_task(id="b")
        graph.tasks = {"a": t1, "b": t2}
        all_tasks = graph.get_all_tasks()
        assert len(all_tasks) == 2


class TestGetTasks:
    def test_no_filters(self, graph):
        t1 = _make_task(id="a", started_at=None)
        t2 = _make_task(id="b", started_at=None)
        graph.tasks = {"a": t1, "b": t2}
        result = graph.get_tasks(limit=10, offset=0)
        assert len(result) <= 2

    def test_filter_by_status(self, graph):
        t1 = _make_task(id="a", status=TaskStatus.SUCCESS, started_at=None)
        t2 = _make_task(id="b", status=TaskStatus.FAILED, started_at=None)
        graph.tasks = {"a": t1, "b": t2}
        result = graph.get_tasks(limit=10, offset=0, status=TaskStatus.SUCCESS)
        assert len(result) == 1
        assert result[0] is t1

    def test_filter_by_plugin_ids(self, graph):
        t1 = _make_task(id="a", plugin_id="plugin_a", started_at=None)
        t2 = _make_task(id="b", plugin_id="plugin_b", started_at=None)
        graph.tasks = {"a": t1, "b": t2}
        result = graph.get_tasks(limit=10, offset=0, plugin_ids=["plugin_a"])
        assert len(result) == 1
        assert result[0] is t1

    def test_filter_completed_only(self, graph):
        t1 = _make_task(id="a", status=TaskStatus.SUCCESS, started_at=None)
        t2 = _make_task(id="b", status=TaskStatus.RUNNING, started_at=None)
        t3 = _make_task(id="c", status=TaskStatus.FAILED, started_at=None)
        graph.tasks = {"a": t1, "b": t2, "c": t3}
        result = graph.get_tasks(limit=10, offset=0, completed_only=True)
        assert len(result) == 2

    def test_sort_key_none_started_at(self, graph):
        """Tasks with None started_at get sorted to the end."""
        t1 = _make_task(id="a", started_at=None)
        t2 = _make_task(id="b", started_at=datetime.now(UTC))
        graph.tasks = {"a": t1, "b": t2}
        result = graph.get_tasks(limit=10, offset=0)
        assert result[0] is t2  # newest first

    def test_sort_key_non_datetime(self, graph):
        """started_at is not a datetime → treated as -inf."""
        t1 = _make_task(id="a", started_at="not_a_datetime")
        t2 = _make_task(id="b", started_at=datetime.now(UTC))
        graph.tasks = {"a": t1, "b": t2}
        result = graph.get_tasks(limit=10, offset=0)
        assert result[0] is t2

    def test_sort_key_naive_datetime(self, graph):
        """Naive datetime gets UTC timezone."""
        t1 = _make_task(id="a", started_at=datetime(2024, 1, 1, 12, 0, 0))  # no tz
        t2 = _make_task(id="b", started_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=UTC))
        graph.tasks = {"a": t1, "b": t2}
        result = graph.get_tasks(limit=10, offset=0)
        assert result[0] is t2


class TestAddTask:
    def test_adds(self, graph):
        task = _make_task(id="t1")
        graph.add_task(task)
        assert graph.tasks["t1"] is task


class TestRemoveTasks:
    def test_removes_with_future(self, graph):
        task = _make_task(id="t1")
        future = MagicMock()
        graph.tasks["t1"] = task
        graph.task_futures["t1"] = future
        count = graph.remove_tasks(["t1"])
        assert count == 1
        assert "t1" not in graph.tasks
        assert "t1" not in graph.task_futures
        future.cancel.assert_called_once()

    def test_removes_without_future(self, graph, persistence):
        task = _make_task(id="t1")
        graph.tasks["t1"] = task
        count = graph.remove_tasks(["t1"])
        assert count == 1
        persistence.delete_tasks.assert_called_once_with(["t1"])

    def test_removes_empty_list(self, graph, persistence):
        count = graph.remove_tasks([])
        assert count == 0
        persistence.delete_tasks.assert_not_called()


class TestCreateFuture:
    def test_creates(self, graph):
        future = MagicMock()
        graph.create_future("t1", future)
        assert graph.task_futures["t1"] is future


class TestResolveFuture:
    def test_resolves(self, graph):
        future = MagicMock()
        graph.task_futures["t1"] = future
        graph.resolve_future("t1", result=True)
        future.set_result.assert_called_once_with(True)
        assert "t1" not in graph.task_futures

    def test_resolve_nonexistent(self, graph):
        graph.resolve_future("nonexistent")  # should not raise


class TestRemoveFuture:
    def test_removes(self, graph):
        future = MagicMock()
        graph.task_futures["t1"] = future
        graph.remove_future("t1")
        assert "t1" not in graph.task_futures

    def test_remove_nonexistent(self, graph):
        graph.remove_future("nonexistent")  # should not raise
# #endregion Test.TaskManager.Graph
