import pytest
from datetime import date, timedelta
from sunwaee.cli import app
from .conftest import ok, err


# --- create ---

def test_create_task(runner, ws):
    data = ok(runner.invoke(app, ["task", "create", "--title", "My task", "--workspace", "personal"]))
    assert data["title"] == "My task"
    assert data["status"] == "todo"
    assert data["priority"] == "medium"
    assert data["parent_id"] is None
    assert "path" in data


def test_create_task_with_priority(runner, ws):
    data = ok(runner.invoke(app, ["task", "create", "--title", "Urgent", "--priority", "high", "--workspace", "personal"]))
    assert data["priority"] == "high"


def test_create_task_with_due_date(runner, ws):
    data = ok(runner.invoke(app, ["task", "create", "--title", "Deadline", "--due", "2026-12-31", "--workspace", "personal"]))
    assert data["due_date"] == "2026-12-31"


def test_create_task_due_today(runner, ws):
    today = date.today().isoformat()
    data = ok(runner.invoke(app, ["task", "create", "--title", "Due today", "--due", "today", "--workspace", "personal"]))
    assert data["due_date"] == today


def test_create_task_due_tomorrow(runner, ws):
    tomorrow = (date.today() + timedelta(days=1)).isoformat()
    data = ok(runner.invoke(app, ["task", "create", "--title", "Due tomorrow", "--due", "tomorrow", "--workspace", "personal"]))
    assert data["due_date"] == tomorrow


def test_create_task_due_this_week(runner, ws):
    from datetime import date as d_
    today = d_.today()
    end_of_week = (today - timedelta(days=today.weekday()) + timedelta(days=6)).isoformat()
    data = ok(runner.invoke(app, ["task", "create", "--title", "This week", "--due", "this-week", "--workspace", "personal"]))
    assert data["due_date"] == end_of_week


def test_create_task_due_this_month(runner, ws):
    import calendar as cal_
    today = date.today()
    last_day = cal_.monthrange(today.year, today.month)[1]
    end_of_month = today.replace(day=last_day).isoformat()
    data = ok(runner.invoke(app, ["task", "create", "--title", "This month", "--due", "this-month", "--workspace", "personal"]))
    assert data["due_date"] == end_of_month


def test_create_task_due_overdue_rejected(runner, ws):
    e = err(runner.invoke(app, ["task", "create", "--title", "Overdue?", "--due", "overdue", "--workspace", "personal"]))
    assert e["code"] == "VALIDATION_ERROR"


def test_create_task_invalid_due_date(runner, ws):
    e = err(runner.invoke(app, ["task", "create", "--title", "Bad date", "--due", "31-12-2026", "--workspace", "personal"]))
    assert e["code"] == "VALIDATION_ERROR"


def test_create_task_with_tags(runner, ws):
    data = ok(runner.invoke(app, ["task", "create", "--title", "Tagged", "--tags", "dev,urgent", "--workspace", "personal"]))
    assert data["tags"] == ["dev", "urgent"]


def test_create_subtask(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    parent = ok(runner.invoke(app, ["task", "show", "Parent", "--workspace", "personal"]))
    assert data["parent_id"] == parent["id"]


def test_create_subtask_invalid_parent(runner, ws):
    e = err(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "ghost", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


# --- list ---

def test_list_tasks_empty(runner, ws):
    data = ok(runner.invoke(app, ["task", "list", "--workspace", "personal"]))
    assert data == []


def test_list_tasks(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "A", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "B", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--workspace", "personal"]))
    assert len(data) == 2


def test_list_tasks_hierarchical_order(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "list", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert titles.index("Parent") < titles.index("Child")


def test_list_tasks_hides_completed_by_default(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Active", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Done", "--workspace", "personal"])
    runner.invoke(app, ["task", "complete", "Done", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert "Active" in titles
    assert "Done" not in titles


def test_list_tasks_show_completed(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Active", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Done", "--workspace", "personal"])
    runner.invoke(app, ["task", "complete", "Done", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--show-completed", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert "Active" in titles
    assert "Done" in titles


def test_list_tasks_filter_by_status(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Todo task", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Done task", "--workspace", "personal"])
    runner.invoke(app, ["task", "complete", "Done task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--status", "todo", "--workspace", "personal"]))
    assert all(t["status"] == "todo" for t in data)


def test_list_tasks_filter_by_priority(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "High", "--priority", "high", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Low", "--priority", "low", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--priority", "high", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "High"


def test_list_tasks_filter_by_tags(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Dev task", "--tags", "dev", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Other task", "--tags", "other", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--tags", "dev", "--workspace", "personal"]))
    assert len(data) == 1


def test_list_tasks_filter_by_due_today(runner, ws):
    today = date.today().isoformat()
    tomorrow = (date.today() + timedelta(days=1)).isoformat()
    runner.invoke(app, ["task", "create", "--title", "Today", "--due", today, "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Tomorrow", "--due", tomorrow, "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "today", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "Today"


def test_list_tasks_filter_by_due_tomorrow(runner, ws):
    today = date.today().isoformat()
    tomorrow = (date.today() + timedelta(days=1)).isoformat()
    runner.invoke(app, ["task", "create", "--title", "Today", "--due", today, "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Tomorrow", "--due", tomorrow, "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "tomorrow", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "Tomorrow"


def test_list_tasks_filter_by_due_this_week(runner, ws):
    today = date.today().isoformat()
    far_future = "2099-01-01"
    runner.invoke(app, ["task", "create", "--title", "This week", "--due", today, "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Far future", "--due", far_future, "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "this-week", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert "This week" in titles
    assert "Far future" not in titles


def test_list_tasks_filter_by_due_this_month(runner, ws):
    today = date.today().isoformat()
    far_future = "2099-01-01"
    runner.invoke(app, ["task", "create", "--title", "This month", "--due", today, "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Far future", "--due", far_future, "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "this-month", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert "This month" in titles
    assert "Far future" not in titles


def test_list_tasks_filter_by_due_overdue(runner, ws):
    past = "2020-01-01"
    today = date.today().isoformat()
    runner.invoke(app, ["task", "create", "--title", "Overdue", "--due", past, "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Current", "--due", today, "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "overdue", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "Overdue"


def test_list_tasks_filter_by_due_exact_date(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Exact", "--due", "2026-06-15", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Other", "--due", "2026-06-16", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "2026-06-15", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "Exact"


def test_list_tasks_filter_by_due_no_due_date_excluded(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "No due", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--due", "today", "--workspace", "personal"]))
    assert len(data) == 0


def test_list_tasks_filter_by_due_invalid(runner, ws):
    e = err(runner.invoke(app, ["task", "list", "--due", "next-year", "--workspace", "personal"]))
    assert e["code"] == "VALIDATION_ERROR"


def test_list_tasks_filter_by_parent(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Unrelated", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "list", "--parent", "Parent", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "Child"


def test_list_tasks_filter_by_parent_not_found(runner, ws):
    e = err(runner.invoke(app, ["task", "list", "--parent", "ghost", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


def test_list_tasks_hierarchical_already_visited(runner, ws):
    """A task with a parent not in the filtered set should appear as a root (depth 0)."""
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    # Filter to only child — parent not in result set, child should still appear
    data = ok(runner.invoke(app, ["task", "list", "--status", "todo", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert "Child" in titles


def test_list_includes_path(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Path task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "list", "--workspace", "personal"]))
    assert all("path" in t for t in data)


# --- show ---

def test_show_task(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Show me", "--body", "details", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "show", "Show me", "--workspace", "personal"]))
    assert data["title"] == "Show me"
    assert data["body"] == "details"
    assert "subtasks" in data


def test_show_task_includes_subtasks(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "show", "Parent", "--workspace", "personal"]))
    assert len(data["subtasks"]) == 1
    assert data["subtasks"][0]["title"] == "Child"


def test_show_task_not_found(runner, ws):
    e = err(runner.invoke(app, ["task", "show", "ghost", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


# --- done ---

def test_complete_task(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Finish me", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "complete", "Finish me", "--workspace", "personal"]))
    assert data["status"] == "done"
    assert data["completed_at"] is not None


def test_complete_sets_completed_at(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Task A", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "show", "Task A", "--workspace", "personal"]))
    assert data["completed_at"] is None
    ok(runner.invoke(app, ["task", "complete", "Task A", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "show", "Task A", "--workspace", "personal"]))
    assert data["completed_at"] is not None


def test_complete_cascades_to_children(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child 1", "--parent", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child 2", "--parent", "Parent", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "complete", "Parent", "--workspace", "personal"]))
    assert len(data["completed"]) == 3
    assert all(t["status"] == "done" for t in data["completed"])
    assert all(t["completed_at"] is not None for t in data["completed"])


def test_complete_task_not_found(runner, ws):
    e = err(runner.invoke(app, ["task", "complete", "ghost", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


def test_uncomplete_task(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "complete", "Task", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "uncomplete", "Task", "--workspace", "personal"]))
    assert data["status"] == "todo"
    assert data["completed_at"] is None


def test_uncomplete_cascades_to_children(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "complete", "Parent", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "uncomplete", "Parent", "--workspace", "personal"]))
    assert len(data["uncompleted"]) == 2
    assert all(t["status"] == "todo" for t in data["uncompleted"])
    assert all(t["completed_at"] is None for t in data["uncompleted"])


def test_uncomplete_task_not_found(runner, ws):
    e = err(runner.invoke(app, ["task", "uncomplete", "ghost", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


def test_edit_status_clears_completed_at(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "complete", "Task", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--status", "todo", "--workspace", "personal"]))
    assert data["completed_at"] is None


def test_edit_status_to_done_does_not_set_completed_at(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--status", "done", "--workspace", "personal"]))
    assert data["completed_at"] is None


# --- edit ---

def test_edit_task_title(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Old", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Old", "--title", "New", "--workspace", "personal"]))
    assert data["title"] == "New"


def test_edit_task_status(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--status", "in_progress", "--workspace", "personal"]))
    assert data["status"] == "in_progress"


def test_edit_task_priority(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--priority", "low", "--workspace", "personal"]))
    assert data["priority"] == "low"


def test_edit_task_due_date(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--due", "2027-01-01", "--workspace", "personal"]))
    assert data["due_date"] == "2027-01-01"


def test_edit_task_clear_due(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--due", "2027-01-01", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--clear-due", "--workspace", "personal"]))
    assert data["due_date"] is None


def test_edit_task_tags(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--tags", "a,b", "--workspace", "personal"]))
    assert data["tags"] == ["a", "b"]


def test_edit_task_body(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "edit", "Task", "--body", "new body", "--workspace", "personal"]))
    assert data["body"] == "new body"


def test_edit_task_not_found(runner, ws):
    e = err(runner.invoke(app, ["task", "edit", "ghost", "--title", "x", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


# --- delete ---

def test_delete_task_requires_confirm(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--workspace", "personal"])
    e = err(runner.invoke(app, ["task", "delete", "Task", "--workspace", "personal"]))
    assert e["code"] == "CONFIRMATION_REQUIRED"


def test_delete_task(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Delete me", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "delete", "Delete me", "--confirm", "--workspace", "personal"]))
    assert any(t["title"] == "Delete me" for t in data["deleted"])
    e = err(runner.invoke(app, ["task", "show", "Delete me", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


def test_delete_cascades_to_children(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Parent", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Child", "--parent", "Parent", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "delete", "Parent", "--confirm", "--workspace", "personal"]))
    assert len(data["deleted"]) == 2
    titles = [t["title"] for t in data["deleted"]]
    assert "Parent" in titles
    assert "Child" in titles


def test_delete_task_not_found(runner, ws):
    e = err(runner.invoke(app, ["task", "delete", "ghost", "--confirm", "--workspace", "personal"]))
    assert e["code"] == "NOT_FOUND"


# --- search ---

def test_search_tasks_by_title(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Fix login bug", "--workspace", "personal"])
    runner.invoke(app, ["task", "create", "--title", "Write docs", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "search", "login", "--workspace", "personal"]))
    assert len(data) == 1
    assert data[0]["title"] == "Fix login bug"


def test_search_tasks_by_body(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Task", "--body", "check the OAuth flow", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "search", "oauth", "--workspace", "personal"]))
    assert len(data) == 1


def test_search_tasks_no_match(runner, ws):
    runner.invoke(app, ["task", "create", "--title", "Something", "--workspace", "personal"])
    data = ok(runner.invoke(app, ["task", "search", "zzznomatch", "--workspace", "personal"]))
    assert data == []


# --- model ---

def test_task_model_subtask_field():
    from sunwaee.modules.tasks.model import Task, TaskStatus, TaskPriority
    t = Task(title="Subtask", parent_id="parent-uuid")
    assert t.parent_id == "parent-uuid"
    assert t.status == TaskStatus.TODO
    d = t.to_dict()
    assert d["parent_id"] == "parent-uuid"


def test_task_frontmatter_includes_parent_id():
    from sunwaee.modules.tasks.model import Task
    t = Task(title="Child", parent_id="abc")
    meta = t.to_frontmatter_meta()
    assert meta["parent_id"] == "abc"


def test_task_from_dict_restores_parent_id():
    from sunwaee.modules.tasks.model import Task
    t = Task.from_dict({"title": "X", "parent_id": "parent-uuid"}, "")
    assert t.parent_id == "parent-uuid"


# --- sort ---

def test_list_tasks_sort_by_title(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Zebra", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Alpha", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "list", "--sort", "title", "--workspace", "personal"]))
    titles = [t["title"] for t in data]
    assert titles == sorted(titles, key=str.lower)


def test_list_tasks_sort_by_priority(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Low", "--priority", "low", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "High", "--priority", "high", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Med", "--priority", "medium", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "list", "--sort", "priority", "--workspace", "personal"]))
    priorities = [t["priority"] for t in data]
    assert priorities == ["high", "medium", "low"]


def test_list_tasks_sort_by_due(runner, ws):
    ok(runner.invoke(app, ["task", "create", "--title", "Later", "--due", "2026-12-31", "--workspace", "personal"]))
    ok(runner.invoke(app, ["task", "create", "--title", "Soon", "--due", "2026-03-01", "--workspace", "personal"]))
    data = ok(runner.invoke(app, ["task", "list", "--sort", "due", "--workspace", "personal"]))
    dues = [t["due_date"] for t in data if t["due_date"]]
    assert dues == sorted(dues)


def test_list_tasks_sort_invalid(runner, ws):
    e = err(runner.invoke(app, ["task", "list", "--sort", "banana", "--workspace", "personal"]))
    assert e["code"] == "VALIDATION_ERROR"
