"""Tests for priority-based task ordering.

Priority values: 1=highest, 5=lowest, 0 or absent=unset (treated as 999).
Tasks with higher priority (lower number) are picked first by:
- find_planned_not_executed
- find_all_unplanned
- topological_sort
"""

import textwrap
from pathlib import Path

import pytest
import tasks


# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════


def _task(
    task_id: str,
    *,
    priority: int | None = None,
    author: str = "",
    planned: bool = True,
    executed: bool = False,
    split: bool = False,
    deps: str = "[]",
) -> str:
    lines = [
        "---",
        f'id: {task_id}',
        f'title: "Task {task_id}"',
        "parent: null",
        f"deps: {deps}",
        f"split: {str(split).lower()}",
        "depth: 0",
        f"planned: {str(planned).lower()}",
        f"executed: {str(executed).lower()}",
    ]
    if priority is not None:
        lines.append(f"priority: {priority}")
    if author:
        lines.append(f"author: {author}")
    lines.append("---")
    lines.append("")
    lines.append(f"## Task {task_id}")
    return "\n".join(lines)


# ═══════════════════════════════════════════════════════════════════════════
# load_tasks parses priority and author
# ═══════════════════════════════════════════════════════════════════════════


class TestLoadTasksPriority:
    def test_parses_numeric_priority(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a", priority=1))
        loaded = tasks.load_tasks(tasks_dir)
        assert loaded["a"]["priority"] == 1

    def test_parses_zero_priority(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a", priority=0))
        loaded = tasks.load_tasks(tasks_dir)
        assert loaded["a"]["priority"] == 0

    def test_missing_priority_defaults_to_zero(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a"))
        loaded = tasks.load_tasks(tasks_dir)
        assert loaded["a"]["priority"] == 0

    def test_non_numeric_priority_defaults_to_zero(self, tasks_dir: Path):
        content = _task("a").replace("---\n\n", "priority: TOP — VIP\n---\n\n")
        (tasks_dir / "0001-a.md").write_text(content)
        loaded = tasks.load_tasks(tasks_dir)
        assert loaded["a"]["priority"] == 0

    def test_parses_author(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a", author="lilumi"))
        loaded = tasks.load_tasks(tasks_dir)
        assert loaded["a"]["author"] == "lilumi"

    def test_missing_author_defaults_to_empty(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a"))
        loaded = tasks.load_tasks(tasks_dir)
        assert loaded["a"]["author"] == ""


# ═══════════════════════════════════════════════════════════════════════════
# find_planned_not_executed sorts by priority
# ═══════════════════════════════════════════════════════════════════════════


class TestFindPlannedNotExecutedPriority:
    def test_p1_before_p3(self, tasks_dir: Path):
        (tasks_dir / "0001-low.md").write_text(_task("low", priority=3))
        (tasks_dir / "0002-high.md").write_text(_task("high", priority=1))
        result = tasks.find_planned_not_executed(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["high", "low"]

    def test_priority_before_unset(self, tasks_dir: Path):
        (tasks_dir / "0001-unset.md").write_text(_task("unset"))
        (tasks_dir / "0002-p2.md").write_text(_task("p2", priority=2))
        result = tasks.find_planned_not_executed(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["p2", "unset"]

    def test_same_priority_sorts_by_id(self, tasks_dir: Path):
        (tasks_dir / "0001-b.md").write_text(_task("b-task", priority=1))
        (tasks_dir / "0002-a.md").write_text(_task("a-task", priority=1))
        result = tasks.find_planned_not_executed(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["a-task", "b-task"]

    def test_full_priority_ordering(self, tasks_dir: Path):
        (tasks_dir / "0001-e.md").write_text(_task("e", priority=5))
        (tasks_dir / "0002-c.md").write_text(_task("c"))  # no priority
        (tasks_dir / "0003-a.md").write_text(_task("a", priority=1))
        (tasks_dir / "0004-b.md").write_text(_task("b", priority=2))
        (tasks_dir / "0005-d.md").write_text(_task("d", priority=1))
        result = tasks.find_planned_not_executed(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["a", "d", "b", "e", "c"]

    def test_executed_excluded(self, tasks_dir: Path):
        (tasks_dir / "0001-done.md").write_text(_task("done", priority=1, executed=True))
        (tasks_dir / "0002-pending.md").write_text(_task("pending", priority=2))
        result = tasks.find_planned_not_executed(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["pending"]

    def test_empty_dir(self, tasks_dir: Path):
        result = tasks.find_planned_not_executed(tasks_dir)
        assert result == []


# ═══════════════════════════════════════════════════════════════════════════
# find_all_unplanned sorts by priority
# ═══════════════════════════════════════════════════════════════════════════


class TestFindAllUnplannedPriority:
    def test_p1_before_p3(self, tasks_dir: Path):
        (tasks_dir / "0001-low.md").write_text(_task("low", priority=3, planned=False))
        (tasks_dir / "0002-high.md").write_text(_task("high", priority=1, planned=False))
        result = tasks.find_all_unplanned(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["high", "low"]

    def test_priority_before_unset(self, tasks_dir: Path):
        (tasks_dir / "0001-unset.md").write_text(_task("unset", planned=False))
        (tasks_dir / "0002-p2.md").write_text(_task("p2", priority=2, planned=False))
        result = tasks.find_all_unplanned(tasks_dir)
        ids = [r[0] for r in result]
        assert ids == ["p2", "unset"]

    def test_empty_dir(self, tasks_dir: Path):
        result = tasks.find_all_unplanned(tasks_dir)
        assert result == []


# ═══════════════════════════════════════════════════════════════════════════
# topological_sort respects priority
# ═══════════════════════════════════════════════════════════════════════════


class TestTopologicalSortPriority:
    def test_p1_before_p3_no_deps(self, tasks_dir: Path):
        (tasks_dir / "0001-low.md").write_text(_task("low", priority=3))
        (tasks_dir / "0002-high.md").write_text(_task("high", priority=1))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert order == ["high", "low"]

    def test_priority_before_unset_no_deps(self, tasks_dir: Path):
        (tasks_dir / "0001-unset.md").write_text(_task("unset"))
        (tasks_dir / "0002-p2.md").write_text(_task("p2", priority=2))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert order == ["p2", "unset"]

    def test_deps_override_priority(self, tasks_dir: Path):
        """Even if 'b' has higher priority, 'a' must come first because b depends on a."""
        (tasks_dir / "0001-a.md").write_text(_task("a", priority=5))
        (tasks_dir / "0002-b.md").write_text(_task("b", priority=1, deps="[a]"))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert order.index("a") < order.index("b")

    def test_priority_among_independent_with_deps(self, tasks_dir: Path):
        """c depends on a, d depends on b. a(P5) and b(P1) are independent.
        b should come first because P1 > P5, then d (ready after b), then a, then c."""
        (tasks_dir / "0001-a.md").write_text(_task("a", priority=5))
        (tasks_dir / "0002-b.md").write_text(_task("b", priority=1))
        (tasks_dir / "0003-c.md").write_text(_task("c", priority=2, deps="[a]"))
        (tasks_dir / "0004-d.md").write_text(_task("d", priority=2, deps="[b]"))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        # b (P1) before a (P5) since both are roots
        assert order.index("b") < order.index("a")
        # d (depends on b) comes after b
        assert order.index("d") > order.index("b")
        # c (depends on a) comes after a
        assert order.index("c") > order.index("a")

    def test_full_priority_ordering_no_deps(self, tasks_dir: Path):
        (tasks_dir / "0001-e.md").write_text(_task("e", priority=5))
        (tasks_dir / "0002-c.md").write_text(_task("c"))  # no priority
        (tasks_dir / "0003-a.md").write_text(_task("a", priority=1))
        (tasks_dir / "0004-b.md").write_text(_task("b", priority=2))
        (tasks_dir / "0005-d.md").write_text(_task("d", priority=1))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        # P1 first (a, d), then P2 (b), then P5 (e), then unset (c)
        assert order == ["a", "d", "b", "e", "c"]

    def test_executed_tasks_excluded(self, tasks_dir: Path):
        (tasks_dir / "0001-done.md").write_text(_task("done", priority=1, executed=True))
        (tasks_dir / "0002-pending.md").write_text(_task("pending", priority=2))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert "done" not in order
        assert order == ["pending"]

    def test_already_executed_set_excludes(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a", priority=1))
        (tasks_dir / "0002-b.md").write_text(_task("b", priority=2))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, {"a"})
        assert order == ["b"]

    def test_same_priority_sorts_alphabetically(self, tasks_dir: Path):
        (tasks_dir / "0001-z.md").write_text(_task("z-task", priority=1))
        (tasks_dir / "0002-a.md").write_text(_task("a-task", priority=1))
        (tasks_dir / "0003-m.md").write_text(_task("m-task", priority=1))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert order == ["a-task", "m-task", "z-task"]

    def test_empty_tasks_dir(self, tasks_dir: Path):
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert order == []

    def test_all_executed(self, tasks_dir: Path):
        (tasks_dir / "0001-a.md").write_text(_task("a", priority=1, executed=True))
        loaded = tasks.load_tasks(tasks_dir)
        order = tasks.topological_sort(loaded, set())
        assert order == []
