//! Content-driven layout system for plot element positioning.
//!
//! This module provides a layout calculator that computes element positions
//! based on actual content measurements rather than arbitrary margin percentages.
//!
//! # Key Insight
//!
//! Margins should be computed FROM text measurements, not the other way around.
//!
//! # Layout Algorithm
//!
//! 1. **Measure content** - Estimate or measure all text elements
//! 2. **Calculate margins** - Margins = content size + padding
//! 3. **Position elements** - Each element adjacent to its neighbor
//! 4. **Center the plot** - Distribute extra space symmetrically

use crate::core::{RenderScale, SpacingConfig, TypographyConfig};

// =============================================================================
// Data Structures
// =============================================================================

/// Position for a text element
#[derive(Debug, Clone, PartialEq)]
pub struct TextPosition {
    /// Horizontal position (center for centered text, left edge otherwise)
    pub x: f32,
    /// Vertical position (top for horizontal text, center for rotated)
    pub y: f32,
    /// Font size in pixels
    pub size: f32,
}

/// Computed margins in pixels (for debugging and inspection)
#[derive(Debug, Clone, PartialEq)]
pub struct ComputedMarginsPixels {
    pub left: f32,
    pub right: f32,
    pub top: f32,
    pub bottom: f32,
}

/// A rectangle representing the plot area
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LayoutRect {
    pub left: f32,
    pub top: f32,
    pub right: f32,
    pub bottom: f32,
}

impl LayoutRect {
    pub fn width(&self) -> f32 {
        self.right - self.left
    }

    pub fn height(&self) -> f32 {
        self.bottom - self.top
    }

    pub fn center_x(&self) -> f32 {
        (self.left + self.right) / 2.0
    }

    pub fn center_y(&self) -> f32 {
        (self.top + self.bottom) / 2.0
    }
}

/// Complete layout with computed positions for all plot elements
#[derive(Debug, Clone, PartialEq)]
pub struct PlotLayout {
    /// The plotting area where data is drawn
    pub plot_area: LayoutRect,

    /// Title position (top center point), None if no title
    pub title_pos: Option<TextPosition>,

    /// X-axis label position (top center point)
    pub xlabel_pos: Option<TextPosition>,

    /// Y-axis label position (center point, rotated 90° CCW)
    pub ylabel_pos: Option<TextPosition>,

    /// Y-coordinate for x-axis tick label top positions
    pub xtick_baseline_y: f32,

    /// X-coordinate for right edge of y-axis tick labels
    pub ytick_right_x: f32,

    /// Computed margins in pixels (for debugging/inspection)
    pub margins: ComputedMarginsPixels,
}

/// Content information needed for layout calculation
#[derive(Debug, Clone)]
pub struct PlotContent {
    pub title: Option<String>,
    pub xlabel: Option<String>,
    pub ylabel: Option<String>,
    /// Whether layout should reserve space for tick labels.
    pub show_tick_labels: bool,
    /// Maximum number of characters in y-tick labels (for width estimation)
    pub max_ytick_chars: usize,
    /// Compatibility-only x-tick estimate. Current layout ignores character
    /// count here because x-tick spacing is driven by measured/estimated height.
    pub max_xtick_chars: usize,
}

impl Default for PlotContent {
    fn default() -> Self {
        Self {
            title: None,
            xlabel: None,
            ylabel: None,
            show_tick_labels: true,
            max_ytick_chars: 0,
            max_xtick_chars: 0,
        }
    }
}

impl PlotContent {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    pub fn with_xlabel(mut self, label: impl Into<String>) -> Self {
        self.xlabel = Some(label.into());
        self
    }

    pub fn with_ylabel(mut self, label: impl Into<String>) -> Self {
        self.ylabel = Some(label.into());
        self
    }

    #[deprecated(
        since = "0.3.6",
        note = "x-tick character counts are currently ignored; use with_ytick_chars() instead"
    )]
    pub fn with_tick_chars(self, max_ytick: usize, _max_xtick: usize) -> Self {
        self.with_ytick_chars(max_ytick)
    }

    /// Set the y-tick label width estimate used when actual measurements are unavailable.
    pub fn with_ytick_chars(mut self, max_ytick: usize) -> Self {
        self.max_ytick_chars = max_ytick;
        self
    }

    pub fn with_tick_labels(mut self, show_tick_labels: bool) -> Self {
        self.show_tick_labels = show_tick_labels;
        self
    }
}

/// Optional pre-measured text dimensions `(width, height)` in pixels.
///
/// `ylabel` is measured in its unrotated orientation; layout uses its measured
/// height as horizontal footprint because ylabel is rendered rotated.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MeasuredDimensions {
    pub title: Option<(f32, f32)>,
    pub xlabel: Option<(f32, f32)>,
    pub ylabel: Option<(f32, f32)>,
    pub xtick: Option<(f32, f32)>,
    pub ytick: Option<(f32, f32)>,
    pub right_margin: Option<f32>,
}

// =============================================================================
// Text Size Estimation
// =============================================================================

/// Estimate text width in pixels based on character count and font size
///
/// Uses a conservative estimate of 0.6 * font_size per character,
/// which works well for most sans-serif fonts.
pub fn estimate_text_width(text: &str, font_size_px: f32) -> f32 {
    let char_width = font_size_px * 0.6;
    text.chars().count() as f32 * char_width
}

/// Estimate text height in pixels based on font size
///
/// Returns font_size * 1.2 to account for line height and descenders.
pub fn estimate_text_height(font_size_px: f32) -> f32 {
    font_size_px * 1.2
}

/// Estimate maximum width of tick labels in pixels
///
/// Uses the maximum character count to estimate width.
pub fn estimate_tick_label_width(max_chars: usize, font_size_px: f32) -> f32 {
    let chars = max_chars.max(3); // Minimum 3 chars (e.g., "0.0")
    estimate_text_width(&"X".repeat(chars), font_size_px)
}

// =============================================================================
// Layout Calculator
// =============================================================================

/// Configuration for the layout calculator
#[derive(Debug, Clone)]
pub struct LayoutConfig {
    /// Small buffer from canvas edges (in points)
    pub edge_buffer_pt: f32,
    /// Whether to center the plot by distributing extra space
    pub center_plot: bool,
    /// Maximum margin as fraction of dimension (prevents overflow)
    pub max_margin_fraction: f32,
}

impl Default for LayoutConfig {
    fn default() -> Self {
        Self {
            edge_buffer_pt: 5.0, // Tight default edge buffer
            center_plot: true,
            max_margin_fraction: 0.4, // Max 40% of dimension for any margin
        }
    }
}

/// Calculator for content-driven plot layout
#[derive(Default)]
pub struct LayoutCalculator {
    pub config: LayoutConfig,
}

impl LayoutCalculator {
    pub fn new(config: LayoutConfig) -> Self {
        Self { config }
    }

    /// Compute the complete layout for a plot
    ///
    /// # Arguments
    ///
    /// * `canvas_size` - Width and height in pixels
    /// * `content` - Information about plot content (title, labels, etc.)
    /// * `typography` - Font size configuration
    /// * `spacing` - Padding configuration
    /// * `dpi` - Dots per inch for unit conversion
    /// * `measurements` - Optional pre-measured text dimensions in pixels
    pub fn compute(
        &self,
        canvas_size: (u32, u32),
        content: &PlotContent,
        typography: &TypographyConfig,
        spacing: &SpacingConfig,
        dpi: f32,
        measurements: Option<&MeasuredDimensions>,
    ) -> PlotLayout {
        let (canvas_width, canvas_height) = (canvas_size.0 as f32, canvas_size.1 as f32);
        let render_scale = RenderScale::from_canvas_size(canvas_size.0, canvas_size.1, dpi);

        let edge_buffer = render_scale.points_to_pixels(self.config.edge_buffer_pt);
        let title_pad = render_scale.points_to_pixels(spacing.title_pad);
        let label_pad = render_scale.points_to_pixels(spacing.label_pad);
        let tick_pad = render_scale.points_to_pixels(spacing.tick_pad);

        // Get font sizes in pixels
        let title_size_px = render_scale.points_to_pixels(typography.title_size());
        let label_size_px = render_scale.points_to_pixels(typography.label_size());
        let tick_size_px = render_scale.points_to_pixels(typography.tick_size());

        // Step 1: Measure/estimate content sizes
        let measured_title = measurements.and_then(|m| m.title);
        let measured_xlabel = measurements.and_then(|m| m.xlabel);
        let measured_ylabel = measurements.and_then(|m| m.ylabel);
        let measured_xtick = measurements.and_then(|m| m.xtick);
        let measured_ytick = measurements.and_then(|m| m.ytick);
        let measured_right_margin = measurements.and_then(|m| m.right_margin);

        let title_height = if content.title.is_some() {
            measured_title
                .map(|(_, h)| h)
                .unwrap_or_else(|| estimate_text_height(title_size_px))
        } else {
            0.0
        };

        let xlabel_height = if content.xlabel.is_some() {
            measured_xlabel
                .map(|(_, h)| h)
                .unwrap_or_else(|| estimate_text_height(label_size_px))
        } else {
            0.0
        };

        let ylabel_width = if content.ylabel.is_some() {
            // Rotated text: height becomes width
            measured_ylabel
                .map(|(_, h)| h)
                .unwrap_or_else(|| estimate_text_height(label_size_px))
        } else {
            0.0
        };

        let (xtick_height, ytick_width, tick_pad) = if content.show_tick_labels {
            (
                measured_xtick
                    .map(|(_, h)| h)
                    .unwrap_or_else(|| estimate_text_height(tick_size_px)),
                measured_ytick.map(|(w, _)| w).unwrap_or_else(|| {
                    estimate_tick_label_width(
                        content.max_ytick_chars.max(5), // Default to 5 chars if not specified
                        tick_size_px,
                    )
                }),
                tick_pad,
            )
        } else {
            (0.0, 0.0, 0.0)
        };

        // Step 2: Calculate minimum required margins
        let mut min_top = edge_buffer;
        if content.title.is_some() {
            min_top += title_height + title_pad;
        }

        let mut min_bottom = edge_buffer + xtick_height + tick_pad;
        if content.xlabel.is_some() {
            min_bottom += xlabel_height + label_pad;
        }

        let mut min_left = edge_buffer + ytick_width + tick_pad;
        if content.ylabel.is_some() {
            min_left += ylabel_width + label_pad;
        }

        let min_right = measured_right_margin
            .unwrap_or(edge_buffer)
            .max(edge_buffer);

        // Clamp margins to max fraction of dimension
        let max_h_margin = canvas_width * self.config.max_margin_fraction;
        let max_v_margin = canvas_height * self.config.max_margin_fraction;

        let final_top = min_top.min(max_v_margin);
        let final_bottom = min_bottom.min(max_v_margin);
        let final_left = min_left.min(max_h_margin);
        let mut final_right = min_right.min(max_h_margin);

        // Step 3: Center the chart area in the canvas when requested.
        // Add extra right margin to balance the left margin (ylabel + ticks),
        // which keeps the plotting area itself centered on the canvas.
        if self.config.center_plot {
            let extra_right = (final_left - final_right).max(0.0);
            final_right += extra_right;
        }

        // Step 4: Compute plot area
        let plot_area = LayoutRect {
            left: final_left,
            top: final_top,
            right: canvas_width - final_right,
            bottom: canvas_height - final_bottom,
        };

        // Step 5: Position elements - centered on PLOT AREA, not canvas
        // Note: y positions are the TOP of the text rendering area.
        let title_pos = content.title.as_ref().map(|_| TextPosition {
            x: plot_area.center_x(), // Centered on plot area
            y: edge_buffer,          // Title top at edge buffer
            size: title_size_px,
        });

        let xlabel_pos = content.xlabel.as_ref().map(|_| TextPosition {
            x: plot_area.center_x(),                        // Centered on plot area
            y: canvas_height - edge_buffer - xlabel_height, // X-label top position
            size: label_size_px,
        });

        let ylabel_pos = content.ylabel.as_ref().map(|_| TextPosition {
            x: edge_buffer + ylabel_width / 2.0, // In left margin
            y: plot_area.center_y(),             // Vertically centered on plot
            size: label_size_px,
        });

        // Position tick labels just below/left of the plot area with small padding
        // y positions are the TOP of the text rendering area
        let xtick_baseline_y = plot_area.bottom + tick_pad;
        let ytick_right_x = plot_area.left - tick_pad;

        PlotLayout {
            plot_area,
            title_pos,
            xlabel_pos,
            ylabel_pos,
            xtick_baseline_y,
            ytick_right_x,
            margins: ComputedMarginsPixels {
                left: final_left,
                right: final_right,
                top: final_top,
                bottom: final_bottom,
            },
        }
    }
}

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

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

    fn default_typography() -> TypographyConfig {
        TypographyConfig::default()
    }

    fn default_spacing() -> SpacingConfig {
        SpacingConfig::default()
    }

    #[test]
    fn test_with_ytick_chars_sets_only_ytick_estimate() {
        let content = PlotContent::new().with_ytick_chars(7);

        assert_eq!(content.max_ytick_chars, 7);
        assert_eq!(content.max_xtick_chars, 0);
    }

    #[test]
    fn test_with_tick_chars_compatibility_matches_ytick_only_layout() {
        let calculator = LayoutCalculator::default();
        #[allow(deprecated)]
        let compatibility_content = PlotContent::new().with_tick_chars(6, 42);
        let ytick_only_content = PlotContent::new().with_ytick_chars(6);

        assert_eq!(compatibility_content.max_ytick_chars, 6);
        assert_eq!(compatibility_content.max_xtick_chars, 0);

        let compatibility_layout = calculator.compute(
            (640, 480),
            &compatibility_content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );
        let ytick_only_layout = calculator.compute(
            (640, 480),
            &ytick_only_content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        assert_eq!(compatibility_layout, ytick_only_layout);
    }

    #[test]
    fn test_estimate_text_width() {
        let width = estimate_text_width("Hello", 12.0);
        // 5 chars * 12 * 0.6 = 36
        assert!((width - 36.0).abs() < 0.1);
    }

    #[test]
    fn test_estimate_text_height() {
        let height = estimate_text_height(14.0);
        // 14 * 1.2 = 16.8
        assert!((height - 16.8).abs() < 0.1);
    }

    #[test]
    fn test_layout_all_elements() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("Test Title")
            .with_xlabel("X Values")
            .with_ylabel("Y Values")
            .with_ytick_chars(5);

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // Plot area should have positive dimensions
        assert!(layout.plot_area.width() > 0.0);
        assert!(layout.plot_area.height() > 0.0);

        // Title should be present and near top
        assert!(layout.title_pos.is_some());
        let title = layout.title_pos.unwrap();
        assert!(title.y < 50.0); // Near top

        // Labels should be present
        assert!(layout.xlabel_pos.is_some());
        assert!(layout.ylabel_pos.is_some());
    }

    #[test]
    fn test_layout_horizontal_text_positions_use_top_origin() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("Baseline Title")
            .with_xlabel("Baseline X");
        let typography = default_typography();
        let spacing = default_spacing();

        let layout = calculator.compute((640, 480), &content, &typography, &spacing, 100.0, None);

        let pt_to_px = |pt: f32| pt * 100.0 / 72.0;
        let edge_buffer = pt_to_px(LayoutConfig::default().edge_buffer_pt);
        let expected_title_top = edge_buffer;
        let expected_xlabel_top =
            480.0 - edge_buffer - estimate_text_height(pt_to_px(typography.label_size()));

        let title = layout.title_pos.expect("title should be present");
        let xlabel = layout.xlabel_pos.expect("xlabel should be present");

        assert!((title.y - expected_title_top).abs() < 1.0);
        assert!((xlabel.y - expected_xlabel_top).abs() < 1.0);
    }

    #[test]
    fn test_layout_uses_measured_dimensions_when_provided() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("Measured Title")
            .with_xlabel("Measured X")
            .with_ylabel("Measured Y");

        let estimated = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        let measured_dims = MeasuredDimensions {
            title: Some((180.0, 42.0)),
            xlabel: Some((120.0, 34.0)),
            ylabel: Some((140.0, 50.0)),
            xtick: None,
            ytick: None,
            right_margin: None,
        };
        let measured = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            Some(&measured_dims),
        );

        assert!(measured.margins.top > estimated.margins.top);
        assert!(measured.margins.bottom > estimated.margins.bottom);
        assert!(measured.margins.left > estimated.margins.left);
    }

    #[test]
    fn test_layout_empty_measurements_match_estimates() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("Title")
            .with_xlabel("X")
            .with_ylabel("Y");

        let estimated = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );
        let empty = MeasuredDimensions::default();
        let with_empty = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            Some(&empty),
        );

        assert_eq!(estimated, with_empty);
    }

    #[test]
    fn test_layout_no_title() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new().with_xlabel("X").with_ylabel("Y");

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // No title position
        assert!(layout.title_pos.is_none());

        // Top margin should be smaller
        assert!(layout.margins.top < 50.0);
    }

    #[test]
    fn test_layout_no_labels() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new().with_ytick_chars(5);

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // No title or label positions
        assert!(layout.title_pos.is_none());
        assert!(layout.xlabel_pos.is_none());
        assert!(layout.ylabel_pos.is_none());

        // Plot area should be maximized
        assert!(layout.plot_area.width() > 500.0);
        assert!(layout.plot_area.height() > 400.0);
    }

    #[test]
    fn test_layout_without_tick_labels_reclaims_tick_margins() {
        let calculator = LayoutCalculator::default();
        let with_ticks = PlotContent::new()
            .with_xlabel("X Axis")
            .with_ylabel("Y Axis")
            .with_ytick_chars(6);
        let without_ticks = with_ticks.clone().with_tick_labels(false);

        let layout_with_ticks = calculator.compute(
            (640, 480),
            &with_ticks,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );
        let layout_without_ticks = calculator.compute(
            (640, 480),
            &without_ticks,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        assert!(layout_without_ticks.margins.bottom < layout_with_ticks.margins.bottom);
        assert!(layout_without_ticks.margins.left < layout_with_ticks.margins.left);
        assert!(layout_without_ticks.plot_area.width() > layout_with_ticks.plot_area.width());
        assert!(layout_without_ticks.plot_area.height() > layout_with_ticks.plot_area.height());
    }

    #[test]
    fn test_layout_respects_edge_buffer() {
        let calculator = LayoutCalculator::new(LayoutConfig {
            edge_buffer_pt: 10.0,
            ..Default::default()
        });
        let content = PlotContent::new();

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // At 100 DPI, 10pt = ~14px
        let expected_buffer = 10.0 * 100.0 / 72.0;
        assert!(layout.margins.right >= expected_buffer - 1.0);
    }

    #[test]
    fn test_layout_measured_right_margin_keeps_edge_buffer_floor() {
        let calculator = LayoutCalculator::new(LayoutConfig {
            edge_buffer_pt: 10.0,
            center_plot: false,
            ..Default::default()
        });
        let content = PlotContent::new();
        let measured = MeasuredDimensions {
            right_margin: Some(2.0),
            ..Default::default()
        };

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            Some(&measured),
        );

        let expected_buffer = 10.0 * 100.0 / 72.0;
        assert!(
            layout.margins.right >= expected_buffer - 1.0,
            "measured right margins should not shrink below the configured edge buffer"
        );
    }

    #[test]
    fn test_layout_centering() {
        let calculator = LayoutCalculator::new(LayoutConfig {
            center_plot: true,
            ..Default::default()
        });
        let content = PlotContent::new()
            .with_ylabel("Y Label")
            .with_ytick_chars(5);

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        let margin_diff = (layout.margins.right - layout.margins.left).abs();
        assert!(
            margin_diff < 1.0,
            "Margins should be equal: left={}, right={}",
            layout.margins.left,
            layout.margins.right
        );

        let plot_center = layout.plot_area.center_x();
        let canvas_center = 640.0 / 2.0;
        let center_diff = (plot_center - canvas_center).abs();
        assert!(
            center_diff < 1.0,
            "Plot should be centered: plot_center={}, canvas_center={}",
            plot_center,
            canvas_center
        );
    }

    #[test]
    fn test_layout_keeps_asymmetric_margins_when_centering_disabled() {
        let calculator = LayoutCalculator::new(LayoutConfig {
            center_plot: false,
            ..Default::default()
        });
        let content = PlotContent::new()
            .with_ylabel("Y Label")
            .with_ytick_chars(5);

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        assert!(
            layout.margins.left > layout.margins.right,
            "left margin should grow for ylabel/ticks without mirroring to the right"
        );

        let plot_center = layout.plot_area.center_x();
        let canvas_center = 640.0 / 2.0;
        let center_diff = plot_center - canvas_center;
        assert!(
            center_diff > 0.0,
            "plot area center should move right when the left margin grows without right mirroring: plot_center={}, canvas_center={}",
            plot_center,
            canvas_center
        );
    }

    #[test]
    fn test_layout_uses_measured_tick_dimensions_when_provided() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new().with_ytick_chars(5);

        let estimated = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        let measured_dims = MeasuredDimensions {
            xtick: Some((40.0, 28.0)),
            ytick: Some((72.0, 20.0)),
            ..MeasuredDimensions::default()
        };
        let measured = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            Some(&measured_dims),
        );

        assert!(measured.margins.bottom > estimated.margins.bottom);
        assert!(measured.margins.left > estimated.margins.left);
    }

    #[test]
    fn test_layout_margin_clamping() {
        let calculator = LayoutCalculator::new(LayoutConfig {
            max_margin_fraction: 0.3,
            ..Default::default()
        });

        // Very long ylabel that would normally cause huge left margin
        let content = PlotContent::new().with_ylabel("Very Long Y-Axis Label That Would Be Huge");

        let layout = calculator.compute(
            (200, 150), // Small canvas
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // Margin should be clamped to 30% of width = 60px
        assert!(layout.margins.left <= 200.0 * 0.3 + 1.0);
    }

    #[test]
    fn test_layout_very_long_title() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("This is an Extremely Long Title That Should Still Render Properly Without Breaking the Layout")
            .with_xlabel("X Axis")
            .with_ylabel("Y Axis");

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // Plot area should still have positive dimensions
        assert!(layout.plot_area.width() > 0.0);
        assert!(layout.plot_area.height() > 0.0);

        // Title should be present
        assert!(layout.title_pos.is_some());

        // Plot area should not be squished too much
        assert!(layout.plot_area.width() > 300.0);
    }

    #[test]
    fn test_layout_unicode_labels() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("数据分析 - データ分析")
            .with_xlabel("時間 (μs)")
            .with_ylabel("Amplitude (±σ)")
            .with_ytick_chars(6);

        let layout = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // Plot area should still have positive dimensions
        assert!(layout.plot_area.width() > 0.0);
        assert!(layout.plot_area.height() > 0.0);

        // All positions should be present
        assert!(layout.title_pos.is_some());
        assert!(layout.xlabel_pos.is_some());
        assert!(layout.ylabel_pos.is_some());
    }

    #[test]
    fn test_layout_very_small_canvas() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("Title")
            .with_xlabel("X")
            .with_ylabel("Y");

        let layout = calculator.compute(
            (100, 80), // Very small canvas
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        // Even on very small canvas, plot area should be valid
        assert!(layout.plot_area.width() > 0.0);
        assert!(layout.plot_area.height() > 0.0);

        // Margins should be clamped
        assert!(layout.margins.left < 50.0);
        assert!(layout.margins.top < 40.0);
    }

    #[test]
    fn test_layout_high_dpi() {
        let calculator = LayoutCalculator::default();
        let content = PlotContent::new()
            .with_title("High DPI Title")
            .with_xlabel("X Axis")
            .with_ylabel("Y Axis");

        let layout_100dpi = calculator.compute(
            (640, 480),
            &content,
            &default_typography(),
            &default_spacing(),
            100.0,
            None,
        );

        let layout_200dpi = calculator.compute(
            (1280, 960), // Doubled canvas for 2x DPI
            &content,
            &default_typography(),
            &default_spacing(),
            200.0,
            None,
        );

        // At 2x DPI with 2x canvas, proportions should be similar
        let ratio_100 = layout_100dpi.plot_area.width() / 640.0;
        let ratio_200 = layout_200dpi.plot_area.width() / 1280.0;

        // Ratios should be close (within 20%)
        let diff = (ratio_100 - ratio_200).abs() / ratio_100;
        assert!(diff < 0.2, "DPI scaling ratio diff: {}", diff);
    }
}
