"""Tests for the TodoList side-panel widget."""

from __future__ import annotations

from pathlib import Path

import pytest
from rich.text import Text

from clients.terminal.tui.widgets.todo_list import TodoList


def _plain(content: str) -> str:
    """Strip Rich markup so assertions see the visible text, not colour tags."""
    return Text.from_markup(content).plain


@pytest.fixture(autouse=True)
def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    """Override the state dir so tests never touch ~/.navi_code."""
    from clients.terminal import config

    original = config.settings.state_dir
    config.settings.state_dir = tmp_path
    import clients.terminal.tui.settings as settings_module

    settings_module._tui_settings = None
    yield tmp_path
    config.settings.state_dir = original
    settings_module._tui_settings = None


@pytest.mark.anyio
async def test_todo_list_empty_state() -> None:
    """A fresh TodoList shows the 'No plan yet' placeholder."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        # _build_content is the source of truth (never name this _render —
        # it shadows Widget._render and crashes Textual's layout pass).
        assert "No plan yet" in todo._build_content()


@pytest.mark.anyio
async def test_todo_list_renders_tasks_with_header() -> None:
    from clients.terminal.tui.tui_app import NaviCodeTui

    tasks = [
        {"index": 1, "text": "step one", "status": "done", "validation": "ran tests"},
        {"index": 2, "text": "step two", "status": "in_progress", "validation": ""},
        {"index": 3, "text": "step three", "status": "failed", "validation": "boom"},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        plain = _plain(todo._build_content())
        assert "1/3 done" in plain
        assert "step one" in plain
        assert "step two" in plain
        assert "(in_progress)" in plain
        assert "(failed)" in plain
        assert "✓ ran tests" in plain  # validation note on the done step
        assert "✓ boom" in plain       # validation note on the failed step


@pytest.mark.anyio
async def test_todo_list_clear_resets_to_placeholder() -> None:
    from clients.terminal.tui.tui_app import NaviCodeTui

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks([{"index": 1, "text": "x", "status": "pending", "validation": ""}])
        assert "x" in todo._build_content()
        todo.clear()
        assert "No plan yet" in todo._build_content()


def test_build_content_never_named_render():
    """Guard against the Textual _render shadow pitfall: a method called _render
    on a Widget subclass crashes the layout pass (AttributeError get_height)."""
    assert "_render" not in TodoList.__dict__, (
        "TodoList must not override _render — it shadows Widget._render "
        "(Textual calls it expecting a Strip, gets a str → AttributeError)."
    )
    assert "_build_content" in TodoList.__dict__


def test_task_text_with_brackets_is_rendered_literal() -> None:
    """Task text containing ``[``/``]`` is escaped so rich markup does not consume
    it as a tag (regression for markup injection via task data)."""
    from rich.console import Console

    from clients.terminal.tui.themes import set_active_theme

    set_active_theme("gnexus-dark")
    todo = TodoList()
    todo._tasks = [
        {"index": 1, "text": "fix [bug] in module", "status": "pending", "validation": ""}
    ]
    console = Console(record=True, width=80, force_terminal=True, color_system=None)
    console.print(todo._build_content())
    rendered = console.export_text()
    # Brackets survive verbatim in the rendered text, not swallowed as markup.
    assert "fix [bug] in module" in rendered


def test_milestone_label_with_brackets_is_rendered_literal() -> None:
    """A milestone label containing brackets must not break the group header."""
    from rich.console import Console

    from clients.terminal.tui.themes import set_active_theme

    set_active_theme("gnexus-dark")
    todo = TodoList()
    todo._tasks = [
        {"index": 1, "text": "step [a]", "status": "done", "validation": "", "milestone": "M [x]"},
    ]
    console = Console(record=True, width=80, force_terminal=True, color_system=None)
    console.print(todo._build_content())
    rendered = console.export_text()
    assert "M [x]" in rendered


@pytest.mark.anyio
async def test_status_markers_are_coloured_and_emphasised() -> None:
    """Each status gets its own marker colour, and the active step is bolded
    while completed/skipped steps are dimmed — so progress is legible at a glance."""
    from clients.terminal.tui.tui_app import NaviCodeTui
    from clients.terminal.tui.themes import get_active_theme

    tasks = [
        {"index": 1, "text": "done step", "status": "done", "validation": ""},
        {"index": 2, "text": "active step", "status": "in_progress", "validation": ""},
        {"index": 3, "text": "failed step", "status": "failed", "validation": ""},
        {"index": 4, "text": "queued step", "status": "pending", "validation": ""},
    ]
    theme = get_active_theme()
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        content = todo._build_content()
        # Marker icons are wrapped in their status colour.
        assert f"[{theme.success.hex}]✓" in content   # done
        assert f"[{theme.accent.hex}]◎" in content    # in_progress
        assert f"[{theme.error.hex}]✗" in content     # failed
        assert f"[{theme.text_dim.hex}]○" in content  # pending
        # The active step is bold; completed steps are dimmed.
        assert "bold" in content
        assert "dim" in content
        # Plain text still carries every step.
        plain = _plain(content)
        assert "active step" in plain
        assert "queued step" in plain


@pytest.mark.anyio
async def test_todo_list_groups_by_status_in_progress_first_done_last() -> None:
    """Steps are grouped for display: in_progress on top, then pending, then
    failed/skipped, then done at the bottom — regardless of the payload order.
    Original order is preserved within each group; the header still counts all
    done steps."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    # Payload in plan order: done, pending, in_progress, done, failed, skipped.
    tasks = [
        {"index": 1, "text": "done one", "status": "done", "validation": ""},
        {"index": 2, "text": "queued one", "status": "pending", "validation": ""},
        {"index": 3, "text": "active one", "status": "in_progress", "validation": ""},
        {"index": 4, "text": "done two", "status": "done", "validation": ""},
        {"index": 5, "text": "boom", "status": "failed", "validation": ""},
        {"index": 6, "text": "skip", "status": "skipped", "validation": ""},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        plain = _plain(todo._build_content())
        # Header counts both done steps.
        assert "2/6 done" in plain
        # The visible order is the display order, not the payload order.
        assert plain.index("active one") < plain.index("queued one")
        assert plain.index("queued one") < plain.index("boom")
        assert plain.index("boom") < plain.index("done one")
        assert plain.index("skip") < plain.index("done one")
        assert plain.index("done one") < plain.index("done two")
        # Original order preserved within the done group (done one before done two).
        assert plain.index("done one") < plain.index("done two")


@pytest.mark.anyio
async def test_groups_tasks_by_milestone() -> None:
    """When tasks carry a ``milestone`` label, the panel renders milestone
    headers and nests each step under its group. Groups are ordered by their
    most active step's status-rank (with a stable tiebreak on first-appearance
    order); the unnamed group takes part in the same ranking and renders
    without a header."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    tasks = [
        {"index": 1, "text": "read spec", "status": "done", "validation": "", "milestone": "A"},
        {"index": 2, "text": "write code", "status": "in_progress", "validation": "", "milestone": "A"},
        {"index": 3, "text": "run tests", "status": "pending", "validation": "", "milestone": "B"},
        {"index": 4, "text": "orphan step", "status": "pending", "validation": "", "milestone": ""},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        plain = _plain(todo._build_content())
        # Both milestone headers present.
        assert "Milestone A" in plain
        assert "Milestone B" in plain
        # Ranks: A=0 (in_progress), B=1 (pending), ""=1 (pending). Stable
        # tiebreak keeps first-appearance order among equal ranks, so A, then B,
        # then the unnamed group.
        assert plain.index("Milestone A") < plain.index("Milestone B")
        # Steps nest under their group (group header precedes its steps).
        assert plain.index("Milestone A") < plain.index("write code")
        assert plain.index("Milestone B") < plain.index("run tests")
        # The unnamed group ("orphan step") follows the named groups of equal
        # or higher activity (here B is also pending, so it keeps plan position
        # ahead of the unnamed block via the stable tiebreak).
        assert plain.index("run tests") < plain.index("orphan step")
        # Header still counts all done steps.
        assert "1/4 done" in plain


@pytest.mark.anyio
async def test_milestone_groups_ordered_by_rank_done_groups_sink() -> None:
    """A fully-done milestone sinks below one with work in progress, extending
    the per-step "done to the bottom" rule to the group level — regardless of
    plan order in the payload."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    # Payload in plan order A (done), B (active), C (pending).
    tasks = [
        {"index": 1, "text": "A done 1", "status": "done", "validation": "", "milestone": "A"},
        {"index": 2, "text": "A done 2", "status": "done", "validation": "", "milestone": "A"},
        {"index": 3, "text": "B active", "status": "in_progress", "validation": "", "milestone": "B"},
        {"index": 4, "text": "B pending", "status": "pending", "validation": "", "milestone": "B"},
        {"index": 5, "text": "C pending", "status": "pending", "validation": "", "milestone": "C"},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        plain = _plain(todo._build_content())
        # Ranks: A=3 (all done), B=0 (in_progress), C=1 (pending).
        # Expected group order: B (active) -> C (pending) -> A (done, sunk).
        assert plain.index("Milestone B") < plain.index("Milestone C")
        assert plain.index("Milestone C") < plain.index("Milestone A")
        # The done group A carries the ✓ marker (it is fully done).
        a_line = next(line for line in todo._build_content().splitlines() if "Milestone A" in line)
        assert "✓" in a_line


@pytest.mark.anyio
async def test_unnamed_group_ranks_with_named_groups() -> None:
    """An unnamed (no-milestone) step block ranks alongside named groups by its
    own activity: an active unnamed block leads the panel while a finished
    named milestone sinks below it. The unnamed block renders without a
    header."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    tasks = [
        {"index": 1, "text": "A done", "status": "done", "validation": "", "milestone": "A"},
        {"index": 2, "text": "orphan active", "status": "in_progress", "validation": "", "milestone": ""},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        plain = _plain(todo._build_content())
        # Ranks: ""=0 (in_progress), A=3 (done). Unnamed block leads.
        assert plain.index("orphan active") < plain.index("Milestone A")


@pytest.mark.anyio
async def test_milestone_groups_equal_rank_keep_plan_order() -> None:
    """Groups of equal rank keep the plan's first-appearance order (stable
    sort), so two equally-inactive milestones are not shuffled arbitrarily."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    tasks = [
        {"index": 1, "text": "A pending", "status": "pending", "validation": "", "milestone": "A"},
        {"index": 2, "text": "B pending", "status": "pending", "validation": "", "milestone": "B"},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        plain = _plain(todo._build_content())
        # Both rank 1 (pending); stable tiebreak keeps plan order A -> B.
        assert plain.index("Milestone A") < plain.index("Milestone B")


@pytest.mark.anyio
async def test_milestone_group_marked_done_when_all_steps_done() -> None:
    """A milestone group whose steps are all done/skipped gets a ✓ on its
    header; an in-progress group does not."""
    from clients.terminal.tui.tui_app import NaviCodeTui

    tasks = [
        {"index": 1, "text": "done one", "status": "done", "validation": "", "milestone": "A"},
        {"index": 2, "text": "skipped one", "status": "skipped", "validation": "", "milestone": "A"},
        {"index": 3, "text": "active", "status": "in_progress", "validation": "", "milestone": "B"},
    ]
    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        todo = pilot.app.query_one(TodoList)
        todo.set_tasks(tasks)
        content = todo._build_content()
        # Group A is fully done/skipped -> ✓ in its header line.
        a_line = next(line for line in content.splitlines() if "Milestone A" in line)
        assert "✓" in a_line
        # Group B is not -> no ✓ in its header line.
        b_line = next(line for line in content.splitlines() if "Milestone B" in line)
        assert "✓" not in b_line


@pytest.mark.anyio
async def test_todo_panel_is_scrollable_and_wraps_list() -> None:
    """The todo lives in its own scrollable panel below the info block, not
    inside StatusPanel."""
    from textual.containers import ScrollableContainer

    from clients.terminal.tui.tui_app import NaviCodeTui
    from clients.terminal.tui.widgets.todo_list import TodoPanel

    async with NaviCodeTui(new_session=True).run_test() as pilot:
        await pilot.pause()
        panel = pilot.app.query_one(TodoPanel)
        assert isinstance(panel, ScrollableContainer)
        # The TodoList is a child of the panel.
        assert panel.query_one(TodoList) is not None
        # The panel fills the remaining column height; the info block shrinks.
        assert str(panel.styles.height) == "1fr"