"""Tests for the filesystem tool renderers (tool_started + tool_call result)."""

from __future__ import annotations

from rich.console import Console, Group
from rich.syntax import Syntax
from rich.text import Text

from clients.terminal.tui.renderers.filesystem import (
    FilesystemToolResultRenderer,
    FilesystemToolStartedRenderer,
)
from clients.terminal.tui.themes import get_active_theme, set_active_theme


def _render_text(renderable) -> str:
    console = Console(record=True, width=100, force_terminal=True, color_system=None)
    console.print(renderable)
    return console.export_text()


def _body(panel_or_renderable):
    """Unwrap a Panel to its inner renderable (Text or Group)."""
    r = panel_or_renderable
    if hasattr(r, "renderable") and not isinstance(r, Text):
        r = r.renderable
    return r


def _group_parts(group) -> list:
    """The renderables inside a rich Group."""
    return list(group.renderables)


def _find_syntax(group) -> Syntax | None:
    """The first ``Syntax`` renderable inside a Group (or None)."""
    for r in _group_parts(group):
        if isinstance(r, Syntax):
            return r
    return None


def _span_styles_at(text: Text, substring: str) -> set[str]:
    """Return the styles that cover the first occurrence of ``substring``.

    Covers both explicit ``Text.spans`` (from ``.append(…, style=)``) and the
    ``Text.style`` default (from ``Text(str, style=…)``), which does not create
    a span.
    """
    styles: set[str] = set()
    idx = text.plain.find(substring)
    if idx < 0:
        return styles
    start, end = idx, idx + len(substring)
    covered = False
    for span in text.spans:
        if span.start < end and span.end > start:
            styles.add(span.style)
            covered = True
    if not covered and text.style:
        styles.add(text.style)
    return styles


# ── accepts ──────────────────────────────────────────────────────────────────


def test_accepts_filesystem_tool_call() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    assert renderer.accepts({"type": "tool_call", "tool": "filesystem", "args": {}})


def test_does_not_accept_other_tools() -> None:
    renderer = FilesystemToolResultRenderer()
    assert not renderer.accepts({"type": "tool_call", "tool": "code_exec", "args": {}})


def test_does_not_accept_non_tool_call() -> None:
    renderer = FilesystemToolResultRenderer()
    assert not renderer.accepts({"type": "tool_started", "tool": "filesystem", "args": {}})
    assert not renderer.accepts({"type": "diff", "content": ""})


# ── diff ──────────────────────────────────────────────────────────────────────


def test_diff_action_highlights_added_and_removed() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "diff"},
        "result": "--- a.py\n+++ b.py\n@@ -1,2 +1,2 @@\n-old\n+new\n unchanged\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "+new") == {theme.success.hex}
    assert _span_styles_at(body, "-old") == {theme.error.hex}
    assert theme.text_dim.hex in _span_styles_at(body, "@@ -1,2 +1,2 @@")


def test_diff_identical_renders_plain_dim() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "diff"},
        "result": "Files are identical.",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert body.plain == "Files are identical."
    assert _span_styles_at(body, "Files are identical.") == {theme.text_dim.hex}


def test_edit_lines_separates_summary_and_diff() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "edit_lines"},
        "result": (
            "Applied 2 operation(s) to a.py (3 → 5 lines).\n\n"
            "--- a.py\n+++ b.py\n@@ -1,1 +1,1 @@\n-x\n+y\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    # Group(summary, blank, diff)
    renderables = body.renderables
    summary = renderables[0]
    diff = renderables[2]
    assert isinstance(summary, Text)
    assert "Applied 2 operation(s) to a.py" in summary.plain
    assert _span_styles_at(summary, "Applied 2 operation(s) to a.py") == {theme.text_dim.hex}
    assert isinstance(diff, Text)
    assert _span_styles_at(diff, "+y") == {theme.success.hex}
    assert _span_styles_at(diff, "-x") == {theme.error.hex}


def test_edit_lines_without_diff_body_shows_summary_only() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "edit_lines"},
        "result": "Applied 1 operation(s) to a.py (3 → 3 lines).",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert body.plain.startswith("Applied 1 operation(s)")


def test_edit_renders_diff_from_metadata() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "edit"},
        "result": "Edited a.py: replaced 5 B with 5 B (delta +0 B).",
        "success": True,
        "metadata": {
            "diff": "--- a.py\n+++ b.py\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n"
        },
    }
    body = _body(renderer.render(msg))
    # Group(summary, blank, diff)
    renderables = body.renderables
    summary = renderables[0]
    diff = renderables[2]
    assert isinstance(summary, Text)
    assert "Edited a.py" in summary.plain
    assert _span_styles_at(summary, "Edited a.py") == {theme.text_dim.hex}
    assert isinstance(diff, Text)
    assert _span_styles_at(diff, "+BETA") == {theme.success.hex}
    assert _span_styles_at(diff, "-beta") == {theme.error.hex}


def test_diff_line_number_column_is_dim() -> None:
    """Server-prefixed ``{marker} {num}│ {content}`` lines render with the
    number+marker in the marker colour (green/red) and the content neutral
    (theme.text) — the number is a coloured anchor, the code reads plainly.
    Display order is ``{num} {marker}  {content}``; the server still emits
    marker-first in the model-facing text, so the agent's contract is unchanged."""
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "diff"},
        "result": (
            "--- a.py\n+++ b.py\n@@ -1,3 +1,3 @@\n"
            "  1│ alpha\n- 2│ beta\n+ 2│ BETA\n  3│ gamma\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    # The number+marker anchor takes the marker colour (red for removed).
    assert _span_styles_at(body, "2 -  ") == {theme.error.hex}
    # ...and green for added.
    assert _span_styles_at(body, "2 +  ") == {theme.success.hex}
    # The content after the anchor is neutral (theme.text), not the marker colour.
    assert _span_styles_at(body, "BETA") == {theme.text.hex}
    assert _span_styles_at(body, "beta") == {theme.text.hex}
    assert _span_styles_at(body, "alpha") == {theme.text.hex}
    # Hunk header is dim, file headers are plain text.
    assert theme.text_dim.hex in _span_styles_at(body, "@@ -1,3 +1,3 @@")


def test_diff_numbered_lines_beyond_nine_rows_parses_width() -> None:
    """The server right-aligns line numbers to the max width, so a diff of a
    >9-line file has multi-space leading on single-digit rows. The number regex
    must still match these (the old single-space regex dropped them to the
    fallback path and coloured the whole line). Number+marker keep the marker
    colour; content is neutral."""
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "diff"},
        "result": (
            "--- a.py\n+++ b.py\n@@ -1,12 +1,12 @@\n"
            "   1│ a\n  10│ j\n  11│ k\n- 12│ old\n+ 12│ NEW\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    # Single-digit row (1) and double-digit rows (12) both parse: the anchor
    # (num+marker) carries the marker colour, not the whole line.
    assert _span_styles_at(body, " 12 -  ") == {theme.error.hex}
    assert _span_styles_at(body, " 12 +  ") == {theme.success.hex}
    # Content is neutral.
    assert _span_styles_at(body, "old") == {theme.text.hex}
    assert _span_styles_at(body, "NEW") == {theme.text.hex}
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "edit"},
        "result": "Edited a.py: replaced 5 B with 5 B (delta +0 B).",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "Edited a.py") == {theme.text_dim.hex}


def test_diff_error_renders_plain_error_color() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "diff"},
        "result": "'destination' is required for diff (path to second file)",
        "success": False,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "destination") == {theme.tool_error.hex}


# ── list ──────────────────────────────────────────────────────────────────────


def test_list_highlights_directories_and_files() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "list"},
        "result": (
            "[/proj  |  2 entries]\n"
            "d  src/  (12 items)\n"
            "   main.py                                       1.2K  2026-07-12 14:30\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    # Header plaque is dim.
    assert theme.text_dim.hex in _span_styles_at(body, "/proj")
    # Directory name in info color.
    assert theme.info.hex in _span_styles_at(body, "src/")
    # File name in text color, size in accent.
    assert theme.text.hex in _span_styles_at(body, "main.py")
    assert theme.accent.hex in _span_styles_at(body, "1.2K")
    # Directory entry carries the ▸ icon.
    assert "▸" in body.plain
    assert "(12 items)" in body.plain


def test_list_truncated_header_uses_warning_color() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "list"},
        "result": "[/proj  |  500 entries  ⚠ truncated]\n   a.py                                         0B  2026-07-12 00:00\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert theme.warning.hex in _span_styles_at(body, "truncated")


# ── grep ──────────────────────────────────────────────────────────────────────


def test_grep_highlights_pattern_and_dims_location() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "grep", "pattern": "foo"},
        "result": (
            "[3 matches for 'foo' in /proj (5 files searched)]\n"
            "src/a.py:3: foo bar\n"
            "src/b.py:7: foobar\n",
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    # Header is dim.
    assert theme.text_dim.hex in _span_styles_at(body, "3 matches")
    # Location prefix is dim.
    assert theme.text_dim.hex in _span_styles_at(body, "src/a.py:3:")
    # Matched pattern occurrence on the match line is in accent. Check "foo bar"
    # (the match line) rather than "foo" alone — "foo" first appears in the dim
    # header plaque "[3 matches for 'foo' in …]".
    styles_foo_bar = _span_styles_at(body, "foo bar")
    assert theme.accent.hex in styles_foo_bar


def test_grep_no_matches_renders_dim() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "grep", "pattern": "foo"},
        "result": "No matches for 'foo' in /proj",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "No matches") == {theme.text_dim.hex}


def test_grep_without_pattern_arg_does_not_crash() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "grep"},
        "result": "[1 matches for 'x' in /proj (1 files searched)]\na.py:1: x y\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert "a.py:1: x y" in body.plain


# ── find ──────────────────────────────────────────────────────────────────────


def test_find_highlights_header_and_match_lines() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "find", "pattern": "*.py"},
        "result": (
            "[2 matches for '*.py' in /proj]\n"
            "/proj/a.py  (1.2K)\n"
            "/proj/src  (<dir>)\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    # Header plaque is dim.
    assert theme.text_dim.hex in _span_styles_at(body, "2 matches")
    # Match path in text, size in dim.
    assert theme.text.hex in _span_styles_at(body, "/proj/a.py")
    assert theme.text_dim.hex in _span_styles_at(body, "1.2K")
    # "<dir>" marker in info color.
    assert theme.info.hex in _span_styles_at(body, "<dir>")


def test_find_truncated_header_uses_warning_color() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "find", "pattern": "*"},
        "result": "[200 matches for '*' in /proj  ⚠ showing first 200]\n/p/a  (0B)\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert theme.warning.hex in _span_styles_at(body, "showing first 200")


def test_find_no_matches_renders_dim() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "find", "pattern": "*.x"},
        "result": "No matches for '*.x' in /proj",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "No matches") == {theme.text_dim.hex}


# ── find_up ───────────────────────────────────────────────────────────────────


def test_find_up_found_renders_path_in_accent() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "find_up", "pattern": "pyproject.toml"},
        "result": "/proj/pyproject.toml",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "/proj/pyproject.toml") == {theme.accent.hex}


def test_find_up_not_found_renders_dim() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "find_up", "pattern": "x.txt"},
        "result": "not found (searched: /a/x.txt, /x.txt)",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "not found") == {theme.text_dim.hex}


# ── fallback ──────────────────────────────────────────────────────────────────


# ── read ──────────────────────────────────────────────────────────────────────


def test_read_separates_header_and_body() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "path": "src/main.py"},
        "result": "[src/main.py  |  2 lines  |  64B]\n1: print('hi')\n2: print('bye')\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Group)
    header = _group_parts(body)[0]
    assert isinstance(header, Text)
    # Path inside the header plaque is accent.
    assert theme.accent.hex in _span_styles_at(header, "src/main.py")
    # The "N lines | size" tail of the plaque is dim.
    assert theme.text_dim.hex in _span_styles_at(header, "2 lines  |  64B")

    # Body is syntax-highlighted: a Syntax renderable whose lexer is Python
    # (guessed from the .py path) and whose raw code recovered the file
    # content (server ``num: `` prefix stripped).
    syntax = _find_syntax(body)
    assert syntax is not None
    assert syntax.lexer.name == "Python"
    assert syntax.code == "print('hi')\nprint('bye')"
    assert syntax.line_numbers is True
    assert syntax.start_line == 1


def test_read_offset_header() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "offset": 5, "limit": 10, "path": "src/main.py"},
        "result": "[src/main.py  |  lines 5–14 of 100  |  4.0K]\n     5: body line\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Group)
    header = _group_parts(body)[0]
    assert isinstance(header, Text)
    assert theme.accent.hex in _span_styles_at(header, "src/main.py")
    assert theme.text_dim.hex in _span_styles_at(header, "lines 5–14 of 100")

    # The line-number column starts at the read offset (5), so the Syntax
    # block's own numbering matches the server's, not always 1.
    syntax = _find_syntax(body)
    assert syntax is not None
    assert syntax.start_line == 5
    assert syntax.code == "body line"


def test_read_large_file_warning_is_warning_color() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "path": "big.log"},
        "result": (
            "[big.log  |  9000 lines  |  1.2M]\n"
            "⚠ Large file (1.2M) — consider offset/limit next time.\n"
            "1: first line\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Group)
    parts = _group_parts(body)
    # [header_text, warning_text, blank, syntax]
    assert isinstance(parts[0], Text)
    assert theme.accent.hex in _span_styles_at(parts[0], "big.log")
    warning = parts[1]
    assert isinstance(warning, Text)
    assert theme.warning.hex in _span_styles_at(warning, "Large file")
    # The body line still reaches the Syntax block.
    syntax = _find_syntax(body)
    assert syntax is not None
    assert "first line" in syntax.code


def test_read_numbered_highlights_line_numbers() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "numbered": True, "path": "a.py"},
        "result": "[a.py  |  2 lines  |  16B]\n1: foo\n2: bar\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Group)
    # With numbered output the server ``num: `` prefix is stripped and Syntax
    # renders its own line-number column; the raw code is the file content
    # without the numbers.
    syntax = _find_syntax(body)
    assert syntax is not None
    assert syntax.line_numbers is True
    assert syntax.start_line == 1
    assert syntax.code == "foo\nbar"


def test_read_unnumbered_omits_line_numbers() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "numbered": False, "path": "a.py"},
        "result": "[a.py  |  2 lines  |  16B]\nfoo\nbar\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Group)
    syntax = _find_syntax(body)
    assert syntax is not None
    assert syntax.line_numbers is False
    assert syntax.code == "foo\nbar"


def test_read_guesses_language_from_path() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "path": "src/app.ts"},
        "result": "[src/app.ts  |  1 lines  |  8B]\n1: const x = 1\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    syntax = _find_syntax(body)
    assert syntax is not None
    assert syntax.lexer.name in {"TypeScript", "TSX"}


def test_read_unknown_extension_falls_back_to_text_lexer() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read", "path": "notes.weirdext"},
        "result": "[notes.weirdext  |  1 lines  |  3B]\n1: hi\n",
        "success": True,
    }
    body = _body(renderer.render(msg))
    syntax = _find_syntax(body)
    assert syntax is not None
    # Unknown extension -> "text" lexer (plain, no token colors).
    assert syntax.lexer.name in {"Text only", "Text", "Raw token data"}


def test_read_error_renders_plain_error_color() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "read"},
        "result": "File not found: /missing.py",
        "success": False,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert _span_styles_at(body, "File not found") == {theme.tool_error.hex}


# ── info ──────────────────────────────────────────────────────────────────────


def test_info_highlights_keys_and_values() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "info"},
        "result": (
            "path:     /proj/main.py\n"
            "type:     file\n"
            "size:     1.2K\n"
            "modified: 2026-07-12 14:30\n"
        ),
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    # Keys are accent.
    assert _span_styles_at(body, "path") == {theme.accent.hex}
    assert _span_styles_at(body, "type") == {theme.accent.hex}
    assert _span_styles_at(body, "modified") == {theme.accent.hex}
    # Values are text.
    assert _span_styles_at(body, "/proj/main.py") == {theme.text.hex}
    assert _span_styles_at(body, "file") == {theme.text.hex}
    assert _span_styles_at(body, "1.2K") == {theme.text.hex}


def test_unknown_action_falls_back_to_plain() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "args": {"action": "query"},
        "result": "some natural-language answer",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert body.plain == "some natural-language answer"


def test_missing_args_treats_action_as_unknown() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    msg = {
        "type": "tool_call",
        "tool": "filesystem",
        "result": "raw output",
        "success": True,
    }
    body = _body(renderer.render(msg))
    assert isinstance(body, Text)
    assert body.plain == "raw output"


# ── subagent + title ──────────────────────────────────────────────────────────


def test_panel_title_carries_status() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    ok = renderer.render(
        {"type": "tool_call", "tool": "filesystem", "args": {"action": "list"},
         "result": "[/p  |  0 entries]\n(empty directory)", "success": True}
    )
    assert "← filesystem" in str(ok.title)
    assert "✓" in str(ok.title)

    err = renderer.render(
        {"type": "tool_call", "tool": "filesystem", "args": {"action": "list"},
         "result": "Path not found: /p", "success": False}
    )
    assert "✗" in str(err.title)


def test_subagent_result_is_indented() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    renderable = renderer.render(
        {"type": "tool_call", "tool": "filesystem", "args": {"action": "read"},
         "result": "x", "success": True, "is_subagent": True}
    )
    assert any(line.startswith("  ") for line in _render_text(renderable).splitlines())


def test_non_subagent_result_not_indented() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolResultRenderer()
    renderable = renderer.render(
        {"type": "tool_call", "tool": "filesystem", "args": {"action": "read"},
         "result": "x", "success": True, "is_subagent": False}
    )
    assert any(line.startswith("╭") for line in _render_text(renderable).splitlines())

# ── tool_started ──────────────────────────────────────────────────────────────


def _started_msg(args: dict, **extra) -> dict:
    msg = {"type": "tool_started", "tool": "filesystem", "args": args}
    msg.update(extra)
    return msg


def test_started_accepts_filesystem_only() -> None:
    renderer = FilesystemToolStartedRenderer()
    assert renderer.accepts(_started_msg({"action": "read", "path": "a.py"}))
    assert not renderer.accepts({"type": "tool_started", "tool": "code_exec", "args": {}})
    assert not renderer.accepts({"type": "tool_call", "tool": "filesystem", "args": {}})


def test_started_title_carries_action_only() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(_started_msg({"action": "read", "path": "src/main.py"}))
    title = str(panel.title)
    assert "filesystem" in title
    assert "read" in title
    # Path is in the body, not the title.
    assert "src/main.py" not in title
    body = _body(panel)
    assert "src/main.py" in body.plain


def test_started_destination_in_body_for_move_copy_diff() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    move = renderer.render(_started_msg({"action": "move", "path": "a.py", "destination": "b.py"}))
    assert "b.py" not in str(move.title)
    assert "b.py" in _body(move).plain
    diff = renderer.render(_started_msg({"action": "diff", "path": "a.py", "destination": "b.py"}))
    assert "b.py" in _body(diff).plain
    # Non-destination actions do not add a destination line.
    read = renderer.render(_started_msg({"action": "read", "path": "a.py", "destination": "x"}))
    # "destination" key is absent; "x" may appear only if path == "x" (it isn't).
    assert "destination" not in _body(read).plain


def test_started_info_shows_path_in_body() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(_started_msg({"action": "info", "path": "a.py"}))
    body = _body(panel)
    assert isinstance(body, Text)
    assert "path" in body.plain
    assert "a.py" in body.plain


def test_started_read_shows_range_and_numbered() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(
        _started_msg({"action": "read", "path": "a.py", "offset": 5, "limit": 10, "numbered": True})
    )
    body = _body(panel)
    assert "range" in body.plain
    assert "5–10" in body.plain
    assert "numbered" in body.plain


def test_started_write_previews_multiline_content() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(
        _started_msg({"action": "write", "path": "a.py", "content": "line1\nline2\nline3\n"})
    )
    body = _body(panel)
    assert '"line1"' in body.plain
    assert "(+2 lines)" in body.plain
    # Full content is not dumped.
    assert "line3" not in body.plain


def test_started_edit_previews_old_and_new() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(
        _started_msg({"action": "edit", "path": "a.py", "old": "foo", "new": "bar\nbaz"})
    )
    body = _body(panel)
    assert "old" in body.plain
    assert "new" in body.plain
    assert '"foo"' in body.plain
    assert '"bar"' in body.plain
    assert "(+1 lines)" in body.plain


def test_started_edit_lines_summarizes_operations() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    ops = [
        {"op": "replace", "start": 1, "end": 1, "content": "x"},
        {"op": "delete", "start": 5, "end": 5},
        {"op": "insert", "after": 7, "content": "y"},
    ]
    panel = renderer.render(_started_msg({"action": "edit_lines", "path": "a.py", "operations": ops}))
    body = _body(panel)
    assert "operations" in body.plain
    assert "3:" in body.plain
    assert "replace" in body.plain
    assert "delete" in body.plain


def test_started_grep_shows_pattern_glob_regex() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(
        _started_msg({"action": "grep", "path": "src", "pattern": "TODO", "glob": "*.py", "regex": True})
    )
    body = _body(panel)
    assert "pattern" in body.plain
    assert "TODO" in body.plain
    assert "glob" in body.plain
    assert "*.py" in body.plain
    assert "regex" in body.plain


def test_started_smart_edit_previews_instruction() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(
        _started_msg({"action": "smart_edit", "path": "a.py", "instruction": "rename foo to bar"})
    )
    body = _body(panel)
    assert "instruction" in body.plain
    assert "rename foo to bar" in body.plain


def test_started_query_previews_question() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(
        _started_msg({"action": "query", "path": "a.py", "question": "What does calculate() return?"})
    )
    body = _body(panel)
    assert "question" in body.plain
    assert "calculate()" in body.plain


def test_started_subagent_is_indented() -> None:
    set_active_theme("gnexus-dark")
    renderer = FilesystemToolStartedRenderer()
    renderable = renderer.render(
        _started_msg({"action": "read", "path": "a.py"}, is_subagent=True)
    )
    assert any(line.startswith("  ") for line in _render_text(renderable).splitlines())


def test_started_body_keys_are_dim() -> None:
    set_active_theme("gnexus-dark")
    theme = get_active_theme()
    renderer = FilesystemToolStartedRenderer()
    panel = renderer.render(_started_msg({"action": "grep", "path": "src", "pattern": "TODO"}))
    body = _body(panel)
    assert isinstance(body, Text)
    assert _span_styles_at(body, "pattern") == {theme.text_dim.hex}
    # Pattern value is in text.
    assert theme.text.hex in _span_styles_at(body, "TODO")
    # Path value is in accent.
    assert _span_styles_at(body, "src") == {theme.accent.hex}
