"""TaskRow widget: three-line renderer used by Task Center."""

from __future__ import annotations

import time

import pytest
from textual.app import ComposeResult
from textual.containers import VerticalScroll

from lilbee.cli.tui.task_queue import Task, TaskStatus
from lilbee.cli.tui.widgets.task_row import TaskRow, _build_head, _format_elapsed
from tests._lilbee_app_test_host import LilbeeAppHost


class _Host(LilbeeAppHost):
    def compose(self) -> ComposeResult:
        yield VerticalScroll(id="host")


def _task(
    *,
    status: TaskStatus = TaskStatus.ACTIVE,
    progress: float = 0.0,
    detail: str = "",
    indeterminate: bool = False,
    started_at: float | None = None,
    completed_at: float | None = None,
    name: str = "demo",
    task_type: str = "download",
) -> Task:
    return Task(
        task_id="t1",
        name=name,
        task_type=task_type,
        fn=lambda: None,
        status=status,
        progress=progress,
        detail=detail,
        indeterminate=indeterminate,
        started_at=started_at,
        completed_at=completed_at,
    )


def test_format_elapsed_shows_mmss_after_start() -> None:
    started = time.monotonic() - 65
    assert _format_elapsed(_task(status=TaskStatus.ACTIVE, started_at=started)) == "01:05"


def test_format_elapsed_returns_queued_label_for_queued_task() -> None:
    assert _format_elapsed(_task(status=TaskStatus.QUEUED)) == "queued"


def test_format_elapsed_empty_for_active_without_started_at() -> None:
    assert _format_elapsed(_task(status=TaskStatus.ACTIVE, started_at=None)) == ""


def test_build_head_composes_name_type_and_status_pills() -> None:
    """Head content shows the name, type pill, status pill, and elapsed trailing."""
    task = Task(
        task_id="t1",
        name="Nomic Embed Text v1.5",
        task_type="download",
        fn=lambda: None,
        status=TaskStatus.ACTIVE,
        started_at=time.monotonic() - 3,
    )
    content = _build_head(task, elapsed="00:03")
    plain = content.plain
    assert "Nomic Embed Text v1.5" in plain
    assert "download" in plain
    assert "active" in plain
    assert "00:03" in plain


def test_build_head_renders_setup_pill() -> None:
    """bb-wq8g: TaskType.SETUP rows render with the 'setup' type pill.

    The head content must include the literal 'setup' string so
    TaskCenter users can tell a bootstrap task apart from download /
    sync / crawl / etc.
    """
    task = Task(
        task_id="t-setup",
        name="Install Chromium browser",
        task_type="setup",
        fn=lambda: None,
        status=TaskStatus.ACTIVE,
        started_at=time.monotonic() - 5,
    )
    content = _build_head(task, elapsed="00:05")
    plain = content.plain
    assert "Install Chromium browser" in plain
    assert "setup" in plain
    assert "active" in plain


def test_build_head_omits_elapsed_when_empty() -> None:
    task = Task(
        task_id="t2",
        name="demo",
        task_type="wiki",
        fn=lambda: None,
        status=TaskStatus.QUEUED,
    )
    content = _build_head(task, elapsed="")
    plain = content.plain
    assert "demo" in plain
    assert "wiki" in plain
    assert "queued" in plain


def test_format_elapsed_freezes_on_completed_at() -> None:
    """Once completed_at is set, elapsed is stable regardless of wall-clock drift."""
    started = time.monotonic() - 100
    completed = started + 12  # 12 s total
    task = _task(status=TaskStatus.DONE, started_at=started, completed_at=completed)
    first = _format_elapsed(task)
    time.sleep(0.05)
    second = _format_elapsed(task)
    assert first == second == "00:12"


@pytest.mark.asyncio
async def test_update_applies_active_class_and_bar_percent() -> None:
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        row.update(_task(status=TaskStatus.ACTIVE, progress=42.0, detail="halfway"), 0)
        assert row.has_class("-active")
        bar_widget = row.query_one("#row-bar")
        bar = str(bar_widget._Static__content)  # type: ignore[attr-defined]
        assert "42.0%" in bar
        # On tick 0 the pulse is on (even beat).
        assert row.has_class("-pulse")


@pytest.mark.asyncio
async def test_update_blanks_stale_detail_on_done_keeps_it_on_failed() -> None:
    """A DONE row drops its last-tick detail (it'd read "441/610 MB" beside 100%);
    FAILED keeps it, since there the detail is the failure reason."""
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        row.update(_task(status=TaskStatus.DONE, progress=100.0, detail="441/610 MB"), 0)
        meta_widget = row.query_one("#row-meta")
        done_meta = str(meta_widget._Static__content)  # type: ignore[attr-defined]
        assert "441/610 MB" not in done_meta
        assert "%" not in done_meta
        row.update(_task(status=TaskStatus.FAILED, progress=12.0, detail="connection reset"), 0)
        assert "connection reset" in str(meta_widget._Static__content)  # type: ignore[attr-defined]


@pytest.mark.asyncio
async def test_update_pulse_toggles_on_active_across_ticks() -> None:
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        row.update(_task(status=TaskStatus.ACTIVE), 0)
        on_beat = row.has_class("-pulse")
        row.update(_task(status=TaskStatus.ACTIVE), 5)
        off_beat = row.has_class("-pulse")
        assert on_beat != off_beat, "rail should pulse between even/odd 5-tick halves"


@pytest.mark.asyncio
async def test_update_never_pulses_queued_task() -> None:
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        row.update(_task(status=TaskStatus.QUEUED), 0)
        row.update(_task(status=TaskStatus.QUEUED), 5)
        assert not row.has_class("-pulse")
        assert row.has_class("-queued")


@pytest.mark.asyncio
async def test_update_uses_indeterminate_cell_when_flagged() -> None:
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        row.update(_task(indeterminate=True, status=TaskStatus.ACTIVE), 0)
        bar_widget = row.query_one("#row-bar")
        bar = str(bar_widget._Static__content)  # type: ignore[attr-defined]
        assert "%" not in bar


@pytest.mark.asyncio
async def test_update_freezes_indeterminate_bar_in_terminal_states() -> None:
    """once a row goes CANCELLED/FAILED, the indeterminate bar
    stops animating. Successive ticks render the same static bar."""
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        bar_widget = row.query_one("#row-bar")

        for status in (TaskStatus.CANCELLED, TaskStatus.FAILED, TaskStatus.DONE):
            row.update(_task(indeterminate=True, status=status), 0)
            frame_a = str(bar_widget._Static__content)  # type: ignore[attr-defined]
            row.update(_task(indeterminate=True, status=status), 7)
            frame_b = str(bar_widget._Static__content)  # type: ignore[attr-defined]
            assert frame_a == frame_b, (
                f"{status.value} indeterminate row still animates across ticks"
            )
            # Frozen bars have no trailing dots (that reads as 'still
            # working') and no percentage (indeterminate never had one).
            assert "···" not in frame_a
            assert "%" not in frame_a


@pytest.mark.asyncio
async def test_update_shows_state_classes_for_every_status() -> None:
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        for status, cls in [
            (TaskStatus.QUEUED, "-queued"),
            (TaskStatus.ACTIVE, "-active"),
            (TaskStatus.DONE, "-done"),
            (TaskStatus.FAILED, "-failed"),
            (TaskStatus.CANCELLED, "-cancelled"),
        ]:
            row.update(_task(status=status), 0)
            assert row.has_class(cls), f"row missing {cls} after update with status {status}"


@pytest.mark.asyncio
async def test_flash_completed_adds_class() -> None:
    app = _Host()
    async with app.run_test() as pilot:
        host = app.query_one("#host", VerticalScroll)
        row = TaskRow("t1")
        await host.mount(row)
        for _ in range(3):
            await pilot.pause()
        row.flash_completed()
        assert row.has_class("-just-completed")
