// Copyright 2025 Lablup Inc. and Jeongkyu Shin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! GPU / ANE / Pkg Power sparkline stack for the right half of the local
//! Activity panel.
//!
//! Renders a compact stack of braille sparkline rows, each formatted as:
//!
//! ```text
//! <label>  <braille sparkline>  <latest>  <scale badge>
//! ```
//!
//! The scale badge shows the row's fixed Y-axis range (e.g. `30-83`), not a
//! per-frame observed min/max, so it reads as a stable legend for the height.
//!
//! Rows rendered (platform-dependent):
//!
//! | Row       | Source                         | Color   |
//! |-----------|--------------------------------|---------|
//! | GPU Util  | `utilization_history`          | Blue    |
//! | GPU Mem   | `memory_history` (%)           | Green   |
//! | GPU Temp  | `temperature_history`          | Magenta |
//! | ANE       | `gpu.ane_utilization` (mW)     | Yellow  |
//! | Pkg Power | `combined_power_mw` / board pwr| Red     |
//!
//! The ANE row is shown on Apple Silicon regardless of current ANE power.
//!
//! ## Rendering model
//!
//! Because the Activity panel renders left and right halves on the same
//! terminal rows, both halves emit their lines into intermediate `Vec`
//! buffers.  The public [`render_combined_activity_panel`] function
//! interleaves the two halves and writes the combined output.

use std::io::Write;

use crossterm::{queue, style::Color, style::Print};

use crate::app_state::AppState;
use crate::common::config::ThemeConfig;
use crate::device::CpuInfo;
use crate::ui::activity_panel;
use crate::ui::braille::sparkline_braille;
use crate::ui::buffer::BufferWriter;
use crate::ui::scale::{ane_range, power_range, scale_badge, temp_range};
use crate::ui::text::print_colored_text;

/// Width reserved for the label column (e.g. "GPU Util").
const LABEL_WIDTH: usize = 9;

/// Width reserved for the latest-value column (e.g. "100.0%").
const VALUE_WIDTH: usize = 7;

/// Width reserved for the min-max badge (e.g. "0-100").
const MINMAX_WIDTH: usize = 9;

/// Fixed spacing characters between columns.
const SPACING: usize = 5; // 1+1 border padding + 3 inter-column spaces

/// Calculate the number of content rows for the GPU sparkline panel.
///
/// Returns the content row count (excluding borders).
pub fn gpu_content_rows(state: &AppState) -> usize {
    if state.gpu_info.is_empty() {
        return 0;
    }
    let ane = show_ane_row(state);
    let npu = show_npu_row(state);
    // GPU Util + GPU Mem + GPU Temp + (ANE?) + (NPU?) + Pkg Power
    3 + usize::from(ane) + usize::from(npu) + 1
}

/// Render the combined Activity panel (CPU left half + GPU right half).
///
/// Both halves are rendered into intermediate line buffers and then
/// interleaved so they appear on the same terminal rows.
///
/// When there is no GPU data, only the CPU left half is rendered.
pub fn render_combined_activity_panel<W: Write>(
    stdout: &mut W,
    state: &AppState,
    cpu_info: &[CpuInfo],
    width: usize,
) {
    if cpu_info.is_empty() || cpu_info[0].per_core_utilization.is_empty() {
        return;
    }

    let left_width = width / 2;
    let right_width = width - left_width;

    // Render left half (CPU) into line buffer
    let left_lines = render_cpu_lines(cpu_info, width);

    // Render right half (GPU) into line buffer
    let right_lines = render_gpu_lines(state, right_width);

    // Determine total lines needed (max of both halves)
    let total_lines = left_lines.len().max(right_lines.len());

    // Interleave and output
    for i in 0..total_lines {
        if i < left_lines.len() {
            // Write pre-formatted left line (contains ANSI escapes)
            stdout.write_all(left_lines[i].as_bytes()).unwrap();
        } else {
            // Pad with spaces for absent left half
            print_colored_text(stdout, &" ".repeat(left_width), Color::White, None, None);
        }

        if i < right_lines.len() {
            stdout.write_all(right_lines[i].as_bytes()).unwrap();
        }
        // else: right half is absent, line ends at left boundary

        queue!(stdout, Print("\r\n")).unwrap();
    }
}

// ---------------------------------------------------------------------------
// Left-half (CPU) line buffer rendering
// ---------------------------------------------------------------------------

/// Render the CPU activity panel into a vector of pre-formatted lines.
///
/// Each line is an ANSI-escaped string WITHOUT a trailing `\r\n`.
fn render_cpu_lines(cpu_info: &[CpuInfo], width: usize) -> Vec<String> {
    // Render the full CPU panel into a buffer
    let mut buf = BufferWriter::new();
    activity_panel::render_activity_panel(&mut buf, cpu_info, width);
    let raw = buf.get_buffer().to_string();

    // Split on "\r\n" and strip trailing empty line
    raw.split("\r\n")
        .filter(|line| !line.is_empty())
        .map(String::from)
        .collect()
}

// ---------------------------------------------------------------------------
// Right-half (GPU) line buffer rendering
// ---------------------------------------------------------------------------

/// Render the GPU sparkline panel into a vector of pre-formatted lines.
///
/// Each line is an ANSI-escaped string WITHOUT a trailing `\r\n`.
fn render_gpu_lines(state: &AppState, panel_width: usize) -> Vec<String> {
    if state.gpu_info.is_empty() {
        return Vec::new();
    }

    let is_apple = detect_apple_silicon(state);
    let ane = show_ane_row(state);
    let npu = show_npu_row(state);
    let rows = build_rows(state, is_apple, ane, npu);

    let mut lines: Vec<String> = Vec::with_capacity(rows.len() + 2);

    // Top border
    lines.push(render_line_to_string(|w| {
        draw_top_border(w, panel_width);
    }));

    // Content rows
    for row in &rows {
        lines.push(render_line_to_string(|w| {
            draw_sparkline_row(w, row, panel_width);
        }));
    }

    // Bottom border
    lines.push(render_line_to_string(|w| {
        draw_bottom_border(w, panel_width);
    }));

    lines
}

/// Helper: render a drawing function into a String (no trailing newline).
fn render_line_to_string<F>(f: F) -> String
where
    F: FnOnce(&mut BufferWriter),
{
    let mut buf = BufferWriter::new();
    f(&mut buf);
    buf.get_buffer().to_string()
}

// ---------------------------------------------------------------------------
// Row data model
// ---------------------------------------------------------------------------

struct SparklineRow {
    label: &'static str,
    color: Color,
    history: Vec<f64>,
    latest_str: String,
    min_max_str: String,
    range: Option<(f64, f64)>,
    /// Optional badge appended after the min-max (e.g. thermal pressure).
    badge: Option<(String, Color)>,
}

// ---------------------------------------------------------------------------
// Row construction
// ---------------------------------------------------------------------------

fn build_rows(state: &AppState, is_apple: bool, has_ane: bool, has_npu: bool) -> Vec<SparklineRow> {
    let mut rows = Vec::with_capacity(6);
    let gpu = state.gpu_info.first();

    // 1. GPU Utilization
    let gpu_util: Vec<f64> = state.utilization_history.iter().copied().collect();
    let latest_util = gpu_util.last().copied().unwrap_or(0.0);
    let util_range = (0.0, 100.0);
    rows.push(SparklineRow {
        label: "GPU Util",
        color: ThemeConfig::gpu_color(),
        latest_str: format!("{latest_util:.1}%"),
        min_max_str: scale_badge(util_range.0, util_range.1),
        history: gpu_util,
        range: Some(util_range),
        badge: None,
    });

    // 2. GPU Memory
    let gpu_mem: Vec<f64> = state.memory_history.iter().copied().collect();
    let latest_mem = gpu_mem.last().copied().unwrap_or(0.0);
    let mem_range = (0.0, 100.0);
    rows.push(SparklineRow {
        label: "GPU Mem",
        color: ThemeConfig::memory_color(),
        latest_str: format!("{latest_mem:.1}%"),
        min_max_str: scale_badge(mem_range.0, mem_range.1),
        history: gpu_mem,
        range: Some(mem_range),
        badge: None,
    });

    // 3. GPU Temperature — fixed axis anchored to the reported thermal
    //    threshold (or a 100°C fallback). The height then tracks how close the
    //    GPU is to throttling instead of rescaling to per-window noise.
    let gpu_temp: Vec<f64> = state.temperature_history.iter().copied().collect();
    let latest_temp = gpu_temp.last().copied().unwrap_or(0.0);
    let temp_rng = temp_range(gpu);
    rows.push(SparklineRow {
        label: "GPU Temp",
        color: ThemeConfig::thermal_color(),
        latest_str: format!("{latest_temp:.0}\u{00B0}C"),
        min_max_str: scale_badge(temp_rng.0, temp_rng.1),
        history: gpu_temp,
        range: Some(temp_rng),
        badge: None,
    });

    // 4. ANE (Apple Silicon -- always shown regardless of current power)
    if has_ane {
        let ane_w = state.ane_power_history.back().copied().unwrap_or_else(|| {
            state
                .gpu_info
                .first()
                .map(|g| g.ane_utilization / 1000.0)
                .unwrap_or(0.0)
        });
        let ane_history: Vec<f64> = if state.ane_power_history.is_empty() {
            vec![ane_w]
        } else {
            state.ane_power_history.iter().copied().collect()
        };
        let ane_rng = ane_range(&ane_history);
        rows.push(SparklineRow {
            label: "ANE",
            color: ThemeConfig::accelerator_color(),
            latest_str: format!("{ane_w:.1}W"),
            min_max_str: scale_badge(ane_rng.0, ane_rng.1),
            history: ane_history,
            range: Some(ane_rng),
            badge: None,
        });
    }

    // 4b. NPU (Intel/Windows -- scaffolding for future NPU reader)
    if has_npu {
        rows.push(SparklineRow {
            label: "NPU",
            color: ThemeConfig::accelerator_color(),
            latest_str: "0.0W".to_string(),
            min_max_str: String::new(),
            history: vec![0.0],
            range: None,
            badge: None,
        });
    }

    // 5. Pkg Power — fixed axis anchored to the enforced power limit when the
    //    driver reports one, else a nice-rounded ceiling over the observed peak.
    let power_w = package_power(state, is_apple);
    let power_history: Vec<f64> = if state.package_power_history.is_empty() {
        vec![power_w]
    } else {
        state.package_power_history.iter().copied().collect()
    };
    // Aggregate axis: package power is summed across all GPUs, so anchor the
    // ceiling to the summed per-GPU limits (not just the first GPU's).
    let power_rng = power_range(&state.gpu_info, &power_history);
    rows.push(SparklineRow {
        label: "Pkg Power",
        color: ThemeConfig::power_color(),
        latest_str: format!("{power_w:.1}W"),
        min_max_str: scale_badge(power_rng.0, power_rng.1),
        history: power_history,
        range: Some(power_rng),
        badge: None,
    });

    rows
}

// ---------------------------------------------------------------------------
// Drawing helpers (write to buffer, no trailing \r\n)
// ---------------------------------------------------------------------------

fn draw_top_border<W: Write>(stdout: &mut W, panel_width: usize) {
    let title = "GPU Metrics";
    let inner_width = panel_width.saturating_sub(2); // 2 corner chars (no left margin unlike CPU panel)
    let title_space = 1 + title.len() + 1;
    let dashes = inner_width.saturating_sub(title_space + 1);

    print_colored_text(
        stdout,
        "\u{256d}\u{2500}",
        ThemeConfig::accent_color(),
        None,
        None,
    );
    print_colored_text(stdout, " ", Color::White, None, None);
    print_colored_text(stdout, title, ThemeConfig::accent_color(), None, None);
    print_colored_text(stdout, " ", Color::White, None, None);
    for _ in 0..dashes {
        print_colored_text(stdout, "\u{2500}", ThemeConfig::accent_color(), None, None);
    }
    print_colored_text(stdout, "\u{256e}", ThemeConfig::accent_color(), None, None);
}

fn draw_bottom_border<W: Write>(stdout: &mut W, panel_width: usize) {
    let inner_width = panel_width.saturating_sub(2);
    print_colored_text(stdout, "\u{2570}", ThemeConfig::accent_color(), None, None);
    for _ in 0..inner_width {
        print_colored_text(stdout, "\u{2500}", ThemeConfig::accent_color(), None, None);
    }
    print_colored_text(stdout, "\u{256f}", ThemeConfig::accent_color(), None, None);
}

fn draw_sparkline_row<W: Write>(stdout: &mut W, row: &SparklineRow, panel_width: usize) {
    // Layout: "| " + label + " " + sparkline + " " + value + " " + minmax + badge + pad + " |"
    let content_width = panel_width.saturating_sub(4); // border chars + inner padding

    // Calculate sparkline width from available space
    let badge_len = row.badge.as_ref().map(|(s, _)| s.len() + 1).unwrap_or(0);
    let fixed = LABEL_WIDTH + VALUE_WIDTH + MINMAX_WIDTH + SPACING + badge_len;
    let sparkline_width = content_width.saturating_sub(fixed).max(4);

    let sparkline = sparkline_braille(&row.history, sparkline_width, row.range);

    // Left border
    print_colored_text(stdout, "\u{2502} ", ThemeConfig::accent_color(), None, None);

    // Label (right-padded to LABEL_WIDTH)
    let label_display = format!("{:<LABEL_WIDTH$}", row.label);
    print_colored_text(stdout, &label_display, row.color, None, None);
    print_colored_text(stdout, " ", Color::White, None, None);

    // Sparkline
    print_colored_text(stdout, &sparkline, row.color, None, None);
    print_colored_text(stdout, " ", Color::White, None, None);

    // Latest value (right-padded)
    let value_display = format!("{:<VALUE_WIDTH$}", row.latest_str);
    print_colored_text(stdout, &value_display, Color::White, None, None);

    // Min-max badge
    let minmax_display = format!("{:<MINMAX_WIDTH$}", row.min_max_str);
    print_colored_text(stdout, &minmax_display, Color::DarkGrey, None, None);

    // Optional badge (thermal pressure etc.)
    if let Some((ref text, color)) = row.badge {
        print_colored_text(stdout, " ", Color::White, None, None);
        print_colored_text(stdout, text, color, None, None);
    }

    // Pad to fill panel, then right border
    let used = 2 + LABEL_WIDTH + 1 + sparkline_width + 1 + VALUE_WIDTH + MINMAX_WIDTH + badge_len;
    let pad = panel_width.saturating_sub(used + 2);
    if pad > 0 {
        print_colored_text(stdout, &" ".repeat(pad), Color::White, None, None);
    }
    print_colored_text(stdout, " \u{2502}", ThemeConfig::accent_color(), None, None);
}

// ---------------------------------------------------------------------------
// Platform detection helpers
// ---------------------------------------------------------------------------

fn detect_apple_silicon(state: &AppState) -> bool {
    state.gpu_info.iter().any(|gpu| {
        gpu.detail
            .get("architecture")
            .map(|arch| arch == "Apple Silicon")
            .unwrap_or(false)
    })
}

/// Whether the ANE row should be shown in the GPU Metrics panel.
///
/// Returns `true` on Apple Silicon regardless of current ANE power.
/// An ANE at 0 W is a meaningful "idle" reading and the row is
/// load-bearing for platform identity even when the Neural Engine
/// is completely idle.
fn show_ane_row(state: &AppState) -> bool {
    detect_apple_silicon(state)
}

/// Whether an NPU row should be shown in the GPU Metrics panel.
///
/// Currently returns `false` -- no Intel/Windows NPU reader exists yet.
/// When an NPU telemetry reader is added (Meteor Lake / Core Ultra),
/// flip this to check for NPU presence via `src/api/metrics/npu/common.rs`.
fn show_npu_row(_state: &AppState) -> bool {
    false
}

fn package_power(state: &AppState, is_apple: bool) -> f64 {
    if is_apple {
        // Apple Silicon: combined CPU+GPU+ANE power from native metrics
        let power_mw = state
            .gpu_info
            .iter()
            .filter_map(|gpu| {
                gpu.detail
                    .get("combined_power_mw")
                    .and_then(|s| s.parse::<f64>().ok())
            })
            .next()
            .unwrap_or(0.0);
        power_mw / 1000.0
    } else {
        // NVIDIA / other: sum GPU board power
        state.gpu_info.iter().map(|g| g.power_consumption).sum()
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app_state::AppState;
    use crate::device::{
        AppleSiliconCpuInfo, CoreType, CoreUtilization, CpuInfo, CpuPlatformType, GpuInfo,
    };
    use std::collections::HashMap;

    fn make_nvidia_state() -> AppState {
        let mut state = AppState::new();
        state.is_local_mode = true;

        let mut detail = HashMap::new();
        detail.insert("architecture".to_string(), "NVIDIA".to_string());

        state.gpu_info.push(GpuInfo {
            uuid: "gpu-0".to_string(),
            time: String::new(),
            name: "RTX 4090".to_string(),
            device_type: "GPU".to_string(),
            host_id: "localhost".to_string(),
            hostname: "localhost".to_string(),
            instance: "localhost".to_string(),
            utilization: 75.0,
            ane_utilization: 0.0,
            dla_utilization: None,
            tensorcore_utilization: None,
            temperature: 72,
            used_memory: 8 * 1024 * 1024 * 1024,
            total_memory: 24 * 1024 * 1024 * 1024,
            frequency: 2100,
            power_consumption: 320.0,
            gpu_core_count: None,
            temperature_threshold_slowdown: None,
            temperature_threshold_shutdown: None,
            temperature_threshold_max_operating: None,
            temperature_threshold_acoustic: None,
            performance_state: None,
            numa_node_id: None,
            gsp_firmware_mode: None,
            gsp_firmware_version: None,
            nvlink_remote_devices: Vec::new(),
            gpm_metrics: None,
            detail,
        });

        // Populate histories
        for i in 0..20 {
            state.utilization_history.push_back(i as f64 * 5.0);
            state.memory_history.push_back(i as f64 * 3.0);
            state.temperature_history.push_back(50.0 + i as f64);
            state
                .package_power_history
                .push_back(120.0 + i as f64 * 2.0);
        }
        state
    }

    fn make_apple_silicon_state() -> AppState {
        let mut state = AppState::new();
        state.is_local_mode = true;

        let mut detail = HashMap::new();
        detail.insert("architecture".to_string(), "Apple Silicon".to_string());
        detail.insert("combined_power_mw".to_string(), "12500".to_string());

        state.gpu_info.push(GpuInfo {
            uuid: "apple-gpu".to_string(),
            time: String::new(),
            name: "Apple M2 Pro".to_string(),
            device_type: "GPU".to_string(),
            host_id: "localhost".to_string(),
            hostname: "localhost".to_string(),
            instance: "localhost".to_string(),
            utilization: 45.0,
            ane_utilization: 3500.0, // 3500 mW = 3.5 W
            dla_utilization: None,
            tensorcore_utilization: None,
            temperature: 55,
            used_memory: 4 * 1024 * 1024 * 1024,
            total_memory: 16 * 1024 * 1024 * 1024,
            frequency: 1398,
            power_consumption: 8.0,
            gpu_core_count: Some(16),
            temperature_threshold_slowdown: None,
            temperature_threshold_shutdown: None,
            temperature_threshold_max_operating: None,
            temperature_threshold_acoustic: None,
            performance_state: None,
            numa_node_id: None,
            gsp_firmware_mode: None,
            gsp_firmware_version: None,
            nvlink_remote_devices: Vec::new(),
            gpm_metrics: None,
            detail,
        });

        for i in 0..20 {
            state.utilization_history.push_back(i as f64 * 4.0);
            state.memory_history.push_back(i as f64 * 2.5);
            state.temperature_history.push_back(40.0 + i as f64);
            state.package_power_history.push_back(8.0 + i as f64 * 0.5);
            state.ane_power_history.push_back(i as f64 * 0.2);
        }
        state
    }

    fn make_standard_cpu(core_count: usize) -> CpuInfo {
        let per_core: Vec<CoreUtilization> = (0..core_count)
            .map(|i| CoreUtilization {
                core_id: i as u32,
                core_type: CoreType::Standard,
                utilization: (i as f64 * 10.0) % 100.0,
            })
            .collect();

        CpuInfo {
            index: 0,
            host_id: "localhost".to_string(),
            hostname: "testhost".to_string(),
            instance: "".to_string(),
            cpu_model: "Test CPU".to_string(),
            architecture: "x86_64".to_string(),
            platform_type: CpuPlatformType::Intel,
            socket_count: 1,
            total_cores: core_count as u32,
            total_threads: core_count as u32 * 2,
            base_frequency_mhz: 3000,
            max_frequency_mhz: 4000,
            cache_size_mb: 16,
            utilization: 50.0,
            temperature: Some(65),
            power_consumption: Some(95.0),
            per_socket_info: Vec::new(),
            apple_silicon_info: None,
            per_core_utilization: per_core,
            time: String::new(),
        }
    }

    fn make_apple_cpu() -> CpuInfo {
        let mut per_core = Vec::new();
        for i in 0..4 {
            per_core.push(CoreUtilization {
                core_id: i as u32,
                core_type: CoreType::Efficiency,
                utilization: 20.0 + i as f64 * 5.0,
            });
        }
        for i in 0..8 {
            per_core.push(CoreUtilization {
                core_id: (4 + i) as u32,
                core_type: CoreType::Performance,
                utilization: 40.0 + i as f64 * 5.0,
            });
        }
        CpuInfo {
            index: 0,
            host_id: "localhost".to_string(),
            hostname: "testhost".to_string(),
            instance: "".to_string(),
            cpu_model: "Apple M2 Pro".to_string(),
            architecture: "arm64".to_string(),
            platform_type: CpuPlatformType::AppleSilicon,
            socket_count: 1,
            total_cores: 12,
            total_threads: 12,
            base_frequency_mhz: 3490,
            max_frequency_mhz: 3490,
            cache_size_mb: 16,
            utilization: 35.0,
            temperature: None,
            power_consumption: None,
            per_socket_info: Vec::new(),
            apple_silicon_info: Some(AppleSiliconCpuInfo {
                s_core_count: 0,
                p_core_count: 8,
                e_core_count: 4,
                gpu_core_count: 16,
                s_core_utilization: 0.0,
                p_core_utilization: 55.0,
                e_core_utilization: 25.0,
                ane_ops_per_second: None,
                s_cluster_frequency_mhz: None,
                p_cluster_frequency_mhz: Some(3490),
                e_cluster_frequency_mhz: Some(2420),
                s_core_l2_cache_mb: None,
                p_core_l2_cache_mb: Some(16),
                e_core_l2_cache_mb: Some(4),
            }),
            per_core_utilization: per_core,
            time: String::new(),
        }
    }

    #[test]
    fn test_gpu_content_rows_empty() {
        let state = AppState::new();
        assert_eq!(gpu_content_rows(&state), 0);
    }

    #[test]
    fn test_gpu_content_rows_nvidia() {
        let state = make_nvidia_state();
        // GPU Util + GPU Mem + GPU Temp + Pkg Power = 4
        assert_eq!(gpu_content_rows(&state), 4);
    }

    #[test]
    fn test_gpu_content_rows_apple_with_ane() {
        let state = make_apple_silicon_state();
        // GPU Util + GPU Mem + GPU Temp + ANE + Pkg Power = 5
        assert_eq!(gpu_content_rows(&state), 5);
    }

    #[test]
    fn test_detect_apple_silicon() {
        assert!(!detect_apple_silicon(&make_nvidia_state()));
        assert!(detect_apple_silicon(&make_apple_silicon_state()));
    }

    #[test]
    fn test_show_ane_row() {
        assert!(!show_ane_row(&make_nvidia_state()));
        assert!(show_ane_row(&make_apple_silicon_state()));
    }

    #[test]
    fn test_show_ane_row_even_when_ane_idle() {
        // ANE row should be shown even when ane_utilization is 0
        let mut state = make_apple_silicon_state();
        state.gpu_info[0].ane_utilization = 0.0;
        assert!(show_ane_row(&state));
    }

    #[test]
    fn test_show_npu_row_returns_false() {
        assert!(!show_npu_row(&make_nvidia_state()));
        assert!(!show_npu_row(&make_apple_silicon_state()));
    }

    #[test]
    fn test_gpu_content_rows_apple_with_zero_ane_still_shows_row() {
        let mut state = make_apple_silicon_state();
        state.gpu_info[0].ane_utilization = 0.0;
        // GPU Util + GPU Mem + GPU Temp + ANE (always-on) + Pkg Power = 5
        assert_eq!(gpu_content_rows(&state), 5);
    }

    #[test]
    fn test_package_power_apple_silicon() {
        let state = make_apple_silicon_state();
        let watts = package_power(&state, true);
        assert!((watts - 12.5).abs() < 0.01);
    }

    #[test]
    fn test_package_power_nvidia() {
        let state = make_nvidia_state();
        let watts = package_power(&state, false);
        assert!((watts - 320.0).abs() < 0.01);
    }

    #[test]
    fn test_build_rows_nvidia() {
        let state = make_nvidia_state();
        let rows = build_rows(&state, false, false, false);
        assert_eq!(rows.len(), 4);
        assert_eq!(rows[0].label, "GPU Util");
        assert_eq!(rows[1].label, "GPU Mem");
        assert_eq!(rows[2].label, "GPU Temp");
        assert_eq!(rows[3].label, "Pkg Power");
        assert!(rows[2].badge.is_none());
    }

    #[test]
    fn test_build_rows_apple_silicon() {
        let state = make_apple_silicon_state();
        let rows = build_rows(&state, true, true, false);
        assert_eq!(rows.len(), 5);
        assert_eq!(rows[0].label, "GPU Util");
        assert_eq!(rows[3].label, "ANE");
        assert_eq!(rows[4].label, "Pkg Power");
        assert!(rows[2].badge.is_none());
        // Scale badges now show the fixed axis, not the observed window:
        //   ANE  peak 3.8 W -> floored to 8 W -> nice_ceil 10 W
        //   Pkg  peak 17.5 W (no power limit) -> nice_ceil 20 W
        assert_eq!(rows[3].min_max_str, "0-10");
        assert_eq!(rows[4].min_max_str, "0-20");
        // GPU Temp uses the 30°C floor + 100°C fallback (no thresholds set).
        assert_eq!(rows[2].min_max_str, "30-100");
    }

    #[test]
    fn test_build_rows_with_npu_scaffolding() {
        let state = make_nvidia_state();
        let rows = build_rows(&state, false, false, true);
        assert_eq!(rows.len(), 5);
        assert_eq!(rows[3].label, "NPU");
        assert_eq!(rows[4].label, "Pkg Power");
    }

    #[test]
    fn test_render_gpu_lines_nvidia() {
        let state = make_nvidia_state();
        let lines = render_gpu_lines(&state, 60);
        // top border + 4 content rows + bottom border = 6
        assert_eq!(lines.len(), 6);
        assert!(!lines[0].is_empty()); // top border
    }

    #[test]
    fn test_render_gpu_lines_apple_silicon() {
        let state = make_apple_silicon_state();
        let lines = render_gpu_lines(&state, 60);
        // top border + 5 content rows + bottom border = 7
        assert_eq!(lines.len(), 7);
    }

    #[test]
    fn test_render_gpu_lines_empty() {
        let state = AppState::new();
        let lines = render_gpu_lines(&state, 60);
        assert!(lines.is_empty());
    }

    #[test]
    fn test_render_combined_does_not_panic_nvidia() {
        let mut state = make_nvidia_state();
        let cpu = vec![make_standard_cpu(8)];
        state.cpu_info = cpu.clone();
        let mut buf: Vec<u8> = Vec::new();
        render_combined_activity_panel(&mut buf, &state, &cpu, 120);
        assert!(!buf.is_empty());
    }

    #[test]
    fn test_render_combined_does_not_panic_apple() {
        let mut state = make_apple_silicon_state();
        let cpu = vec![make_apple_cpu()];
        state.cpu_info = cpu.clone();
        let mut buf: Vec<u8> = Vec::new();
        render_combined_activity_panel(&mut buf, &state, &cpu, 120);
        assert!(!buf.is_empty());
    }

    #[test]
    fn test_render_combined_no_gpu() {
        let state = AppState::new();
        let cpu = vec![make_standard_cpu(4)];
        let mut buf: Vec<u8> = Vec::new();
        render_combined_activity_panel(&mut buf, &state, &cpu, 120);
        // Should still render - CPU only
        assert!(!buf.is_empty());
    }

    #[test]
    fn test_render_combined_no_cpu() {
        let state = make_nvidia_state();
        let cpu: Vec<CpuInfo> = Vec::new();
        let mut buf: Vec<u8> = Vec::new();
        render_combined_activity_panel(&mut buf, &state, &cpu, 120);
        // No CPU info -> no output
        assert!(buf.is_empty());
    }
}
