use super::*;
use crate::app::generation;
use iced::widget::column;

impl App {
    pub(super) fn stats_dashboard(&self) -> Element<'_, Message> {
        let stats = &self.metrics_snapshot;
        let session = SessionStats::from_messages(&self.conversation, self.config.model);
        let context_fraction = if stats.context_limit == 0 {
            0.0
        } else {
            stats.context_used.min(stats.context_limit) as f32 / stats.context_limit as f32
        };
        let cache_fraction = if stats.last_prompt_tokens == 0 {
            0.0
        } else {
            stats.last_cached_tokens as f32 / stats.last_prompt_tokens as f32
        };
        let cache_hit_fraction = if stats.kv_lookups == 0 {
            0.0
        } else {
            stats.kv_hits as f32 / stats.kv_lookups as f32
        };
        let endpoint = if stats.server_listening {
            format!("Listening · 127.0.0.1:{}", stats.server_port)
        } else {
            "Stopped".to_owned()
        };
        let phase_color = match stats.phase {
            crate::metrics::RuntimePhase::Generating => Color::from_rgb8(84, 170, 255),
            crate::metrics::RuntimePhase::Prefilling | crate::metrics::RuntimePhase::Loading => {
                Color::from_rgb8(240, 180, 70)
            }
            crate::metrics::RuntimePhase::Ready => Color::from_rgb8(72, 176, 112),
            crate::metrics::RuntimePhase::Failed => Color::from_rgb8(220, 80, 86),
            crate::metrics::RuntimePhase::Unloaded => muted_text(),
        };
        let heading = container(
            row![
                column![
                    text("Runtime observability").size(24),
                    text(format!(
                        "{} · {} · uptime {}",
                        stats.model,
                        stats.source.label(),
                        format_duration(stats.uptime_seconds as f64)
                    ))
                    .size(12)
                    .color(muted_text()),
                ]
                .spacing(5),
                Space::new().width(Length::Fill),
                container(text(stats.phase.label()).size(12).color(phase_color))
                    .padding([7, 11])
                    .style(move |_| status_badge_style(phase_color)),
            ]
            .align_y(Alignment::Center),
        )
        .padding(16)
        .style(overview_style);

        let headline = column![
            row![
                metric_card(
                    "DECODE",
                    format!("{:.1} tok/s", stats.decode_tokens_per_second),
                    format!(
                        "{} completion tokens total",
                        format_count(stats.completion_tokens)
                    ),
                ),
                metric_card(
                    "PREFILL",
                    format!("{:.1} tok/s", stats.prefill_tokens_per_second),
                    format!("{} prompt tokens total", format_count(stats.prompt_tokens)),
                ),
            ]
            .spacing(10),
            row![
                metric_card(
                    "CONTEXT",
                    format!(
                        "{} / {}",
                        format_count(u64::from(stats.context_used)),
                        format_count(u64::from(stats.context_limit))
                    ),
                    format!("{:.0}% occupied", context_fraction * 100.0),
                ),
                metric_card(
                    "WORK",
                    format!(
                        "{} active · {} queued",
                        stats.http_active, stats.queue_depth
                    ),
                    format!("{} runtime requests", format_count(stats.runtime_requests)),
                ),
            ]
            .spacing(10),
        ]
        .spacing(10);

        let session_title = self
            .selected_session
            .and_then(|id| self.session_title(id))
            .unwrap_or_else(|| "No session selected".to_owned());
        let session_cache_fraction = if session.input_tokens == 0 {
            0.0
        } else {
            session.cached_tokens as f64 / session.input_tokens as f64
        };
        let current_session = stats_panel(
            "CURRENT SESSION",
            column![
                metric_row("Session", session_title),
                metric_row(
                    "Messages",
                    format!(
                        "{} · {} user · {} assistant · {} tool",
                        format_count(session.messages),
                        format_count(session.user_messages),
                        format_count(session.assistant_messages),
                        format_count(session.tool_messages),
                    ),
                ),
                metric_row("Tool calls", format_count(session.tool_calls)),
                metric_row("Context rebuilds", format_count(session.context_rebuilds)),
                metric_row("Completed turns", format_count(session.completed_turns),),
                metric_row(
                    "Generation time",
                    format_duration(session.duration_ms as f64 / 1_000.0),
                ),
                metric_row("Input tokens", format_count(session.input_tokens)),
                metric_row(
                    "Cached tokens",
                    format!(
                        "{} · {:.1}% reused",
                        format_count(session.cached_tokens),
                        session_cache_fraction * 100.0,
                    ),
                ),
                metric_row("Output tokens", format_count(session.output_tokens)),
            ]
            .spacing(9)
            .into(),
        );

        let throughput = stats_panel(
            "MODEL ACTIVITY · LAST 24 SECONDS",
            column![
                mini_chart(
                    &self.metrics_history,
                    |point| point.decode_tokens_per_second,
                    Color::from_rgb8(84, 170, 255),
                ),
                row![
                    text("Decode")
                        .size(12)
                        .color(Color::from_rgb8(84, 170, 255)),
                    Space::new().width(Length::Fill),
                    text(format!("{:.1} tok/s", stats.decode_tokens_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
                mini_chart(
                    &self.metrics_history,
                    |point| point.prefill_tokens_per_second,
                    Color::from_rgb8(157, 119, 255),
                ),
                row![
                    text("Prefill")
                        .size(12)
                        .color(Color::from_rgb8(157, 119, 255)),
                    Space::new().width(Length::Fill),
                    text(format!("{:.1} tok/s", stats.prefill_tokens_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
            ]
            .spacing(7)
            .into(),
        );
        let requests = stats_panel(
            "SERVER REQUEST RATE · LAST 24 SECONDS",
            column![
                mini_chart(
                    &self.metrics_history,
                    |point| point.http_requests_per_second,
                    Color::from_rgb8(72, 176, 112),
                ),
                row![
                    text(format!("{} requests", format_count(stats.http_requests))).size(12),
                    Space::new().width(Length::Fill),
                    text(format!(
                        "{} errors · {} streaming",
                        stats.http_errors, stats.http_streaming_requests
                    ))
                    .size(12)
                    .color(muted_text()),
                ],
            ]
            .spacing(7)
            .into(),
        );
        let latest = self.metrics_history.back().copied().unwrap_or_default();
        let kv_io = stats_panel(
            "KV CHECKPOINT I/O · LAST 24 SECONDS",
            column![
                mini_chart(
                    &self.metrics_history,
                    |point| point.kv_read_bytes_per_second,
                    Color::from_rgb8(67, 194, 203),
                ),
                row![
                    text(if stats.kv_read_active {
                        "Disk read · active"
                    } else {
                        "Disk read"
                    })
                    .size(12)
                    .color(Color::from_rgb8(67, 194, 203)),
                    Space::new().width(Length::Fill),
                    text(format_rate(latest.kv_read_bytes_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
                mini_chart(
                    &self.metrics_history,
                    |point| point.kv_write_bytes_per_second,
                    Color::from_rgb8(240, 180, 70),
                ),
                row![
                    text(if stats.kv_write_active {
                        "Disk write · active"
                    } else {
                        "Disk write"
                    })
                    .size(12)
                    .color(Color::from_rgb8(240, 180, 70)),
                    Space::new().width(Length::Fill),
                    text(format_rate(latest.kv_write_bytes_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
            ]
            .spacing(7)
            .into(),
        );
        let ssd_activity = stats_panel(
            "SSD STREAMING ACTIVITY · LAST 24 SECONDS",
            column![
                mini_chart(
                    &self.metrics_history,
                    |point| point.ssd_requests_per_second,
                    Color::from_rgb8(240, 180, 70),
                ),
                row![
                    text("Selected loads")
                        .size(12)
                        .color(Color::from_rgb8(240, 180, 70)),
                    Space::new().width(Length::Fill),
                    text(format!("{:.1}/s", latest.ssd_requests_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
                mini_chart(
                    &self.metrics_history,
                    |point| point.ssd_bytes_per_second,
                    Color::from_rgb8(67, 194, 203),
                ),
                row![
                    text("Requested expert data")
                        .size(12)
                        .color(Color::from_rgb8(67, 194, 203)),
                    Space::new().width(Length::Fill),
                    text(format_rate(latest.ssd_bytes_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
                mini_chart(
                    &self.metrics_history,
                    |point| point.ssd_wait_ms_per_second,
                    Color::from_rgb8(220, 80, 86),
                ),
                row![
                    text("Inference wait")
                        .size(12)
                        .color(Color::from_rgb8(220, 80, 86)),
                    Space::new().width(Length::Fill),
                    text(format!("{:.0} ms/s", latest.ssd_wait_ms_per_second))
                        .size(12)
                        .color(muted_text()),
                ],
            ]
            .spacing(7)
            .into(),
        );

        let model = stats_panel(
            "MODEL CORE",
            column![
                metric_row("State", stats.phase.label()),
                metric_row("Loaded model", stats.model),
                metric_row("Mapped weights", format_bytes(stats.model_bytes)),
                metric_row("Tensors", format_count(stats.tensor_count)),
                metric_row("Vocabulary", format_count(stats.vocabulary_size)),
                metric_row("Last load", format_milliseconds(stats.model_load_ms)),
                metric_row(
                    "Lifecycle",
                    format!(
                        "{} loads · {} unloads",
                        stats.model_loads, stats.model_unloads
                    ),
                ),
            ]
            .spacing(9)
            .into(),
        );
        let runtime = stats_panel(
            "GENERATION",
            column![
                metric_row("Last runtime", format_milliseconds(stats.last_runtime_ms)),
                metric_row(
                    "Average runtime",
                    format_milliseconds(stats.average_runtime_ms)
                ),
                metric_row("Last prompt", format_count(stats.last_prompt_tokens)),
                metric_row("Last reused", format_count(stats.last_cached_tokens)),
                metric_row(
                    "Last completion",
                    format_count(stats.last_completion_tokens)
                ),
                metric_row("Cached tokens total", format_count(stats.cached_tokens)),
                metric_row(
                    "Cache reuse",
                    format!("{:.0}% of last prompt", cache_fraction * 100.0),
                ),
                metric_row(
                    "Results",
                    format!(
                        "{} completed · {} failed",
                        stats.completed_requests, stats.failed_requests
                    ),
                ),
            ]
            .spacing(9)
            .into(),
        );
        let acceptance = if stats.drafted_tokens == 0 {
            0.0
        } else {
            stats.accepted_draft_tokens as f64 / stats.drafted_tokens as f64
        };
        let target_passes = stats
            .speculative_cycles
            .saturating_add(stats.verifier_passes);
        let effective_speedup = if target_passes == 0 {
            1.0
        } else {
            stats
                .speculative_cycles
                .saturating_add(stats.accepted_draft_tokens) as f64
                / target_passes as f64
        };
        let speculative = stats_panel(
            "SPECULATIVE DECODING",
            column![
                metric_row(
                    "Mode",
                    match stats.speculative_mode {
                        1 => "Legacy MTP",
                        2 => "DSpark",
                        3 => "GLM MTP",
                        _ => "Off",
                    },
                ),
                metric_row("Cycles", format_count(stats.speculative_cycles)),
                metric_row("Drafted", format_count(stats.drafted_tokens)),
                metric_row("Accepted", format_count(stats.accepted_draft_tokens)),
                metric_row("Acceptance", format!("{:.1}%", acceptance * 100.0)),
                metric_row(
                    "Target verifier passes",
                    format_count(stats.verifier_passes)
                ),
                metric_row("Verifier wall time", format_milliseconds(stats.verifier_ms)),
                metric_row(
                    "Effective target-pass speedup",
                    format!("{effective_speedup:.2}×")
                ),
            ]
            .spacing(9)
            .into(),
        );
        let ssd = stats_panel(
            "SSD EXPERT STREAMING",
            column![
                metric_row("State", if stats.ssd_enabled { "Enabled" } else { "Off" }),
                metric_row("Resident weights", format_bytes(stats.ssd_resident_bytes)),
                metric_row("Expert cache", format_bytes(stats.ssd_cache_bytes)),
                metric_row(
                    "Cache capacity",
                    format!(
                        "{} / {} experts",
                        stats.ssd_cache_entries, stats.ssd_cache_experts
                    )
                ),
                metric_row(
                    "Preloaded",
                    format!(
                        "{} / {} experts",
                        stats.ssd_cache_entries.min(stats.ssd_preloaded_experts),
                        stats.ssd_preloaded_experts
                    )
                ),
                metric_row(
                    "Cache hits / misses",
                    format!("{} / {}", stats.ssd_cache_hits, stats.ssd_cache_misses)
                ),
                metric_row(
                    "Hit rate",
                    format!(
                        "{:.1}%",
                        if stats.ssd_cache_hits + stats.ssd_cache_misses == 0 {
                            0.0
                        } else {
                            100.0 * stats.ssd_cache_hits as f64
                                / (stats.ssd_cache_hits + stats.ssd_cache_misses) as f64
                        }
                    )
                ),
                metric_row(
                    "Evictions / wraps",
                    format!("{} / {}", stats.ssd_cache_evictions, stats.ssd_cache_wraps)
                ),
                metric_row(
                    "Buffers allocated / reused",
                    format!("{} / {}", stats.ssd_buffer_allocs, stats.ssd_buffer_reuses)
                ),
                metric_row(
                    "Direct SSD reads",
                    format!(
                        "{} · {}",
                        format_bytes(stats.ssd_pread_bytes),
                        format_milliseconds(stats.ssd_pread_ms)
                    )
                ),
                metric_row(
                    "VM advice",
                    format!(
                        "{} evicted · {} prefetched",
                        format_bytes(stats.ssd_evict_advise_bytes),
                        format_bytes(stats.ssd_willneed_advise_bytes)
                    )
                ),
                metric_row(
                    "Selected-load requests",
                    format_count(stats.ssd_selected_requests)
                ),
                metric_row(
                    "Requested expert bytes",
                    format_bytes(stats.ssd_requested_bytes)
                ),
                metric_row("Selected-load wait", format_milliseconds(stats.ssd_wait_ms)),
                metric_row(
                    "Average load wait",
                    format_milliseconds(
                        stats
                            .ssd_wait_ms
                            .checked_div(stats.ssd_selected_requests)
                            .unwrap_or(0)
                    )
                ),
            ]
            .spacing(9)
            .into(),
        );
        let cache = stats_panel(
            "KV CACHE",
            column![
                metric_row(
                    "Total",
                    format!(
                        "{} · {} files",
                        format_bytes(stats.kv_bytes),
                        stats.kv_files
                    )
                ),
                metric_row(
                    "Local sessions",
                    format!(
                        "{} · {} files",
                        format_bytes(stats.local_kv_bytes),
                        stats.local_kv_files
                    ),
                ),
                metric_row(
                    "HTTP transient",
                    format!(
                        "{} · {} files",
                        format_bytes(stats.http_kv_bytes),
                        stats.http_kv_files
                    ),
                ),
                metric_row("Checkpoint writes", format_count(stats.checkpoint_writes)),
                metric_row(
                    "Exact hits",
                    format!(
                        "{} · {} memory / {} disk",
                        stats.kv_hits, stats.kv_memory_hits, stats.kv_disk_hits
                    ),
                ),
                metric_row(
                    "Misses",
                    format!("{} · {} invalid", stats.kv_misses, stats.kv_invalid),
                ),
                metric_row("Lookups", format_count(stats.kv_lookups)),
                metric_row(
                    "Exact hit rate",
                    format!("{:.1}%", cache_hit_fraction * 100.0)
                ),
                metric_row("Prefix hits", format_count(stats.kv_prefix_hits)),
                metric_row(
                    "Reads",
                    format!(
                        "{} · {} · {} errors · last {}",
                        stats.kv_read_operations,
                        format_bytes(stats.kv_read_bytes),
                        stats.kv_read_errors,
                        format_milliseconds(stats.last_kv_read_ms),
                    ),
                ),
                metric_row(
                    "Writes",
                    format!(
                        "{} · {} · {} errors · last {}",
                        stats.kv_write_operations,
                        format_bytes(stats.kv_write_bytes),
                        stats.kv_write_errors,
                        format_milliseconds(stats.last_kv_write_ms),
                    ),
                ),
                progress_bar(0.0..=1.0, cache_fraction.min(1.0)).girth(4),
            ]
            .spacing(9)
            .into(),
        );
        let disc = stats_panel("KV CACHE DISC USAGE", self.cache_explorer());
        let server = stats_panel(
            "LOCAL SERVER",
            column![
                metric_row("Endpoint", endpoint),
                metric_row("Active", format_count(u64::from(stats.http_active))),
                metric_row("Completed", format_count(stats.http_completed)),
                metric_row("Chat completions", format_count(stats.http_chat_requests)),
                metric_row("Model queries", format_count(stats.http_model_requests)),
                metric_row(
                    "Runtime sources",
                    format!(
                        "{} local · {} HTTP",
                        stats.local_requests, stats.endpoint_generations
                    ),
                ),
                metric_row("Received", format_bytes(stats.http_bytes_received)),
                metric_row("Last latency", format_milliseconds(stats.last_http_ms)),
                metric_row(
                    "Average latency",
                    format_milliseconds(stats.average_http_ms)
                ),
            ]
            .spacing(9)
            .into(),
        );

        scrollable(
            container(
                column![
                    heading,
                    headline,
                    current_session,
                    throughput,
                    ssd_activity,
                    kv_io,
                    requests,
                    disc,
                    row![model, runtime].spacing(10),
                    row![speculative, ssd].spacing(10),
                    row![cache, server].spacing(10),
                    text("Counters are published by the runtime with relaxed atomics and sampled by the UI every 200 ms.")
                        .size(11)
                        .color(muted_text()),
                ]
                .spacing(12),
            )
            .padding(24)
            .max_width(960)
            .center_x(Length::Fill),
        )
        .height(Length::Fill)
        .into()
    }

    /// Disc usage of both cache buckets. The bar splits the transient store by
    /// age — the order the budget evicts in — and carries the session
    /// checkpoints as one further segment, since those are only freed with
    /// their session and so compete for disc without ever ageing out.
    fn cache_explorer(&self) -> Element<'_, Message> {
        let usage = &self.kv_cache_report;
        let total = usage.total_bytes();
        let capacity = total.max(1);
        let segments = crate::metrics::CACHE_AGE_BUCKETS
            .iter()
            .enumerate()
            .map(|(index, (label, _))| (*label, usage.age_bytes[index], AGE_COLORS[index]))
            .chain([("Sessions", usage.session_bytes, SESSION_COLOR)]);
        let mut bar = row![].height(14).spacing(2);
        let mut legend = column![].spacing(6);
        for (label, bytes, color) in segments {
            if bytes == 0 {
                continue;
            }
            bar = bar.push(
                container(Space::new().width(Length::Fill).height(Length::Fill))
                    .width(Length::FillPortion(portion(bytes, capacity)))
                    .style(move |_| chart_bar_style(color)),
            );
            legend = legend.push(
                row![
                    container(Space::new().width(9).height(9))
                        .style(move |_| chart_bar_style(color)),
                    text(label).size(12).color(muted_text()),
                    Space::new().width(Length::Fill),
                    text(format!(
                        "{} · {:.0}%",
                        format_bytes(bytes),
                        bytes as f32 / total.max(1) as f32 * 100.0
                    ))
                    .size(12),
                ]
                .spacing(8)
                .align_y(Alignment::Center),
            );
        }
        if total == 0 {
            bar = bar.push(
                container(Space::new().width(Length::Fill).height(Length::Fill))
                    .style(|_| chart_bar_style(muted_text().scale_alpha(0.25))),
            );
        }

        let mut rows = column![].spacing(7);
        for entry in &usage.entries {
            let path = entry.path.clone();
            let label = entry
                .session
                .and_then(|id| self.session_title(id))
                .map_or_else(
                    || entry.name.clone(),
                    |title| format!("{}: {}", entry.name, shorten(&title)),
                );
            rows = rows.push(
                row![
                    text(label).size(12),
                    Space::new().width(Length::Fill),
                    text(format!(
                        "{} · {} old",
                        format_bytes(entry.bytes),
                        format_duration(entry.age_seconds as f64)
                    ))
                    .size(12)
                    .color(muted_text()),
                    action_button(text("Discard").size(12))
                        .on_press(Message::DiscardCacheEntry(path)),
                ]
                .spacing(10)
                .align_y(Alignment::Center),
            );
        }

        column![
            metric_row(
                "Stored",
                format!(
                    "{} · {} files",
                    format_bytes(total),
                    usage.session_files + usage.transient_files
                ),
            ),
            container(bar).height(14).width(Length::Fill),
            legend,
            metric_row(
                "Sessions",
                format!(
                    "{} · {} files · not evicted",
                    format_bytes(usage.session_bytes),
                    usage.session_files
                ),
            ),
            metric_row(
                "Transient",
                format!(
                    "{} of {} budget · {} files · {:.0}% full",
                    format_bytes(usage.transient_bytes),
                    format_bytes(usage.budget_bytes),
                    usage.transient_files,
                    usage.transient_bytes as f32 / usage.budget_bytes.max(1) as f32 * 100.0,
                ),
            ),
            Space::new().height(2),
            text(if usage.entries.is_empty() {
                "No checkpoints stored yet."
            } else {
                "OLDEST CHECKPOINTS"
            })
            .size(10)
            .color(muted_text()),
            rows,
            row![
                text("Discarding a checkpoint costs one prefill to rebuild it.")
                    .size(11)
                    .color(muted_text()),
                Space::new().width(Length::Fill),
                action_button(text("Clear transient cache").size(12))
                    .on_press(Message::ClearTransientCache),
            ]
            .spacing(10)
            .align_y(Alignment::Center),
        ]
        .spacing(9)
        .into()
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
struct SessionStats {
    messages: u64,
    user_messages: u64,
    assistant_messages: u64,
    tool_messages: u64,
    tool_calls: u64,
    context_rebuilds: u64,
    completed_turns: u64,
    duration_ms: u64,
    input_tokens: u64,
    cached_tokens: u64,
    output_tokens: u64,
}

impl SessionStats {
    fn from_messages(messages: &[generation::ChatMessage], model: ModelChoice) -> Self {
        let mut summary = Self::default();
        for message in messages {
            if let Some(stats) = message.generation_stats {
                summary.completed_turns = summary.completed_turns.saturating_add(1);
                summary.duration_ms = summary.duration_ms.saturating_add(stats.duration_ms);
                summary.input_tokens = summary
                    .input_tokens
                    .saturating_add(u64::from(stats.input_tokens));
                summary.cached_tokens = summary
                    .cached_tokens
                    .saturating_add(u64::from(stats.cached_tokens));
                summary.output_tokens = summary
                    .output_tokens
                    .saturating_add(u64::from(stats.output_tokens));
            }
            if message.compaction {
                summary.context_rebuilds = summary.context_rebuilds.saturating_add(1);
            } else if !message.system {
                summary.messages = summary.messages.saturating_add(1);
                if message.user {
                    summary.user_messages = summary.user_messages.saturating_add(1);
                } else if message.tool {
                    summary.tool_messages = summary.tool_messages.saturating_add(1);
                } else {
                    summary.assistant_messages = summary.assistant_messages.saturating_add(1);
                    let calls = crate::agent::parse_tool_calls(model, &message.content)
                        .map(|(_, calls)| calls.len() as u64)
                        .unwrap_or(0);
                    summary.tool_calls = summary.tool_calls.saturating_add(calls);
                }
            }
        }
        summary
    }
}

/// Sessions sit outside the age scale: they are not evicted, only deleted.
const SESSION_COLOR: Color = Color::from_rgb(0.85, 0.44, 0.56);

/// Age buckets from green (fresh) to grey (past every hit half-life).
const AGE_COLORS: [Color; 5] = [
    Color::from_rgb(0.28, 0.69, 0.44),
    Color::from_rgb(0.33, 0.67, 1.0),
    Color::from_rgb(0.62, 0.47, 1.0),
    Color::from_rgb(0.94, 0.71, 0.27),
    Color::from_rgb(0.45, 0.45, 0.48),
];

/// Keeps a long session title from crowding out the size and the action.
fn shorten(title: &str) -> String {
    const LIMIT: usize = 44;
    if title.chars().count() <= LIMIT {
        return title.to_owned();
    }
    title.chars().take(LIMIT - 1).chain(['…']).collect()
}

fn portion(bytes: u64, capacity: u64) -> u16 {
    ((bytes.saturating_mul(1000) / capacity.max(1)) as u16).max(1)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn message(user: bool, tool: bool, system: bool, content: &str) -> generation::ChatMessage {
        generation::ChatMessage {
            id: 1,
            user,
            tool,
            system,
            compaction: false,
            compaction_tail_start: None,
            generation_stats: None,
            reasoning: None,
            reasoning_complete: true,
            reasoning_open: false,
            content: content.to_owned(),
            model_content: None,
            tool_approval_reasons: Vec::new(),
            instruction_metadata: None,
            markdown: markdown::Content::new(),
            transcript: text_editor::Content::new(),
            a2ui_lines_processed: 0,
            a2ui_errors: Vec::new(),
            a2ui_replies: Vec::new(),
            a2ui_open_urls: Vec::new(),
        }
    }

    #[test]
    fn current_session_summary_uses_persisted_chat_facts() {
        let mut user = message(true, false, false, "Run it");
        user.generation_stats = Some(generation::GenerationStats {
            duration_ms: 1_250,
            input_tokens: 2_000,
            cached_tokens: 1_500,
            output_tokens: 300,
        });
        let assistant = message(
            false,
            false,
            false,
            "Checking<｜DSML｜tool_calls><｜DSML｜invoke name=\"read\"><｜DSML｜parameter name=\"path\" string=\"true\">src/main.rs</｜DSML｜parameter></｜DSML｜invoke><｜DSML｜invoke name=\"list\"><｜DSML｜parameter name=\"path\" string=\"true\">src</｜DSML｜parameter></｜DSML｜invoke></｜DSML｜tool_calls>",
        );
        let tool = message(false, true, false, "Tool result 1\n...");
        let mut compaction = message(false, false, true, "summary");
        compaction.compaction = true;
        let system = message(false, false, true, "reminder");

        let summary = SessionStats::from_messages(
            &[user, assistant, tool, compaction, system],
            ModelChoice::DeepSeekV4Flash,
        );

        assert_eq!(
            summary,
            SessionStats {
                messages: 3,
                user_messages: 1,
                assistant_messages: 1,
                tool_messages: 1,
                tool_calls: 2,
                context_rebuilds: 1,
                completed_turns: 1,
                duration_ms: 1_250,
                input_tokens: 2_000,
                cached_tokens: 1_500,
                output_tokens: 300,
            }
        );
    }
}
