import unittest
from datetime import datetime
from io import StringIO
from types import SimpleNamespace
from unittest import IsolatedAsyncioTestCase
from unittest.mock import MagicMock, patch
from uuid import UUID

import numpy as np
from numpy.linalg import norm
from rich import box
from rich.console import Console
from rich.spinner import Spinner
from rich.table import Table
from rich.text import Text

from avalan.cli.theme import fancy as fancy_theme_module
from avalan.cli.theme.fancy import FancyTheme
from avalan.entities import (
    EngineMessage,
    EngineMessageScored,
    HubCache,
    HubCacheDeletion,
    HubCacheFile,
    ImageEntity,
    Message,
    ModelConfig,
    SearchMatch,
    SentenceTransformerModelConfig,
    Similarity,
    Token,
    TokenDetail,
    TokenizerConfig,
    ToolCall,
    ToolCallDiagnostic,
    ToolCallDiagnosticCode,
    ToolCallDiagnosticStage,
    ToolCallError,
    User,
)
from avalan.event import Event, EventStats, EventType
from avalan.memory.partitioner.text import TextPartition
from avalan.memory.permanent import PermanentMemoryPartition


class FancyThemeFlowProgressTestCase(unittest.TestCase):
    def test_flow_run_progress_message_reports_active_node(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)

        message = theme.flow_run_progress_message(
            "flow_node_started",
            node="analyze_pov_1",
            attempt=2,
        )

        self.assertIn("Running", message)
        self.assertIn("analyze_pov_1", message)
        self.assertIn("(attempt 2)", message)

    def test_flow_run_progress_message_omits_first_attempt(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)

        message = theme.flow_run_progress_message(
            "flow_node_started",
            node="analyze_pov_1",
            attempt=1,
        )

        self.assertIn("Running", message)
        self.assertIn("analyze_pov_1", message)
        self.assertNotIn("attempt", message)

    def test_flow_run_progress_message_covers_remaining_events(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        cases = {
            "flow_node_retrying": (
                "Retrying [cyan]node_a[/cyan] after attempt 2."
            ),
            "flow_node_completed": "Finished [cyan]node_a[/cyan].",
            "flow_node_failed": "[cyan]node_a[/cyan] failed.",
            "flow_node_skipped": "Skipped [cyan]node_a[/cyan].",
            "flow_node_paused": "Paused [cyan]node_a[/cyan].",
            "flow_node_resumed": "Resumed [cyan]node_a[/cyan].",
            "flow_node_cancelled": "Cancelled [cyan]node_a[/cyan].",
        }

        for event_type, expected in cases.items():
            with self.subTest(event_type=event_type):
                self.assertEqual(
                    theme.flow_run_progress_message(
                        event_type,
                        node="node_a",
                        attempt=2,
                    ),
                    expected,
                )

        self.assertEqual(
            theme.flow_run_progress_message("flow_cancelled"),
            "Flow run cancelled.",
        )

    def test_flow_run_progress_inverts_running_node_class(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        captured: dict[str, str] = {}

        def fake_render(source: str, console_width: int) -> Text:
            captured["source"] = source
            captured["width"] = str(console_width)
            return Text("diagram")

        with patch(
            "avalan.cli.theme.fancy._flow_run_mermaid_renderable",
            fake_render,
        ):
            theme.flow_run_progress(
                "flowchart LR\n  analyze_pov_1[analyze_pov_1]\n",
                node_states={"analyze_pov_1": "pending"},
                active_nodes=("analyze_pov_1",),
                message="Running node [cyan]analyze_pov_1[/cyan].",
                console_width=120,
            )

        self.assertIn(
            "classDef avalanRunning fill:#ecfeff,stroke:#0f172a",
            captured["source"],
        )
        self.assertIn(
            "class analyze_pov_1 avalanRunning",
            captured["source"],
        )

    def test_flow_run_progress_renders_sidebar_stats(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        console = Console(file=StringIO(), record=True, width=160)
        renderable = theme.flow_run_progress(
            "flowchart LR\n  analyze_pov_1[analyze_pov_1]\n",
            node_states={
                "analyze_pov_1": "succeeded",
                "write_spec_request": "failed",
            },
            active_nodes=(),
            message="Finished [cyan]analyze_pov_1[/cyan].",
            console_width=160,
            flow_stats={
                "__total__": {
                    "elapsed_ms": 1500,
                    "executed_nodes": 2,
                    "succeeded_nodes": 1,
                    "failed_nodes": 1,
                    "average_node_ms": 750,
                    "input_tokens": 1234,
                    "cached_input_tokens": 99,
                    "output_tokens": 56,
                    "reasoning_tokens": 7,
                    "tools_executed": 3,
                },
                "analyze_pov_1": {
                    "elapsed_ms": 1000,
                    "input_tokens": 12,
                    "cached_input_tokens": 4,
                    "output_tokens": 34,
                    "reasoning_tokens": 5,
                    "tools_executed": 2,
                },
            },
        )

        console.print(renderable)
        output = console.export_text()

        self.assertIn("Stats", output)
        self.assertIn("Nodes", output)
        self.assertIn("time", output)
        self.assertIn("nodes", output)
        self.assertIn("cached", output)
        self.assertIn("tool", output)
        self.assertIn("1.5s", output)
        self.assertIn("1,234", output)
        self.assertIn("out 56 🔥", output)
        self.assertNotIn("nodes 🔗", output)
        self.assertNotIn("ok ✅", output)
        self.assertNotIn("out 🚀", output)
        self.assertRegex(output, "📨\\s+12")
        self.assertRegex(output, "💾\\s+33%")
        self.assertRegex(output, "💬\\s+34")
        self.assertRegex(output, "🧠\\s+5")
        self.assertRegex(output, "🛠\\s+2")
        self.assertNotRegex(output, "[\\u2800-\\u28ff]")

    def test_flow_run_stats_header_expands_columns(self):
        console = Console(file=StringIO(), record=True, width=100)
        console.print(
            fancy_theme_module._flow_run_stats_header(
                {
                    "__total__": {
                        "elapsed_ms": 1500,
                        "executed_nodes": 7,
                        "succeeded_nodes": 7,
                        "failed_nodes": 0,
                        "average_node_ms": 3100,
                        "input_tokens": 2092,
                        "cached_input_tokens": 1536,
                        "output_tokens": 94,
                        "reasoning_tokens": 0,
                        "tools_executed": 0,
                    }
                }
            )
        )

        stat_line = next(
            line for line in console.export_text().splitlines()
            if "nodes 7" in line
        )
        self.assertGreaterEqual(stat_line.index("tool"), 75)

    def test_flow_run_progress_sanitizes_negative_stats(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        console = Console(file=StringIO(), record=True, width=120)
        renderable = theme.flow_run_progress(
            "flowchart LR\n  analyze_pov_1[analyze_pov_1]\n",
            node_states={"analyze_pov_1": "running"},
            active_nodes=("analyze_pov_1",),
            message="Running [cyan]analyze_pov_1[/cyan].",
            console_width=120,
            flow_stats={
                "__total__": {
                    "elapsed_ms": -1,
                    "executed_nodes": True,
                    "succeeded_nodes": -2,
                    "failed_nodes": -3,
                    "average_node_ms": -4,
                    "input_tokens": -5,
                    "cached_input_tokens": -9,
                    "output_tokens": -6,
                    "reasoning_tokens": -7,
                    "tools_executed": -8,
                }
            },
        )

        console.print(renderable)
        output = console.export_text()

        self.assertIn("0ms", output)
        self.assertNotIn("-5", output)

    def test_flow_run_styled_mermaid_skips_unsafe_node(self):
        source = fancy_theme_module._flow_run_styled_mermaid_source(
            "flowchart LR\n  bad-node[bad-node]\n",
            node_states={
                "bad-node": "running",
                "safe_node": "unknown",
            },
            active_nodes=(),
        )

        self.assertNotIn("class bad-node", source)
        self.assertNotIn("class safe_node", source)

    def test_flow_run_helper_branches(self):
        self.assertEqual(
            fancy_theme_module._flow_run_node_class("skipped"),
            "avalanSkipped",
        )
        self.assertEqual(
            fancy_theme_module._flow_run_node_class("paused"),
            "avalanPaused",
        )
        self.assertIsNone(fancy_theme_module._flow_run_node_class("pending"))
        self.assertIsNone(fancy_theme_module._flow_run_status_table({}))
        self.assertEqual(
            fancy_theme_module._flow_run_format_duration(61_000),
            "1m01s",
        )
        self.assertEqual(
            fancy_theme_module._flow_run_format_duration(3_660_000),
            "1h01m",
        )
        self.assertEqual(
            fancy_theme_module._flow_run_state_display("skipped")[0],
            "-",
        )
        self.assertEqual(
            fancy_theme_module._flow_run_state_display("paused")[0],
            "=",
        )

    def test_flow_run_mermaid_renderable_falls_back_without_termaid(self):
        with patch(
            "avalan.cli.theme.fancy.import_module",
            side_effect=ImportError,
        ):
            renderable = fancy_theme_module._flow_run_mermaid_renderable(
                "flowchart LR\n",
                80,
            )

        self.assertIsInstance(renderable, Text)

    def test_flow_run_mermaid_renderable_falls_back_without_renderer(self):
        module = SimpleNamespace(render_rich=None)
        with patch(
            "avalan.cli.theme.fancy.import_module",
            return_value=module,
        ):
            renderable = fancy_theme_module._flow_run_mermaid_renderable(
                "flowchart LR\n",
                80,
            )

        self.assertIsInstance(renderable, Text)

    def test_flow_run_mermaid_renderable_reports_renderer_error(self):
        def render_rich(*_args, **_kwargs):
            raise RuntimeError("private renderer detail")

        module = SimpleNamespace(render_rich=render_rich)
        with patch(
            "avalan.cli.theme.fancy.import_module",
            return_value=module,
        ):
            renderable = fancy_theme_module._flow_run_mermaid_renderable(
                "flowchart LR\n",
                80,
            )

        console = Console(file=StringIO(), record=True, width=120)
        console.print(renderable)
        output = console.export_text()

        self.assertIn("Diagram renderer unavailable", output)
        self.assertIn("RuntimeError", output)


class FancyThemeTokensTestCase(IsolatedAsyncioTestCase):
    async def test_tool_running_spinner_text(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        spinner = Spinner("dots", text="[cyan]run[/cyan]", style="cyan")
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=["a"],
                tokens=None,
                input_token_count=0,
                total_tokens=0,
                tool_events=[],
                tool_event_calls=[
                    Event(type=EventType.TOOL_PROCESS, payload=[])
                ],
                tool_event_results=[],
                tool_running_spinner=spinner,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=80,
                logger=MagicMock(),
            )
            frame = await gen.__anext__()
        self.assertTrue(
            any(
                getattr(r, "renderable", None) is spinner
                for r in frame[1].renderables
            )
        )

    async def test_tool_text_tokens_panel(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=["tool"],
                answer_text_tokens=["answer"],
                tokens=None,
                input_token_count=0,
                total_tokens=0,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=80,
                logger=MagicMock(),
            )
            _, frame = await gen.__anext__()
        self.assertEqual(len(frame.renderables), 2)
        self.assertIn("tool", str(frame.renderables[0].renderable.renderable))

    async def test_progress_title_pluralization_singular(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        es = EventStats()
        es.triggers = {
            EventType.TOOL_EXECUTE: 1,
            EventType.TOOL_RESULT: 1,
        }
        es.total_triggers = 1
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=["a"],
                tokens=None,
                input_token_count=1,
                total_tokens=1,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=80,
                logger=MagicMock(),
                event_stats=es,
            )
            _, frame = await gen.__anext__()
        subtitle = frame.renderables[0].subtitle
        self.assertIn("1 token in", subtitle)
        self.assertIn("1 token out", subtitle)
        self.assertIn("1 event", subtitle)
        self.assertIn("1 tool call", subtitle)
        self.assertIn("1 result", subtitle)

    async def test_progress_title_pluralization_plural(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        es = EventStats()
        es.triggers = {
            EventType.TOOL_EXECUTE: 2,
            EventType.TOOL_RESULT: 2,
        }
        es.total_triggers = 2
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=["a"],
                tokens=None,
                input_token_count=2,
                total_tokens=2,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=80,
                logger=MagicMock(),
                event_stats=es,
                tool_token_count=3,
            )
            _, frame = await gen.__anext__()
        subtitle = frame.renderables[0].subtitle
        self.assertIn("2 tokens in", subtitle)
        self.assertIn("2 tokens out", subtitle)
        self.assertIn("3 tool tokens", subtitle)
        self.assertIn("2 events", subtitle)
        self.assertIn("2 tool calls", subtitle)
        self.assertIn("2 results", subtitle)

    async def test_progress_panel_without_text_output(self):
        theme = FancyTheme(lambda s: s, lambda s, p, n: s if n == 1 else p)
        es = EventStats()
        es.triggers = {}
        es.total_triggers = 3
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=[],
                tokens=None,
                input_token_count=4,
                total_tokens=0,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=None,
                ttnt=None,
                ttsr=None,
                elapsed=2.0,
                console_width=80,
                logger=MagicMock(),
                event_stats=es,
            )
            _, frame = await gen.__anext__()

        self.assertEqual(len(frame.renderables), 1)
        panel = frame.renderables[0]
        self.assertIn("Token stats", str(panel.title))
        self.assertIn("4 tokens in", str(panel.renderable))
        self.assertIn("0 tokens out", str(panel.renderable))
        self.assertIn("3 events", str(panel.renderable))


class FancyThemeTestCase(IsolatedAsyncioTestCase):
    def setUp(self):
        self.theme = FancyTheme(
            lambda s: s, lambda s, p, n: s if n == 1 else p
        )

    def test_quantity_data(self):
        self.assertEqual(self.theme.quantity_data, ["likes"])

    def test_agent(self):
        memory = SimpleNamespace(
            has_recent_message=True,
            has_permanent_message=True,
            permanent_message=SimpleNamespace(
                session_id=UUID("11111111-1111-1111-1111-111111111111"),
                has_session=True,
            ),
        )
        agent = SimpleNamespace(id=UUID(int=0), name="agent", memory=memory)
        model = SimpleNamespace(
            id="m",
            parameters=1,
            parameter_types=["p"],
            inference=None,
            library_name=None,
            pipeline_tag=None,
            tags=[],
            architectures=None,
            model_type=None,
            license=None,
            gated=False,
            private=False,
            disabled=False,
            downloads=1,
            likes=2,
            ranking=1,
            author="a",
            created_at=datetime.now(),
            updated_at=datetime.now(),
        )
        panel = self.theme.agent(agent, models=[model], can_access=True)
        self.assertTrue(panel.title)

    def test_ask_methods(self):
        self.assertEqual(
            self.theme.ask_access_token(),
            "Enter your Huggingface access token",
        )
        self.assertEqual(
            self.theme.ask_delete_paths(), "Delete selected paths?"
        )
        self.assertEqual(
            self.theme.ask_login_to_hub(), "Login to huggingface?"
        )
        self.assertEqual(
            self.theme.ask_secret_password("k"), "Enter secret for k"
        )
        self.assertEqual(
            self.theme.ask_override_secret("k"), "Secret k exists, override?"
        )

    def test_cache_methods(self):
        deletion = HubCacheDeletion(
            model_id="m",
            revisions=["r"],
            deletable_size_on_disk=1,
            deletable_blobs=["b"],
            deletable_refs=[],
            deletable_repos=[],
            deletable_snapshots=[],
        )
        result = self.theme.cache_delete(deletion)
        self.assertTrue(
            hasattr(result, "renderables") or hasattr(result, "text")
        )

        cache = HubCache(
            model_id="m",
            path="/p",
            size_on_disk=1,
            revisions=["r"],
            files={},
            total_files=0,
            total_revisions=1,
        )
        table = self.theme.cache_list("/c", [cache], show_summary=True)
        self.assertEqual(len(table.rows), 1)

    def test_download_methods(self):
        self.theme.download_progress()
        self.theme.download_start("m")
        self.theme.download_finished("m", "/path")
        self.theme.download_access_denied("m", "url")

    def test_memory_embeddings(self):
        data = np.arange(6, dtype=float)
        grp = self.theme.memory_embeddings(
            "text",
            data,
            total_tokens=1,
            minv=float(data.min()),
            maxv=float(data.max()),
            meanv=float(data.mean()),
            stdv=float(data.std()),
            normv=float(norm(data)),
        )
        self.assertTrue(grp.renderables)

        grp = self.theme.memory_embeddings(
            "text",
            data,
            total_tokens=1,
            minv=float(data.min()),
            maxv=float(data.max()),
            meanv=float(data.mean()),
            stdv=float(data.std()),
            normv=float(norm(data)),
            embedding_peek=2,
            show_stats=False,
        )
        self.assertTrue(grp.renderables)

    def test_memory_embeddings_comparison(self):
        sim = Similarity(
            cosine_distance=0.0,
            inner_product=0.0,
            l1_distance=0.0,
            l2_distance=0.0,
            pearson=0.0,
        )
        res = self.theme.memory_embeddings_comparison({"t": sim}, "t")
        self.assertEqual(len(res.renderable.rows), 1)

    def test_memory_embeddings_search(self):
        match = SearchMatch(query="q", match="m", l2_distance=0.1)
        res = self.theme.memory_embeddings_search([match])
        self.assertEqual(len(res.renderable.rows), 1)

    def test_memory_partitions(self):
        part = TextPartition(
            data="t", total_tokens=1, embeddings=np.array([1])
        )
        group = self.theme.memory_partitions([part] * 3, display_partitions=2)
        self.assertEqual(len(group.renderables), 3)

    def test_model(self):
        model = SimpleNamespace(
            id="m",
            parameters=1,
            parameter_types=["p"],
            inference=None,
            library_name=None,
            pipeline_tag=None,
            tags=[],
            architectures=None,
            model_type=None,
            license=None,
            gated=False,
            private=False,
            disabled=False,
            downloads=1,
            likes=2,
            ranking=1,
            author="a",
            created_at=datetime.now(),
            updated_at=datetime.now(),
        )
        panel = self.theme.model(model, can_access=True)
        self.assertIn("m", panel.title)

    def test_model_display(self):
        cfg = ModelConfig(
            architectures=["a"],
            attribute_map={},
            bos_token_id=None,
            bos_token=None,
            decoder_start_token_id=None,
            eos_token_id=None,
            eos_token=None,
            finetuning_task=None,
            hidden_size=1,
            hidden_sizes=None,
            keys_to_ignore_at_inference=[],
            loss_type=None,
            max_position_embeddings=10,
            model_type="t",
            num_attention_heads=1,
            num_hidden_layers=1,
            num_labels=1,
            output_attentions=False,
            output_hidden_states=False,
            pad_token_id=None,
            pad_token=None,
            prefix=None,
            sep_token_id=None,
            sep_token=None,
            state_size=1,
            task_specific_params=None,
            torch_dtype=float,
            vocab_size=10,
            tokenizer_class=None,
        )
        tok_cfg = TokenizerConfig(
            name_or_path="t",
            tokens=["a"],
            special_tokens=["b"],
            tokenizer_model_max_length=10,
            fast=True,
        )
        group = self.theme.model_display(cfg, tok_cfg)
        self.assertEqual(len(group.renderables), 2)

    def test_recent_messages(self):
        msg = EngineMessage(
            agent_id=UUID(int=0),
            model_id="m",
            message=Message(role="user", content="hi"),
        )
        agent = SimpleNamespace(name="n")
        group = self.theme.recent_messages(str(UUID(int=1)), agent, [msg])
        self.assertEqual(len(group.renderables), 1)

    def test_saved_tokenizer_files(self):
        pad = self.theme.saved_tokenizer_files("/d", 2)
        self.assertIn("2 tokenizer files", pad.renderable)

    def test_saved_tokenizer_files_with_total_only(self):
        pad = self.theme.saved_tokenizer_files(1)
        self.assertIn("1 tokenizer file", pad.renderable)

    def test_events_log_tool_branches(self):
        tool_call = SimpleNamespace(
            id=UUID(int=2), name="calc", arguments={"x": 1}
        )
        tool_result = SimpleNamespace(call=tool_call, result={"ok": True})
        diagnostic = ToolCallDiagnostic(
            id="diag-1",
            call_id="call-1",
            requested_name="weather",
            code=ToolCallDiagnosticCode.UNKNOWN_TOOL,
            stage=ToolCallDiagnosticStage.RESOLVE,
            message="Unknown tool.",
        )
        events = [
            Event(
                type=EventType.TOOL_EXECUTE,
                payload={"call": tool_call},
            ),
            Event(
                type=EventType.TOOL_MODEL_RUN,
                payload={"model_id": "react", "messages": ["a", "b"]},
            ),
            Event(
                type=EventType.TOOL_MODEL_RESPONSE,
                payload={"model_id": "react"},
            ),
            Event(
                type=EventType.TOOL_PROCESS,
                payload=[SimpleNamespace(name="weather")],
            ),
            Event(
                type=EventType.TOOL_RESULT,
                payload={"result": tool_result},
                elapsed=0.01,
            ),
            Event(
                type=EventType.TOOL_DIAGNOSTIC,
                payload={"diagnostic": diagnostic},
            ),
        ]

        log = self.theme._events_log(
            events,
            events_limit=None,
            include_tokens=False,
            include_tool_detect=False,
            include_tools=True,
            include_non_tools=False,
        )

        assert log is not None
        self.assertEqual(len(log), 6)
        self.assertIn("tool.unknown", log[-1])
        self.assertIn("Unknown tool.", log[-1])

    def test_events_log_tool_result_diagnostic_and_error(self):
        call = ToolCall(id="call-2", name="calc", arguments={"x": 1})
        diagnostic = ToolCallDiagnostic(
            id="diag-2",
            call_id=call.id,
            requested_name="calc",
            code=ToolCallDiagnosticCode.ARGUMENT_VALIDATION_FAILED,
            stage=ToolCallDiagnosticStage.VALIDATE,
            message="Invalid arguments.",
        )
        error = ToolCallError(
            id="error-1",
            call=call,
            name="calc",
            arguments={"x": 1},
            error=RuntimeError("secret"),
            message="Tool failed.",
        )
        events = [
            Event(
                type=EventType.TOOL_RESULT,
                payload={"call": call, "result": diagnostic},
            ),
            Event(
                type=EventType.TOOL_RESULT,
                payload={"result": error},
                elapsed=0.01,
            ),
            Event(
                type=EventType.TOOL_DIAGNOSTIC,
                payload={"diagnostics": ["bad"]},
            ),
        ]

        log = self.theme._events_log(
            events,
            events_limit=None,
            include_tokens=False,
            include_tool_detect=False,
            include_tools=True,
            include_non_tools=False,
        )

        assert log is not None
        self.assertIn("tool_call.arguments_invalid", log[0])
        self.assertIn("Tool call failed.", log[1])
        self.assertNotIn("secret", log[1])
        self.assertNotIn("Tool failed.", log[1])
        self.assertIn("bad", log[2])

    def test_tool_diagnostic_from_payload_variants(self):
        diagnostic = ToolCallDiagnostic(
            id="diag-payload",
            call_id="call-payload",
            requested_name="calc",
            code=ToolCallDiagnosticCode.UNKNOWN_TOOL,
            stage=ToolCallDiagnosticStage.RESOLVE,
            message="Unknown tool.",
        )

        self.assertIsNone(self.theme._tool_diagnostic_from_payload("bad"))
        self.assertIs(
            self.theme._tool_diagnostic_from_payload({"result": diagnostic}),
            diagnostic,
        )
        self.assertIsNone(self.theme._tool_diagnostic_from_payload({}))

    def test_search_message_matches(self):
        msg = EngineMessageScored(
            agent_id=UUID(int=0),
            model_id="m",
            message=Message(role="user", content="hi"),
            score=0.1,
        )
        agent = SimpleNamespace(name="n")
        group = self.theme.search_message_matches(
            str(UUID(int=1)), agent, [msg]
        )
        self.assertEqual(len(group.renderables), 1)

    def test_memory_search_matches(self):
        partition = PermanentMemoryPartition(
            participant_id=UUID(int=1),
            memory_id=UUID(int=0),
            partition=1,
            data="d",
            embedding=np.array([0.1]),
            created_at=datetime.now(),
        )
        group = self.theme.memory_search_matches(
            UUID(int=1), "ns", [partition]
        )
        self.assertEqual(len(group.renderables), 1)

    def test_tokenizer_config(self):
        cfg = TokenizerConfig(
            name_or_path="t",
            tokens=["a"],
            special_tokens=["b"],
            tokenizer_model_max_length=10,
            fast=True,
        )
        panel = self.theme.tokenizer_config(cfg)
        self.assertTrue(panel.renderable.rows)

    def test_tokenizer_tokens(self):
        t1 = Token(id=1, token="a")
        t2 = Token(id=2, token="b")
        panel = self.theme.tokenizer_tokens(
            [t1, t2], ["a"], ["s"], current_dtoken=t1
        )
        self.assertEqual(len(panel.renderable.renderable.renderables), 2)

    async def test_tokens_thinking(self):
        t = Token(id=1, token="a")
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=1,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=lambda x: True,
                thinking_text_tokens=["x\n"],
                tool_text_tokens=[],
                answer_text_tokens=["y"],
                tokens=[t],
                input_token_count=0,
                total_tokens=1,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
                maximum_frames=1,
            )
            frame = await gen.__anext__()
        self.assertTrue(frame[1].renderables)

    async def test_tokens_multiple_frames(self):
        alt1 = Token(id=2, token="b", probability=0.6)
        alt2 = Token(id=3, token="c", probability=0.4)
        dtoken = TokenDetail(
            id=1, token="a", probability=0.8, tokens=[alt1, alt2]
        )

        with (
            patch(
                "avalan.cli.theme.fancy._lf",
                lambda i: list(filter(None, i or [])),
            ),
            patch(
                "avalan.cli.theme.fancy._j",
                lambda sep, items: sep.join(str(x) for x in items if x),
            ),
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=1,
                display_probabilities=True,
                pick=2,
                focus_on_token_when=lambda x: True,
                thinking_text_tokens=["x\n"],
                tool_text_tokens=[],
                answer_text_tokens=["y"],
                tokens=[dtoken],
                input_token_count=0,
                total_tokens=1,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.1,
                ttnt=0.1,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
                maximum_frames=2,
            )
            frame1 = await gen.__anext__()
            frame2 = await gen.__anext__()
            with self.assertRaises(StopAsyncIteration):
                await gen.__anext__()

        self.assertTrue(frame1[1].renderables)
        self.assertTrue(frame2[1].renderables)

    async def test_tokens_early_return(self):
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=["x\n"],
                tokens=None,
                input_token_count=0,
                total_tokens=0,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
            )
            token, frame = await gen.__anext__()
            with self.assertRaises(StopAsyncIteration):
                await gen.__anext__()

        self.assertIsNone(token)
        self.assertTrue(frame.renderables)

    async def test_tokens_pick_first_full_batch(self):
        alt = Token(id=2, token="b", probability=0.5)
        dtoken = TokenDetail(id=1, token="a", probability=0.6, tokens=[alt])
        with (
            patch(
                "avalan.cli.theme.fancy._lf",
                lambda i: list(filter(None, i or [])),
            ),
            patch(
                "avalan.cli.theme.fancy._j",
                lambda sep, items: sep.join(str(x) for x in items if x),
            ),
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=1,
                display_probabilities=True,
                pick=1,
                focus_on_token_when=lambda x: True,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=["x\n"],
                tokens=[dtoken],
                input_token_count=0,
                total_tokens=1,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
                maximum_frames=1,
            )
            frame = await gen.__anext__()
        self.assertTrue(frame[1].renderables)

    async def test_tokens_wrap_long_lines(self):
        long1 = "Word " * 20
        long2 = "Another word " * 20
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[],
                tool_text_tokens=[],
                answer_text_tokens=[f"{long1}\n", long2],
                tokens=None,
                input_token_count=0,
                total_tokens=0,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
            )
            _, frame = await gen.__anext__()

        self.assertEqual(len(frame.renderables), 1)
        text = frame.renderables[0].renderable.renderable
        self.assertIn("Word Word", text)
        self.assertIn("Another word", text)
        self.assertGreaterEqual(text.count("\n"), 1)

    async def test_tokens_thinking_wrap_long_lines(self):
        think_line = "Reasoning " * 10
        answer_line = "Answer " * 10
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=[f"{think_line}\n", f"{think_line}\n"],
                tool_text_tokens=[],
                answer_text_tokens=[f"{answer_line}\n", answer_line],
                tokens=None,
                input_token_count=0,
                total_tokens=0,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
            )
            _, frame = await gen.__anext__()

        self.assertEqual(len(frame.renderables), 2)
        think_text = frame.renderables[0].renderable.renderable
        answer_text = frame.renderables[1].renderable.renderable
        self.assertIn("Reasoning", think_text)
        self.assertIn("Answer", answer_text)
        self.assertGreaterEqual(answer_text.count("\n"), 1)

    async def test_tokens_thinking_uses_full_height(self):
        lines = [f"line{i}\n" for i in range(4)]
        with patch(
            "avalan.cli.theme.fancy._lf", lambda i: list(filter(None, i or []))
        ):
            gen = self.theme.tokens(
                model_id="m",
                added_tokens=None,
                special_tokens=None,
                display_token_size=None,
                display_probabilities=False,
                pick=0,
                focus_on_token_when=None,
                thinking_text_tokens=lines,
                tool_text_tokens=[],
                answer_text_tokens=[],
                tokens=None,
                input_token_count=0,
                total_tokens=0,
                tool_events=None,
                tool_event_calls=None,
                tool_event_results=None,
                tool_running_spinner=None,
                ttft=0.0,
                ttnt=0.0,
                ttsr=0.0,
                elapsed=1.0,
                console_width=40,
                logger=MagicMock(),
            )
            _, frame = await gen.__anext__()

        self.assertEqual(len(frame.renderables), 1)
        think_text = frame.renderables[0].renderable.renderable
        self.assertIn("line0", think_text)
        self.assertGreaterEqual(think_text.count("\n"), 3)


class FancyThemeAdditionalTestCase(unittest.TestCase):
    def setUp(self) -> None:
        self.theme = FancyTheme(
            lambda s: s, lambda s, p, n: s if n == 1 else p
        )

    def test_bye(self):
        self.assertEqual(self.theme.bye(), ":vulcan_salute: bye :)")

    def test_action(self):
        panel = self.theme.action(
            "task",
            "desc",
            "author",
            "m",
            "lib",
            highlight=True,
            finished=True,
        )
        self.assertEqual(panel.box, box.DOUBLE)
        self.assertIn(
            "[green]desc[/green]", panel.renderable.renderable.renderables[0]
        )

    def test_cache_delete_deleted_true(self):
        deletion = HubCacheDeletion(
            model_id="m",
            revisions=["r"],
            deletable_size_on_disk=1,
            deletable_blobs=["b"],
            deletable_refs=[],
            deletable_repos=[],
            deletable_snapshots=[],
        )
        result = self.theme.cache_delete(deletion, deleted=True)
        self.assertTrue(result.renderables)

    def test_cache_list_display_models(self):
        cache = HubCache(
            model_id="m",
            path="/p",
            size_on_disk=1,
            revisions=["r"],
            files={"r": []},
            total_files=0,
            total_revisions=1,
        )
        group = self.theme.cache_list(
            "/c",
            [cache],
            display_models=["m"],
            show_summary=False,
        )
        self.assertEqual(len(group.renderables), 1)
        self.assertEqual(group.renderables[0].renderable.title, cache.model_id)

    def test_logging_in(self):
        self.assertEqual(
            self.theme.logging_in("hf"),
            "Logging in to hf...",
        )

    def test_memory_embeddings_orientation(self):
        data = np.arange(6, dtype=float)
        group = self.theme.memory_embeddings(
            "text",
            data,
            total_tokens=1,
            minv=float(data.min()),
            maxv=float(data.max()),
            meanv=float(data.mean()),
            stdv=float(data.std()),
            normv=float(norm(data)),
            embedding_peek=2,
            horizontal=False,
            show_stats=False,
        )
        table = group.renderables[0].renderable
        self.assertEqual(len(table.columns), 2)

        group = self.theme.memory_embeddings(
            "text",
            data,
            total_tokens=1,
            minv=float(data.min()),
            maxv=float(data.max()),
            meanv=float(data.mean()),
            stdv=float(data.std()),
            normv=float(norm(data)),
            embedding_peek=2,
            horizontal=True,
            show_stats=False,
        )
        table = group.renderables[0].renderable
        self.assertEqual(len(table.columns), 5)

    def test_model_display_sentence_transformer(self):
        cfg = ModelConfig(
            architectures=["a"],
            attribute_map={},
            bos_token_id=None,
            bos_token=None,
            decoder_start_token_id=None,
            eos_token_id=None,
            eos_token=None,
            finetuning_task=None,
            hidden_size=1,
            hidden_sizes=None,
            keys_to_ignore_at_inference=[],
            loss_type=None,
            max_position_embeddings=10,
            model_type="t",
            num_attention_heads=1,
            num_hidden_layers=1,
            num_labels=1,
            output_attentions=False,
            output_hidden_states=False,
            pad_token_id=None,
            pad_token=None,
            prefix=None,
            sep_token_id=None,
            sep_token=None,
            state_size=1,
            task_specific_params=None,
            torch_dtype=float,
            vocab_size=10,
            tokenizer_class=None,
        )
        st_cfg = SentenceTransformerModelConfig(
            backend="torch",
            similarity_function="cosine",
            truncate_dimension=None,
            transformer_model_config=cfg,
        )
        tok_cfg = TokenizerConfig(
            name_or_path="t",
            tokens=["a"],
            special_tokens=["b"],
            tokenizer_model_max_length=10,
            fast=True,
        )
        group = self.theme.model_display(st_cfg, tok_cfg)
        self.assertEqual(len(group.renderables), 2)

    def test_welcome(self):
        user = User(name="u", access_token_name="tok")
        pad = self.theme.welcome("http://u", "avalan", "1.0", "MIT", user)
        text = str(pad.renderable.renderable)
        self.assertIn("avalan", text)
        self.assertIn("1.0", text)
        self.assertIn("tok", text)


class FancyThemeMoreTests(unittest.TestCase):
    def setUp(self) -> None:
        self.theme = FancyTheme(
            lambda s: s, lambda s, p, n: s if n == 1 else p
        )

    def test_cache_delete_none(self):
        result = self.theme.cache_delete(None)
        self.assertIsInstance(result, Text)
        self.assertIn("Nothing found", result.plain)

    def test_cache_list_multiple_revision_files(self):
        now = datetime.now()
        file1 = HubCacheFile(
            name="f1",
            path="/f1",
            size_on_disk=1,
            last_accessed=now,
            last_modified=now,
        )
        file2 = HubCacheFile(
            name="f2",
            path="/f2",
            size_on_disk=1,
            last_accessed=now,
            last_modified=now,
        )
        cache = HubCache(
            model_id="m",
            path="/p",
            size_on_disk=2,
            revisions=["r1", "r2"],
            files={"r1": [file1, file2], "r2": []},
            total_files=2,
            total_revisions=2,
        )
        group = self.theme.cache_list(
            "/c",
            [cache],
            display_models=["m"],
            show_summary=False,
        )
        table = group.renderables[0].renderable
        self.assertEqual(table.row_count, 2)
        self.assertIn("[bright_black]", table.columns[0]._cells[1])

    def test_memory_partitions_many(self):
        part = TextPartition(
            data="t", total_tokens=1, embeddings=np.array([1])
        )
        group = self.theme.memory_partitions([part] * 5, display_partitions=3)
        self.assertEqual(len(group.renderables), 4)

    def test_sentence_transformer_model_config(self):
        cfg = ModelConfig(
            architectures=["a"],
            attribute_map={},
            bos_token_id=1,
            bos_token="<s>",
            decoder_start_token_id=None,
            eos_token_id=2,
            eos_token="</s>",
            finetuning_task=None,
            hidden_size=1,
            hidden_sizes=None,
            keys_to_ignore_at_inference=[],
            loss_type="ce",
            max_position_embeddings=10,
            model_type="t",
            num_attention_heads=1,
            num_hidden_layers=1,
            num_labels=1,
            output_attentions=False,
            output_hidden_states=False,
            pad_token_id=0,
            pad_token="<pad>",
            prefix="pre",
            sep_token_id=3,
            sep_token="<sep>",
            state_size=1,
            task_specific_params=None,
            torch_dtype=float,
            vocab_size=10,
            tokenizer_class=None,
        )
        st_cfg = SentenceTransformerModelConfig(
            backend="torch",
            similarity_function="cosine",
            truncate_dimension=128,
            transformer_model_config=cfg,
        )
        align = self.theme._sentence_transformer_model_config(
            st_cfg, is_runnable=True, summary=False
        )
        table = align.renderable
        headers = table.columns[0]._cells
        self.assertIn("Truncate dimension", headers)

    def test_display_image_entities(self):
        align = self.theme.display_image_entities(
            [
                ImageEntity(label="cat", score=0.5, box=[0.0, 1.0, 2.0, 3.0]),
                ImageEntity(label="dog", score=0.9, box=None),
            ],
            sort=True,
        )
        table = align.renderable
        self.assertEqual(table.row_count, 2)
        # dog should be first due to higher score
        self.assertEqual(table.columns[0]._cells[0], "dog")
        self.assertEqual(table.columns[0]._cells[1], "cat")
        self.assertEqual(table.columns[1]._cells[0], "[score]0.90[/score]")
        self.assertEqual(table.columns[1]._cells[1], "[score]0.50[/score]")

    def test_display_image_entity(self):
        align = self.theme.display_image_entity(ImageEntity(label="cat"))
        table = align.renderable
        self.assertEqual(table.row_count, 1)
        self.assertEqual(table.columns[0]._cells[0], "cat")

    def test_display_image_labels(self):
        align = self.theme.display_image_labels(["cat", "dog"])
        table = align.renderable
        self.assertEqual(table.row_count, 2)
        self.assertEqual(table.columns[0]._cells[0], "cat")
        self.assertEqual(table.columns[0]._cells[1], "dog")

    def test_display_audio_labels(self):
        align = self.theme.display_audio_labels({"dog": 0.9, "cat": 0.5})
        table = align.renderable
        self.assertEqual(table.row_count, 2)
        self.assertEqual(table.columns[0]._cells[0], "dog")
        self.assertEqual(table.columns[0]._cells[1], "cat")
        self.assertEqual(table.columns[1]._cells[0], "[score]0.90[/score]")
        self.assertEqual(table.columns[1]._cells[1], "[score]0.50[/score]")

    def test_display_token_labels(self):
        align = self.theme.display_token_labels([{"tok": "LBL"}])
        table = align.renderable
        self.assertEqual(table.row_count, 1)
        self.assertEqual(table.columns[0]._cells[0], "tok")
        self.assertEqual(table.columns[1]._cells[0], "LBL")

    def test_fill_model_config_table(self):
        cfg = ModelConfig(
            architectures=["a"],
            attribute_map={},
            bos_token_id=1,
            bos_token="<s>",
            decoder_start_token_id=None,
            eos_token_id=2,
            eos_token="</s>",
            finetuning_task=None,
            hidden_size=1,
            hidden_sizes=None,
            keys_to_ignore_at_inference=[],
            loss_type="ce",
            max_position_embeddings=10,
            model_type="t",
            num_attention_heads=1,
            num_hidden_layers=1,
            num_labels=1,
            output_attentions=False,
            output_hidden_states=False,
            pad_token_id=0,
            pad_token="<pad>",
            prefix="pre",
            sep_token_id=3,
            sep_token="<sep>",
            state_size=1,
            task_specific_params=None,
            torch_dtype=float,
            vocab_size=10,
            tokenizer_class=None,
        )
        table = Table(show_lines=True)
        table.add_column()
        table.add_column()
        filled = self.theme._fill_model_config_table(
            cfg, table, is_runnable=True, summary=False
        )
        cells = filled.columns[0]._cells
        self.assertIn("Runs on this instance", cells)
        self.assertIn("Architectures", cells)
        self.assertIn("Start of stream token", cells)

    def test_fill_model_config_table_pad_token(self):
        cfg = ModelConfig(
            architectures=["a"],
            attribute_map={},
            bos_token_id=1,
            bos_token="<s>",
            decoder_start_token_id=None,
            eos_token_id=2,
            eos_token="</s>",
            finetuning_task=None,
            hidden_size=1,
            hidden_sizes=None,
            keys_to_ignore_at_inference=[],
            loss_type="ce",
            max_position_embeddings=10,
            model_type="t",
            num_attention_heads=1,
            num_hidden_layers=1,
            num_labels=1,
            output_attentions=False,
            output_hidden_states=False,
            pad_token_id=4,
            pad_token="<pad>",
            prefix=None,
            sep_token_id=None,
            sep_token=None,
            state_size=1,
            task_specific_params=None,
            torch_dtype=float,
            vocab_size=10,
            tokenizer_class=None,
        )
        table = Table(show_lines=True)
        table.add_column()
        table.add_column()
        filled = self.theme._fill_model_config_table(
            cfg, table, is_runnable=True, summary=False
        )
        cells = filled.columns[0]._cells
        self.assertIn("Padding token", cells)

    def test_tokenizer_config_tokens(self):
        cfg = TokenizerConfig(
            name_or_path="t",
            tokens=["a"],
            special_tokens=["b"],
            tokenizer_model_max_length=10,
            fast=True,
        )
        panel = self.theme.tokenizer_config(cfg)
        headers = panel.renderable.columns[0]._cells
        self.assertIn("Added tokens", headers)

    def test_tokens_table_multiple(self):
        t1 = Token(id=1, token="a", probability=0.1)
        t2 = Token(id=2, token="b", probability=0.2)
        table = self.theme._tokens_table([t1, t2], t1, t2)
        self.assertEqual(table.row_count, 2)
        self.assertIn("[cyan]", table.columns[1]._cells[1])

    def test_parameter_count_none(self):
        self.assertEqual(self.theme._parameter_count(None), "N/A")

    def test_symmetric_indices(self):
        self.assertEqual(
            FancyTheme._symmetric_indices([0.1, 0.5, 0.2, 0.4]),
            [2, 0, 1, 3],
        )

    def test_percentage(self):
        self.assertEqual(FancyTheme._percentage(0.5), "50%")
        self.assertEqual(FancyTheme._percentage(0.123), "12.3%")
        self.assertEqual(FancyTheme._percentage(1), "100%")


class FancyThemeWrapLinesTestCase(unittest.TestCase):
    def setUp(self) -> None:
        self.theme = FancyTheme(
            lambda s: s, lambda s, p, n: s if n == 1 else p
        )

    def test_parameter_count_values(self):
        self.assertEqual(self.theme._parameter_count(10_000), "10.0 thousand")
        self.assertEqual(self.theme._parameter_count(2_000_000_000), "2.0B")

    def test_wrap_lines_blank_lines(self):
        result = FancyTheme._wrap_lines(["a\n\nb"], width=10)
        self.assertEqual(result, ["a", "", "b"])

    def test_wrap_lines_skip_blank_lines(self):
        result = FancyTheme._wrap_lines(
            ["a\n\nb"], width=10, skip_blank_lines=True
        )
        self.assertEqual(result, ["a", "b"])


class FancyThemeEventsTestCase(unittest.TestCase):
    def setUp(self) -> None:
        self.theme = FancyTheme(
            lambda s: s, lambda s, p, n: s if n == 1 else p
        )

    def test_no_events(self):
        self.assertIsNone(self.theme.events([]))
        self.assertIsNone(self.theme.events([], events_limit=0))

    def test_single_event(self):
        event = Event(type=EventType.START)
        panel = self.theme.events([event])
        self.assertEqual(panel.height, 4)
        self.assertIn("<start>", str(panel.renderable))

        panel = self.theme.events([event], events_limit=1)
        self.assertEqual(panel.height, 3)
        self.assertIn("<start>", str(panel.renderable))

    def test_multiple_events_with_limit(self):
        e1 = Event(type=EventType.START)
        e2 = Event(type=EventType.END)

        panel = self.theme.events([e1, e2])
        text = str(panel.renderable)
        self.assertEqual(panel.height, 4)
        self.assertIn("<start>", text)
        self.assertIn("<end>", text)

        panel = self.theme.events([e1, e2], events_limit=1)
        text = str(panel.renderable)
        self.assertEqual(panel.height, 3)
        self.assertNotIn("<start>", text)
        self.assertIn("<end>", text)

        panel = self.theme.events([e1, e2], events_limit=2)
        text = str(panel.renderable)
        self.assertEqual(panel.height, 4)
        self.assertIn("<start>", text)
        self.assertIn("<end>", text)
