"""TaskBar: pulsing-dot status indicator with state-aware copy."""

from __future__ import annotations

from unittest.mock import patch

import pytest
from textual.app import ComposeResult
from textual.widgets import Label

from lilbee.cli.tui.task_queue import TaskType
from lilbee.cli.tui.widgets.task_bar import TaskBar
from lilbee.cli.tui.widgets.task_bar_controller import TaskBarController
from tests._lilbee_app_test_host import LilbeeAppHost


class _Harness(LilbeeAppHost):
    def __init__(self) -> None:
        super().__init__()
        self.task_bar = TaskBarController(self)

    def compose(self) -> ComposeResult:
        yield TaskBar(id="tbar")


def _label_text(bar: TaskBar) -> str:
    label = bar.query_one("#task-status-label", Label)
    return str(label._Static__content)  # type: ignore[attr-defined]


@pytest.mark.asyncio
async def test_taskbar_hidden_when_idle() -> None:
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        bar = app.query_one(TaskBar)
        assert bar.display is False


@pytest.mark.asyncio
async def test_taskbar_single_active_shows_name_and_percent() -> None:
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        tid = app.task_bar.queue.enqueue(lambda: None, "Gemma 4", TaskType.DOWNLOAD.value)
        app.task_bar.queue.advance(TaskType.DOWNLOAD.value)
        app.task_bar.queue.update_task(tid, 42.5, "42/100 MB")
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        assert "●" in text
        assert "Gemma 4" in text
        assert "42.5%" in text
        # Single-active state drops the "1 task running" prefix.
        assert "tasks running" not in text.lower()


@pytest.mark.asyncio
async def test_taskbar_multiple_active_shows_count_plural() -> None:
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        for i in range(2):
            app.task_bar.queue.enqueue(lambda: None, f"m{i}", TaskType.DOWNLOAD.value)
            app.task_bar.queue.advance(TaskType.DOWNLOAD.value)
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        assert "2 tasks running" in text


@pytest.mark.asyncio
async def test_taskbar_dot_pulses_across_ticks() -> None:
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        app.task_bar.queue.enqueue(lambda: None, "demo", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        bar = app.query_one(TaskBar)
        bar._tick_count = 0
        bar._refresh_display()
        on = _label_text(bar)
        bar._tick_count = 5
        bar._refresh_display()
        off = _label_text(bar)
        assert on != off, "dot colour should differ between on-beat and off-beat"


@pytest.mark.asyncio
async def test_taskbar_queued_only_uses_muted_copy() -> None:
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        # Fill the download active slots, then enqueue a queued one.
        for i in range(2):
            tid = app.task_bar.queue.enqueue(lambda: None, f"m{i}", TaskType.DOWNLOAD.value)
            app.task_bar.queue.advance(TaskType.DOWNLOAD.value)
            app.task_bar.queue.complete_task(tid)
        tid_queued = app.task_bar.queue.enqueue(lambda: None, "waiting", TaskType.DOWNLOAD.value)
        # Don't advance: it stays queued (capacity unused because the
        # first two are DONE not ACTIVE; advance promotes next active).
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        # With at most active=0 queued=1, the bar shows "1 queued".
        if "queued" in text.lower():
            assert "1 queued" in text
        # Sanity: the widget is visible.
        assert bar.display is True
        # Cleanup so the next test starts idle.
        app.task_bar.queue.remove_task(tid_queued)


@pytest.mark.asyncio
async def test_taskbar_includes_hint_text() -> None:
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        app.task_bar.queue.enqueue(lambda: None, "demo", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        assert "Press t for Tasks" in text


@pytest.mark.asyncio
async def test_taskbar_hint_falls_back_when_focus_lookup_raises() -> None:
    """If ``self.app.focused`` can't be read, hint silently returns the default."""
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        bar = app.query_one(TaskBar)

        class _Boom:
            @property
            def focused(self):  # type: ignore[no-untyped-def]
                raise RuntimeError("focused lookup blew up")

            @property
            def screen_stack(self):  # type: ignore[no-untyped-def]
                return []

        boom = _Boom()
        with patch.object(type(bar), "app", new=property(lambda _s: boom)):
            assert bar._hint_copy() == "Press t for Tasks"


@pytest.mark.asyncio
async def test_taskbar_hint_becomes_esc_variant_when_input_focused() -> None:
    """With a chat-style Input focused, the hint should read 'Esc then t'."""
    from textual.widgets import Input

    class _InputHarness(LilbeeAppHost):
        def __init__(self) -> None:
            super().__init__()
            self.task_bar = TaskBarController(self)

        def compose(self) -> ComposeResult:
            yield Input(id="dummy-input")
            yield TaskBar(id="tbar")

    app = _InputHarness()
    async with app.run_test() as pilot:
        await pilot.pause()
        app.query_one("#dummy-input", Input).focus()
        await pilot.pause()
        app.task_bar.queue.enqueue(lambda: None, "demo", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        assert "Esc then t for Tasks" in text


@pytest.mark.asyncio
async def test_taskbar_flashes_each_completion_only_once() -> None:
    """Flash arms on the transition to DONE; a later tick must not re-arm.

    History now persists (Task Center rows stay as DONE until the user
    clears them), so the "history has a DONE entry" signal alone can't
    gate the flash or the bar would flash forever on the same task.
    ``_flashed_ids`` records which ids have already flashed.
    """
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        tid = app.task_bar.queue.enqueue(lambda: None, "demo", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        app.task_bar.queue.complete_task(tid)
        bar = app.query_one(TaskBar)
        bar._refresh_display()  # arms flash
        assert tid in bar._flashed_ids
        first_until = bar._flash_until_tick
        assert first_until is not None
        # Walk ticks past the flash window. _flash_until_tick must clear
        # and not re-arm for the same task_id.
        for _ in range(int(first_until) + 5):
            bar._tick_count += 1
            bar._refresh_display()
        assert bar._flash_until_tick is None, "flash re-armed for the same task_id"


@pytest.mark.asyncio
async def test_taskbar_prunes_flashed_ids_after_clear_history() -> None:
    """``_flashed_ids`` must shed entries for tasks the user has cleared."""
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        tid = app.task_bar.queue.enqueue(lambda: None, "demo", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        app.task_bar.queue.complete_task(tid)
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        assert tid in bar._flashed_ids
        app.task_bar.queue.clear_history()
        bar._refresh_display()
        assert tid not in bar._flashed_ids


@pytest.mark.asyncio
async def test_taskbar_completion_flash_after_queue_drains() -> None:
    """After the last task completes, show a 'Done' flash before hiding."""
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        tid = app.task_bar.queue.enqueue(lambda: None, "demo", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        app.task_bar.queue.complete_task(tid)
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        assert "Done" in text


@pytest.mark.asyncio
async def test_taskbar_failure_flash_shows_count() -> None:
    """Failed task keeps the bar lit with error-coloured copy."""
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        tid = app.task_bar.queue.enqueue(lambda: None, "oops", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        app.task_bar.queue.fail_task(tid, "network error")
        bar = app.query_one(TaskBar)
        bar._refresh_display()
        text = _label_text(bar)
        assert "failed" in text.lower()


@pytest.mark.asyncio
async def test_taskbar_starts_in_idle_mode_when_queue_empty() -> None:
    """First mount with no work keeps the timer at the idle cadence."""
    from lilbee.cli.tui.widgets.task_bar import _POLL_INTERVAL_IDLE_S

    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        bar = app.query_one(TaskBar)
        assert bar._idle_mode is True
        assert bar._interval is not None
        assert bar._interval._interval == _POLL_INTERVAL_IDLE_S


@pytest.mark.asyncio
async def test_taskbar_re_arms_at_active_rate_on_first_task() -> None:
    """An enqueued+advanced task flips the bar to the active cadence."""
    from lilbee.cli.tui.widgets.task_bar import _POLL_INTERVAL_ACTIVE_S

    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        bar = app.query_one(TaskBar)
        assert bar._idle_mode is True
        app.task_bar.queue.enqueue(lambda: None, "go", TaskType.DOWNLOAD.value)
        app.task_bar.queue.advance(TaskType.DOWNLOAD.value)
        bar._refresh_display()
        assert bar._idle_mode is False
        assert bar._interval is not None
        assert bar._interval._interval == _POLL_INTERVAL_ACTIVE_S


@pytest.mark.asyncio
async def test_taskbar_returns_to_idle_after_flash_clears() -> None:
    """Once the completion flash drains, the timer drops back to 1 Hz."""
    from lilbee.cli.tui.widgets.task_bar import (
        _POLL_INTERVAL_ACTIVE_S,
        _POLL_INTERVAL_IDLE_S,
    )

    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        bar = app.query_one(TaskBar)
        tid = app.task_bar.queue.enqueue(lambda: None, "one", TaskType.SYNC.value)
        app.task_bar.queue.advance(TaskType.SYNC.value)
        bar._refresh_display()
        assert bar._interval is not None
        assert bar._interval._interval == _POLL_INTERVAL_ACTIVE_S
        app.task_bar.queue.complete_task(tid)
        bar._refresh_display()
        # Flash window holds active cadence; force-expire it and re-poll.
        bar._flash_until_tick = bar._tick_count - 1
        bar._flash_outcome = None
        bar._refresh_display()
        assert bar._idle_mode is True
        assert bar._interval is not None
        assert bar._interval._interval == _POLL_INTERVAL_IDLE_S


@pytest.mark.asyncio
async def test_taskbar_unmount_stops_idle_interval() -> None:
    """on_unmount cancels whichever interval is currently armed."""
    app = _Harness()
    async with app.run_test() as pilot:
        await pilot.pause()
        bar = app.query_one(TaskBar)
        assert bar._idle_mode is True
        bar.on_unmount()
        assert bar._interval is None
