//! WebAssembly bindings for training monitor.
//!
//! Provides JavaScript-accessible functions for real-time training
//! visualization in the browser.
//!
//! # Usage (JavaScript)
//!
//! ```javascript
//! import init, { WasmMetricsCollector, WasmDashboard } from 'entrenar-monitor';
//!
//! await init();
//!
//! const collector = new WasmMetricsCollector();
//! collector.record_loss(0.5);
//! collector.record_accuracy(0.85);
//!
//! const stats = collector.summary_json();
//! console.log(JSON.parse(stats));
//!
//! const dashboard = new WasmDashboard(800, 400);
//! dashboard.update(collector);
//! const pngData = dashboard.render_png();
//! ```

#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

use super::{Metric, MetricStats, MetricsCollector};
use std::collections::HashMap;

// ============================================================================
// WASM Metrics Collector
// ============================================================================

/// WASM-compatible metrics collector.
///
/// Wraps MetricsCollector with JavaScript-friendly API.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Debug)]
pub struct WasmMetricsCollector {
    inner: MetricsCollector,
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl WasmMetricsCollector {
    /// Create a new metrics collector.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
    pub fn new() -> Self {
        Self {
            inner: MetricsCollector::new(),
        }
    }

    /// Record a loss value.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn record_loss(&mut self, value: f64) {
        self.inner.record(Metric::Loss, value);
    }

    /// Record an accuracy value.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn record_accuracy(&mut self, value: f64) {
        self.inner.record(Metric::Accuracy, value);
    }

    /// Record a learning rate value.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn record_learning_rate(&mut self, value: f64) {
        self.inner.record(Metric::LearningRate, value);
    }

    /// Record a gradient norm value.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn record_gradient_norm(&mut self, value: f64) {
        self.inner.record(Metric::GradientNorm, value);
    }

    /// Record a custom metric.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn record_custom(&mut self, name: &str, value: f64) {
        self.inner.record(Metric::Custom(name.to_string()), value);
    }

    /// Get number of recorded metrics.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn count(&self) -> usize {
        self.inner.count()
    }

    /// Check if collector is empty.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    /// Clear all recorded metrics.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn clear(&mut self) {
        self.inner.clear();
    }

    /// Get summary as JSON string.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn summary_json(&self) -> String {
        let summary = self.inner.summary();
        let json_map: HashMap<String, WasmMetricStats> = summary
            .into_iter()
            .map(|(k, v)| (k.as_str().to_string(), WasmMetricStats::from(v)))
            .collect();
        serde_json::to_string(&json_map).unwrap_or_else(|_| "{}".to_string())
    }

    /// Get loss statistics.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_mean(&self) -> f64 {
        self.inner
            .summary()
            .get(&Metric::Loss)
            .map_or(f64::NAN, |s| s.mean)
    }

    /// Get accuracy statistics.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn accuracy_mean(&self) -> f64 {
        self.inner
            .summary()
            .get(&Metric::Accuracy)
            .map_or(f64::NAN, |s| s.mean)
    }

    /// Get loss values as a Vec for JavaScript Float64Array conversion.
    pub fn loss_values(&self) -> Vec<f64> {
        self.inner
            .to_records()
            .iter()
            .filter(|r| r.metric == Metric::Loss)
            .map(|r| r.value)
            .collect()
    }

    /// Get accuracy values as a Vec for JavaScript Float64Array conversion.
    pub fn accuracy_values(&self) -> Vec<f64> {
        self.inner
            .to_records()
            .iter()
            .filter(|r| r.metric == Metric::Accuracy)
            .map(|r| r.value)
            .collect()
    }

    /// Get all timestamps as milliseconds since epoch.
    pub fn timestamps(&self) -> Vec<u64> {
        self.inner
            .to_records()
            .iter()
            .map(|r| r.timestamp)
            .collect()
    }

    /// Get loss standard deviation.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_std(&self) -> f64 {
        self.inner
            .summary()
            .get(&Metric::Loss)
            .map_or(f64::NAN, |s| s.std)
    }

    /// Get accuracy standard deviation.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn accuracy_std(&self) -> f64 {
        self.inner
            .summary()
            .get(&Metric::Accuracy)
            .map_or(f64::NAN, |s| s.std)
    }

    /// Check if NaN was detected in loss.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_has_nan(&self) -> bool {
        self.inner
            .summary()
            .get(&Metric::Loss)
            .is_some_and(|s| s.has_nan)
    }

    /// Check if Inf was detected in loss.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_has_inf(&self) -> bool {
        self.inner
            .summary()
            .get(&Metric::Loss)
            .is_some_and(|s| s.has_inf)
    }
}

impl Default for WasmMetricsCollector {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// WASM Metric Stats (JSON-serializable)
// ============================================================================

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct WasmMetricStats {
    count: usize,
    mean: f64,
    std: f64,
    min: f64,
    max: f64,
    has_nan: bool,
    has_inf: bool,
}

impl From<MetricStats> for WasmMetricStats {
    fn from(s: MetricStats) -> Self {
        Self {
            count: s.count,
            mean: s.mean,
            std: s.std,
            min: s.min,
            max: s.max,
            has_nan: s.has_nan,
            has_inf: s.has_inf,
        }
    }
}

// ============================================================================
// WASM Dashboard Options
// ============================================================================

/// Dashboard rendering options.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Debug, Clone)]
pub struct WasmDashboardOptions {
    width: u32,
    height: u32,
    background_color: String,
    loss_color: String,
    accuracy_color: String,
    show_sparklines: bool,
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl WasmDashboardOptions {
    /// Create default dashboard options.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
    pub fn new() -> Self {
        Self {
            width: 800,
            height: 400,
            background_color: "#1a1a2e".to_string(),
            loss_color: "#ff6b6b".to_string(),
            accuracy_color: "#4ecdc4".to_string(),
            show_sparklines: true,
        }
    }

    /// Set width in pixels.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn width(mut self, width: u32) -> Self {
        self.width = width;
        self
    }

    /// Set height in pixels.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn height(mut self, height: u32) -> Self {
        self.height = height;
        self
    }

    /// Set background color (hex format).
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn background_color(mut self, color: &str) -> Self {
        self.background_color = color.to_string();
        self
    }

    /// Set loss color (hex format).
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_color(mut self, color: &str) -> Self {
        self.loss_color = color.to_string();
        self
    }

    /// Set accuracy color (hex format).
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn accuracy_color(mut self, color: &str) -> Self {
        self.accuracy_color = color.to_string();
        self
    }

    /// Enable/disable sparklines.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn show_sparklines(mut self, show: bool) -> Self {
        self.show_sparklines = show;
        self
    }

    /// Get width.
    pub fn get_width(&self) -> u32 {
        self.width
    }

    /// Get height.
    pub fn get_height(&self) -> u32 {
        self.height
    }
}

impl Default for WasmDashboardOptions {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// WASM Dashboard (Canvas Rendering)
// ============================================================================

/// WASM dashboard for canvas rendering.
///
/// Renders training metrics to a canvas element in the browser.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Debug)]
pub struct WasmDashboard {
    options: WasmDashboardOptions,
    loss_history: Vec<f64>,
    accuracy_history: Vec<f64>,
    max_history: usize,
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl WasmDashboard {
    /// Create a new dashboard with default options.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
    pub fn new() -> Self {
        Self {
            options: WasmDashboardOptions::new(),
            loss_history: Vec::new(),
            accuracy_history: Vec::new(),
            max_history: 100,
        }
    }

    /// Create a dashboard with custom options.
    pub fn with_options(options: WasmDashboardOptions) -> Self {
        Self {
            options,
            loss_history: Vec::new(),
            accuracy_history: Vec::new(),
            max_history: 100,
        }
    }

    /// Set maximum history length.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn max_history(mut self, max: usize) -> Self {
        self.max_history = max;
        self
    }

    /// Update dashboard with new metrics from collector.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn update(&mut self, collector: &WasmMetricsCollector) {
        // Get latest values
        let loss = collector.loss_mean();
        let accuracy = collector.accuracy_mean();

        // Add to history if valid
        if !loss.is_nan() {
            self.loss_history.push(loss);
            if self.loss_history.len() > self.max_history {
                self.loss_history.remove(0);
            }
        }

        if !accuracy.is_nan() {
            self.accuracy_history.push(accuracy);
            if self.accuracy_history.len() > self.max_history {
                self.accuracy_history.remove(0);
            }
        }
    }

    /// Add a loss value directly to history.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn add_loss(&mut self, value: f64) {
        if !value.is_nan() && !value.is_infinite() {
            self.loss_history.push(value);
            if self.loss_history.len() > self.max_history {
                self.loss_history.remove(0);
            }
        }
    }

    /// Add an accuracy value directly to history.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn add_accuracy(&mut self, value: f64) {
        if !value.is_nan() && !value.is_infinite() {
            self.accuracy_history.push(value);
            if self.accuracy_history.len() > self.max_history {
                self.accuracy_history.remove(0);
            }
        }
    }

    /// Get loss history length.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_history_len(&self) -> usize {
        self.loss_history.len()
    }

    /// Get accuracy history length.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn accuracy_history_len(&self) -> usize {
        self.accuracy_history.len()
    }

    /// Clear all history.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn clear(&mut self) {
        self.loss_history.clear();
        self.accuracy_history.clear();
    }

    /// Get canvas width.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn width(&self) -> u32 {
        self.options.width
    }

    /// Get canvas height.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn height(&self) -> u32 {
        self.options.height
    }

    /// Get loss history as normalized Y coordinates (0.0-1.0).
    /// Returns empty vec if no history.
    pub fn loss_normalized(&self) -> Vec<f64> {
        normalize_values(&self.loss_history)
    }

    /// Get accuracy history as normalized Y coordinates (0.0-1.0).
    /// Accuracy is already 0-1 typically, but this handles edge cases.
    pub fn accuracy_normalized(&self) -> Vec<f64> {
        normalize_values(&self.accuracy_history)
    }

    /// Get X coordinates for plotting (normalized 0.0-1.0).
    pub fn x_coordinates(&self, len: usize) -> Vec<f64> {
        if len <= 1 {
            return vec![0.5];
        }
        (0..len).map(|i| i as f64 / (len - 1) as f64).collect()
    }

    /// Generate sparkline characters for terminal display.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn loss_sparkline(&self) -> String {
        generate_sparkline(&self.loss_history, 20)
    }

    /// Generate sparkline characters for accuracy.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn accuracy_sparkline(&self) -> String {
        generate_sparkline(&self.accuracy_history, 20)
    }

    /// Get dashboard state as JSON.
    #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
    pub fn state_json(&self) -> String {
        let state = DashboardState {
            width: self.options.width,
            height: self.options.height,
            loss_history: self.loss_history.clone(),
            accuracy_history: self.accuracy_history.clone(),
            loss_color: self.options.loss_color.clone(),
            accuracy_color: self.options.accuracy_color.clone(),
            background_color: self.options.background_color.clone(),
        };
        serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string())
    }
}

impl Default for WasmDashboard {
    fn default() -> Self {
        Self::new()
    }
}

/// Dashboard state for JSON serialization.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct DashboardState {
    width: u32,
    height: u32,
    loss_history: Vec<f64>,
    accuracy_history: Vec<f64>,
    loss_color: String,
    accuracy_color: String,
    background_color: String,
}

/// Normalize values to 0.0-1.0 range.
fn normalize_values(values: &[f64]) -> Vec<f64> {
    if values.is_empty() {
        return Vec::new();
    }

    let min = values.iter().copied().fold(f64::INFINITY, f64::min);
    let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);

    if (max - min).abs() < 1e-10 {
        // All values are the same
        return vec![0.5; values.len()];
    }

    values.iter().map(|v| (v - min) / (max - min)).collect()
}

/// Generate a sparkline string from values.
fn generate_sparkline(values: &[f64], max_len: usize) -> String {
    const CHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];

    if values.is_empty() {
        return String::new();
    }

    // Subsample if needed
    let subsampled: Vec<f64> = if values.len() > max_len {
        let step = values.len() as f64 / max_len as f64;
        (0..max_len)
            .map(|i| values[(i as f64 * step) as usize])
            .collect()
    } else {
        values.to_vec()
    };

    let normalized = normalize_values(&subsampled);
    normalized
        .iter()
        .map(|&v| {
            let idx = ((v * 7.0).round() as usize).min(7);
            CHARS[idx]
        })
        .collect()
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_wasm_collector_new() {
        let collector = WasmMetricsCollector::new();
        assert!(collector.is_empty());
        assert_eq!(collector.count(), 0);
    }

    #[test]
    fn test_wasm_collector_record_loss() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_loss(0.3);
        assert_eq!(collector.count(), 2);
        assert!((collector.loss_mean() - 0.4).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_collector_record_accuracy() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_accuracy(0.8);
        collector.record_accuracy(0.9);
        assert_eq!(collector.count(), 2);
        assert!((collector.accuracy_mean() - 0.85).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_collector_record_custom() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_custom("perplexity", 15.5);
        assert_eq!(collector.count(), 1);
    }

    #[test]
    fn test_wasm_collector_clear() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_accuracy(0.8);
        assert_eq!(collector.count(), 2);
        collector.clear();
        assert!(collector.is_empty());
    }

    #[test]
    fn test_wasm_collector_summary_json() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_loss(0.3);

        let json = collector.summary_json();
        assert!(json.contains("loss"));
        assert!(json.contains("mean"));

        // Parse and validate
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert!(parsed.get("loss").is_some());
    }

    #[test]
    fn test_wasm_collector_missing_metric() {
        let collector = WasmMetricsCollector::new();
        assert!(collector.loss_mean().is_nan());
        assert!(collector.accuracy_mean().is_nan());
    }

    #[test]
    fn test_wasm_dashboard_options_default() {
        let opts = WasmDashboardOptions::new();
        assert_eq!(opts.width, 800);
        assert_eq!(opts.height, 400);
        assert_eq!(opts.background_color, "#1a1a2e");
    }

    #[test]
    fn test_wasm_dashboard_options_builder() {
        let opts = WasmDashboardOptions::new()
            .width(1024)
            .height(768)
            .background_color("#ffffff")
            .loss_color("#ff0000")
            .accuracy_color("#00ff00")
            .show_sparklines(false);

        assert_eq!(opts.width, 1024);
        assert_eq!(opts.height, 768);
        assert_eq!(opts.background_color, "#ffffff");
        assert_eq!(opts.loss_color, "#ff0000");
        assert_eq!(opts.accuracy_color, "#00ff00");
        assert!(!opts.show_sparklines);
    }

    #[test]
    fn test_wasm_metric_stats_from() {
        let stats = MetricStats {
            count: 10,
            mean: 0.5,
            std: 0.1,
            min: 0.2,
            max: 0.8,
            sum: 5.0,
            has_nan: false,
            has_inf: false,
        };

        let wasm_stats = WasmMetricStats::from(stats);
        assert_eq!(wasm_stats.count, 10);
        assert!((wasm_stats.mean - 0.5).abs() < 1e-6);
        assert!((wasm_stats.std - 0.1).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_collector_all_metric_types() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_accuracy(0.8);
        collector.record_learning_rate(0.001);
        collector.record_gradient_norm(1.5);
        collector.record_custom("perplexity", 15.5);

        assert_eq!(collector.count(), 5);

        let json = collector.summary_json();
        assert!(json.contains("loss"));
        assert!(json.contains("accuracy"));
        assert!(json.contains("learning_rate"));
        assert!(json.contains("gradient_norm"));
        assert!(json.contains("perplexity"));
    }

    #[test]
    fn test_wasm_collector_default() {
        let collector = WasmMetricsCollector::default();
        assert!(collector.is_empty());
    }

    #[test]
    fn test_wasm_dashboard_options_default_trait() {
        let opts = WasmDashboardOptions::default();
        assert_eq!(opts.get_width(), 800);
        assert_eq!(opts.get_height(), 400);
    }

    #[test]
    fn test_wasm_collector_loss_values() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_loss(0.3);
        collector.record_accuracy(0.8); // Should not be in loss values

        let values = collector.loss_values();
        assert_eq!(values.len(), 2);
        assert!((values[0] - 0.5).abs() < 1e-6);
        assert!((values[1] - 0.3).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_collector_accuracy_values() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_accuracy(0.8);
        collector.record_accuracy(0.9);
        collector.record_loss(0.5); // Should not be in accuracy values

        let values = collector.accuracy_values();
        assert_eq!(values.len(), 2);
        assert!((values[0] - 0.8).abs() < 1e-6);
        assert!((values[1] - 0.9).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_collector_timestamps() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_loss(0.3);

        let timestamps = collector.timestamps();
        assert_eq!(timestamps.len(), 2);
        assert!(timestamps[0] > 0);
        assert!(timestamps[1] >= timestamps[0]);
    }

    #[test]
    fn test_wasm_collector_loss_std() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.2);
        collector.record_loss(0.4);
        collector.record_loss(0.6);
        collector.record_loss(0.8);

        let std = collector.loss_std();
        assert!(std > 0.0);
        assert!(std < 1.0);
    }

    #[test]
    fn test_wasm_collector_nan_detection() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        assert!(!collector.loss_has_nan());

        collector.record_loss(f64::NAN);
        assert!(collector.loss_has_nan());
    }

    #[test]
    fn test_wasm_collector_inf_detection() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        assert!(!collector.loss_has_inf());

        collector.record_loss(f64::INFINITY);
        assert!(collector.loss_has_inf());
    }

    #[test]
    fn test_wasm_collector_empty_std() {
        let collector = WasmMetricsCollector::new();
        assert!(collector.loss_std().is_nan());
        assert!(collector.accuracy_std().is_nan());
    }

    // ========================================================================
    // WasmDashboard tests
    // ========================================================================

    #[test]
    fn test_wasm_dashboard_new() {
        let dashboard = WasmDashboard::new();
        assert_eq!(dashboard.loss_history_len(), 0);
        assert_eq!(dashboard.accuracy_history_len(), 0);
        assert_eq!(dashboard.width(), 800);
        assert_eq!(dashboard.height(), 400);
    }

    #[test]
    fn test_wasm_dashboard_add_loss() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_loss(0.5);
        dashboard.add_loss(0.3);
        assert_eq!(dashboard.loss_history_len(), 2);
    }

    #[test]
    fn test_wasm_dashboard_add_accuracy() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_accuracy(0.8);
        dashboard.add_accuracy(0.9);
        assert_eq!(dashboard.accuracy_history_len(), 2);
    }

    #[test]
    fn test_wasm_dashboard_ignores_nan() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_loss(0.5);
        dashboard.add_loss(f64::NAN);
        dashboard.add_loss(0.3);
        assert_eq!(dashboard.loss_history_len(), 2);
    }

    #[test]
    fn test_wasm_dashboard_ignores_inf() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_accuracy(0.8);
        dashboard.add_accuracy(f64::INFINITY);
        assert_eq!(dashboard.accuracy_history_len(), 1);
    }

    #[test]
    fn test_wasm_dashboard_max_history() {
        let mut dashboard = WasmDashboard::new().max_history(5);
        for i in 0..10 {
            dashboard.add_loss(i as f64);
        }
        assert_eq!(dashboard.loss_history_len(), 5);
    }

    #[test]
    fn test_wasm_dashboard_clear() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_loss(0.5);
        dashboard.add_accuracy(0.8);
        dashboard.clear();
        assert_eq!(dashboard.loss_history_len(), 0);
        assert_eq!(dashboard.accuracy_history_len(), 0);
    }

    #[test]
    fn test_wasm_dashboard_update() {
        let mut collector = WasmMetricsCollector::new();
        collector.record_loss(0.5);
        collector.record_accuracy(0.8);

        let mut dashboard = WasmDashboard::new();
        dashboard.update(&collector);

        assert_eq!(dashboard.loss_history_len(), 1);
        assert_eq!(dashboard.accuracy_history_len(), 1);
    }

    #[test]
    fn test_wasm_dashboard_sparkline() {
        let mut dashboard = WasmDashboard::new();
        for i in 0..10 {
            dashboard.add_loss(i as f64 / 10.0);
        }

        let sparkline = dashboard.loss_sparkline();
        assert!(!sparkline.is_empty());
        assert!(sparkline.chars().count() <= 20);
    }

    #[test]
    fn test_wasm_dashboard_state_json() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_loss(0.5);
        dashboard.add_accuracy(0.8);

        let json = dashboard.state_json();
        assert!(json.contains("width"));
        assert!(json.contains("loss_history"));
        assert!(json.contains("accuracy_history"));
    }

    #[test]
    fn test_normalize_values_empty() {
        let result = normalize_values(&[]);
        assert!(result.is_empty());
    }

    #[test]
    fn test_normalize_values_single() {
        let result = normalize_values(&[5.0]);
        assert_eq!(result.len(), 1);
        assert!((result[0] - 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_normalize_values_range() {
        let result = normalize_values(&[0.0, 5.0, 10.0]);
        assert_eq!(result.len(), 3);
        assert!((result[0] - 0.0).abs() < 1e-6);
        assert!((result[1] - 0.5).abs() < 1e-6);
        assert!((result[2] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_normalize_values_constant() {
        let result = normalize_values(&[5.0, 5.0, 5.0]);
        assert_eq!(result.len(), 3);
        // All same value should return 0.5
        for v in &result {
            assert!((v - 0.5).abs() < 1e-6);
        }
    }

    #[test]
    fn test_generate_sparkline_empty() {
        let result = generate_sparkline(&[], 20);
        assert!(result.is_empty());
    }

    #[test]
    fn test_generate_sparkline_basic() {
        let result = generate_sparkline(&[0.0, 0.5, 1.0], 20);
        assert_eq!(result.chars().count(), 3);
    }

    #[test]
    fn test_generate_sparkline_subsample() {
        let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let result = generate_sparkline(&values, 20);
        assert_eq!(result.chars().count(), 20);
    }

    #[test]
    fn test_wasm_dashboard_x_coordinates() {
        let dashboard = WasmDashboard::new();

        let coords = dashboard.x_coordinates(5);
        assert_eq!(coords.len(), 5);
        assert!((coords[0] - 0.0).abs() < 1e-6);
        assert!((coords[4] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_dashboard_x_coordinates_single() {
        let dashboard = WasmDashboard::new();
        let coords = dashboard.x_coordinates(1);
        assert_eq!(coords.len(), 1);
        assert!((coords[0] - 0.5).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_dashboard_loss_normalized() {
        let mut dashboard = WasmDashboard::new();
        dashboard.add_loss(0.0);
        dashboard.add_loss(5.0);
        dashboard.add_loss(10.0);

        let normalized = dashboard.loss_normalized();
        assert_eq!(normalized.len(), 3);
        assert!((normalized[0] - 0.0).abs() < 1e-6);
        assert!((normalized[1] - 0.5).abs() < 1e-6);
        assert!((normalized[2] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn test_wasm_dashboard_with_options() {
        let opts = WasmDashboardOptions::new().width(1024).height(768);
        let dashboard = WasmDashboard::with_options(opts);
        assert_eq!(dashboard.width(), 1024);
        assert_eq!(dashboard.height(), 768);
    }

    #[test]
    fn test_wasm_dashboard_default() {
        let dashboard = WasmDashboard::default();
        assert_eq!(dashboard.width(), 800);
    }
}

#[cfg(test)]
mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        /// Property: Recording values always increases count
        #[test]
        fn prop_record_increases_count(values in prop::collection::vec(-1e10f64..1e10, 1..100)) {
            let mut collector = WasmMetricsCollector::new();
            for v in &values {
                collector.record_loss(*v);
            }
            // Count should equal valid (non-NaN, non-Inf) values
            let valid_count = values.iter().filter(|v| !v.is_nan() && !v.is_infinite()).count();
            prop_assert_eq!(collector.count(), valid_count);
        }

        /// Property: Mean is always within bounds of recorded values
        #[test]
        fn prop_mean_within_bounds(values in prop::collection::vec(0.0f64..100.0, 2..50)) {
            let mut collector = WasmMetricsCollector::new();
            for v in &values {
                collector.record_loss(*v);
            }

            let mean = collector.loss_mean();
            let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
            let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

            prop_assert!(mean >= min - 1e-6);
            prop_assert!(mean <= max + 1e-6);
        }

        /// Property: Standard deviation is non-negative
        #[test]
        fn prop_std_non_negative(values in prop::collection::vec(0.0f64..100.0, 2..50)) {
            let mut collector = WasmMetricsCollector::new();
            for v in &values {
                collector.record_loss(*v);
            }

            let std = collector.loss_std();
            prop_assert!(std >= 0.0 || std.is_nan());
        }

        /// Property: Normalization always produces values in [0, 1]
        #[test]
        fn prop_normalize_bounded(values in prop::collection::vec(-1000.0f64..1000.0, 2..100)) {
            let normalized = normalize_values(&values);

            for v in &normalized {
                prop_assert!(*v >= 0.0 - 1e-10);
                prop_assert!(*v <= 1.0 + 1e-10);
            }
        }

        /// Property: Normalization preserves length
        #[test]
        fn prop_normalize_preserves_length(values in prop::collection::vec(-1000.0f64..1000.0, 1..100)) {
            let normalized = normalize_values(&values);
            prop_assert_eq!(normalized.len(), values.len());
        }

        /// Property: Dashboard respects max_history
        #[test]
        fn prop_dashboard_max_history(
            max in 5usize..50,
            count in 10usize..200
        ) {
            let mut dashboard = WasmDashboard::new().max_history(max);

            for i in 0..count {
                dashboard.add_loss(i as f64);
            }

            prop_assert!(dashboard.loss_history_len() <= max);
        }

        /// Property: Sparkline length is bounded
        #[test]
        fn prop_sparkline_bounded(values in prop::collection::vec(0.0f64..100.0, 1..200)) {
            let sparkline = generate_sparkline(&values, 20);
            let char_count = sparkline.chars().count();
            prop_assert!(char_count <= 20);
        }

        /// Property: Sparkline chars are valid Unicode blocks
        #[test]
        fn prop_sparkline_valid_chars(values in prop::collection::vec(0.0f64..100.0, 1..50)) {
            let sparkline = generate_sparkline(&values, 20);
            let valid_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];

            for c in sparkline.chars() {
                prop_assert!(valid_chars.contains(&c));
            }
        }

        /// Property: X coordinates span [0, 1]
        #[test]
        fn prop_x_coords_span(len in 2usize..100) {
            let dashboard = WasmDashboard::new();
            let coords = dashboard.x_coordinates(len);

            prop_assert_eq!(coords.len(), len);
            prop_assert!((coords[0] - 0.0).abs() < 1e-10);
            prop_assert!((coords[len - 1] - 1.0).abs() < 1e-10);
        }

        /// Property: Dashboard ignores NaN values
        #[test]
        fn prop_dashboard_ignores_nan(
            valid_count in 1usize..20,
            nan_count in 1usize..10
        ) {
            let mut dashboard = WasmDashboard::new();

            for i in 0..valid_count {
                dashboard.add_loss(i as f64);
            }
            for _ in 0..nan_count {
                dashboard.add_loss(f64::NAN);
            }

            prop_assert_eq!(dashboard.loss_history_len(), valid_count);
        }

        /// Property: JSON state is valid
        #[test]
        fn prop_json_state_valid(values in prop::collection::vec(0.0f64..100.0, 0..50)) {
            let mut dashboard = WasmDashboard::new();
            for v in &values {
                dashboard.add_loss(*v);
            }

            let json = dashboard.state_json();
            let parsed: Result<serde_json::Value, _> = serde_json::from_str(&json);
            prop_assert!(parsed.is_ok());
        }
    }
}
