//! MATLAB-compatible `plot` builtin.

use log::trace;
use runmat_accelerate_api::{self, GpuTensorHandle, ProviderPrecision};
use runmat_builtins::{Tensor, Value};
use runmat_macros::runtime_builtin;
use runmat_plot::gpu::ScalarType;
use runmat_plot::plots::{LineGpuStyle, LinePlot, LineStyle};

use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};

use super::common::numeric_pair;
use super::op_common::line_inputs::NumericInput;
use super::op_common::{apply_axes_target, split_leading_axes_handle};
use super::plotting_error;
use super::state::{
    current_axes_state, current_hold_enabled, next_line_style_for_axes, render_active_plot,
    set_line_style_order_for_axes, PlotRenderOptions,
};
use super::style::{
    looks_like_option_name, marker_metadata_from_appearance, parse_line_style_args,
    value_as_string, LineAppearance, LineStyleParseOptions, MarkerAppearance, MarkerColor,
    MarkerKind, DEFAULT_LINE_MARKER_SIZE,
};
use crate::builtins::plotting::type_resolvers::handle_scalar_type;
use std::convert::TryFrom;

use crate::{BuiltinResult, RuntimeError};

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::plotting::plot")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "plot",
    op_kind: GpuOpKind::PlotRender,
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    // Plotting is a sink: it does not participate in fusion, but it *can* consume GPU-resident
    // tensors directly (zero-copy) when the web renderer shares the provider WGPU context.
    // Do not force implicit gathers here; that defeats GPU-resident workloads.
    residency: ResidencyPolicy::InheritInputs,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes: "Plots are rendered by the host renderer; GPU inputs may be consumed zero-copy when a shared WGPU context is installed.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::plotting::plot")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "plot",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "plot performs I/O and terminates fusion graphs.",
};

const BUILTIN_NAME: &str = "plot";

#[runtime_builtin(
    name = "plot",
    category = "plotting",
    summary = "Create MATLAB-compatible 2-D line plots.",
    keywords = "plot,line,2d,visualization",
    sink = true,
    suppress_auto_output = true,
    type_resolver(handle_scalar_type),
    builtin_path = "crate::builtins::plotting::plot"
)]
pub async fn plot_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
    let (axes_target, args) = split_leading_axes_handle(args, BUILTIN_NAME)?;
    apply_axes_target(axes_target, BUILTIN_NAME)?;
    let (mut series_plans, line_style_order) = parse_series_specs(args)?;
    let axes = current_axes_state().active_index;
    let hold_enabled = current_hold_enabled();
    if let Some(order) = line_style_order.as_ref() {
        apply_line_style_order(&mut series_plans, order);
        set_line_style_order_for_axes(axes, order);
    }
    let opts = PlotRenderOptions {
        title: "Plot",
        x_label: "X",
        y_label: "Y",
        ..Default::default()
    };
    let total = series_plans.len();
    let mut plots: Vec<LinePlot> = Vec::with_capacity(series_plans.len());
    for (series_idx, mut plan) in series_plans.drain(..).enumerate() {
        if !plan.line_style_explicit {
            plan.appearance.line_style = if hold_enabled {
                next_line_style_for_axes(axes)
            } else {
                match series_idx % 4 {
                    0 => LineStyle::Solid,
                    1 => LineStyle::Dashed,
                    2 => LineStyle::Dotted,
                    _ => LineStyle::DashDot,
                }
            };
            plan.line_style_explicit = true;
        }
        let inferred_label = plan
            .label
            .take()
            .or_else(|| plan.source_y_arg_index.and_then(crate::callsite::arg_text));
        let label = inferred_label.unwrap_or_else(|| {
            if total == 1 {
                "Data".to_string()
            } else {
                format!("Series {}", series_idx + 1)
            }
        });
        let SeriesRenderPlan {
            data,
            appearance,
            requires_cpu,
            ..
        } = plan;

        let x_kind = match &data.x {
            NumericInput::Host(_) => "Host",
            NumericInput::Gpu(_) => "Gpu",
        };
        let y_kind = match &data.y {
            NumericInput::Host(_) => "Host",
            NumericInput::Gpu(_) => "Gpu",
        };
        let gpu_pair = data.gpu_handles().map(|(x, y)| {
            format!(
                "x(device_id={}, buffer_id={}, shape={:?}) y(device_id={}, buffer_id={}, shape={:?})",
                x.device_id, x.buffer_id, x.shape, y.device_id, y.buffer_id, y.shape
            )
        });
        trace!(
            "plot: series={} requires_cpu={} inputs=({}, {}) gpu_pair={}",
            series_idx + 1,
            requires_cpu,
            x_kind,
            y_kind,
            gpu_pair
                .as_deref()
                .unwrap_or("<none: at least one input not GPU-resident>")
        );

        if !requires_cpu {
            if let Some((x_gpu, y_gpu)) = data.gpu_handles() {
                match build_line_gpu_plot_async(x_gpu, y_gpu, &label, &appearance).await {
                    Ok(line_plot) => {
                        trace!("plot: series={} used GPU path", series_idx + 1);
                        plots.push(line_plot);
                        continue;
                    }
                    Err(err) => {
                        trace!(
                            "plot: series={} GPU path unavailable: {err}",
                            series_idx + 1
                        );
                    }
                }
            }
        }

        trace!(
            "plot: series={} falling back to CPU gather path",
            series_idx + 1
        );
        let (x_tensor, y_tensor) = data.into_tensors_async("plot").await?;
        let (x_vals, y_vals) = numeric_pair(x_tensor, y_tensor, "plot")?;
        plots.push(build_line_plot(x_vals, y_vals, &label, &appearance)?);
    }

    let mut plots_opt = Some(plots);
    let plot_index_out = std::rc::Rc::new(std::cell::RefCell::new(None));
    let plot_index_slot = std::rc::Rc::clone(&plot_index_out);
    let figure_handle = crate::builtins::plotting::current_figure_handle();
    let render_result = render_active_plot(BUILTIN_NAME, opts, move |figure, axes_index| {
        let plots = plots_opt.take().expect("plot series consumed exactly once");
        for (idx, plot) in plots.into_iter().enumerate() {
            let plot_index = figure.add_line_plot_on_axes(plot, axes_index);
            if idx == 0 {
                *plot_index_slot.borrow_mut() = Some((axes_index, plot_index));
            }
        }
        Ok(())
    });
    let Some((axes_index, plot_index)) = *plot_index_out.borrow() else {
        return render_result.map(|_| f64::NAN);
    };
    let handle = crate::builtins::plotting::state::register_line_handle(
        figure_handle,
        axes_index,
        plot_index,
    );
    if let Err(err) = render_result {
        let lower = err.to_string().to_lowercase();
        if lower.contains("plotting is unavailable") || lower.contains("non-main thread") {
            return Ok(handle);
        }
        return Err(err);
    }
    Ok(handle)
}

fn build_line_plot(
    x: Vec<f64>,
    y: Vec<f64>,
    label: &str,
    appearance: &LineAppearance,
) -> BuiltinResult<LinePlot> {
    let point_count = x.len();
    let mut plot = LinePlot::new(x, y)
        .map_err(|e| plotting_error(BUILTIN_NAME, format!("plot: {e}")))?
        .with_label(label)
        .with_style(
            appearance.color,
            appearance.line_width,
            appearance.line_style,
        );
    apply_marker_metadata(&mut plot, appearance, point_count);
    Ok(plot)
}

fn parse_series_specs(
    args: Vec<Value>,
) -> BuiltinResult<(Vec<SeriesRenderPlan>, Option<Vec<LineStyle>>)> {
    if args.is_empty() {
        return Err(plot_err("expected at least one data series"));
    }
    let mut plans = Vec::new();
    let mut line_style_order: Option<Vec<LineStyle>> = None;
    let mut inline_opts = LineStyleParseOptions::plot();
    inline_opts.forbid_leading_numeric = false;

    let mut idx = 0usize;
    while idx < args.len() {
        let first_val = args[idx].clone();
        idx += 1;
        if !is_numeric_value(&first_val) {
            return Err(plot_err(
                "expected numeric X data before style arguments or options",
            ));
        }

        let shorthand_plot_y = match args.get(idx) {
            None => true,
            Some(Value::String(_) | Value::CharArray(_)) => true,
            Some(next) => !is_numeric_value(next),
        };

        let (x_val, y_val, y_arg_index) = if shorthand_plot_y {
            let y_val = first_val;
            let x_val = infer_plot_x_from_y(&y_val)?;
            (x_val, y_val, idx.saturating_sub(1))
        } else {
            let y_arg_index = idx;
            let y_val = args[idx].clone();
            idx += 1;
            if !is_numeric_value(&y_val) {
                return Err(plot_err("expected numeric Y argument after X data"));
            }
            (first_val, y_val, y_arg_index)
        };

        let series_input = PlotSeriesInput::new(x_val, y_val)?;

        let mut style_tokens = Vec::new();
        loop {
            let should_consume =
                matches!(args.get(idx), Some(Value::String(_) | Value::CharArray(_)));
            if !should_consume {
                break;
            }
            let token = args[idx].clone();
            idx += 1;
            let token_text = value_as_string(&token)
                .ok_or_else(|| plot_err("style tokens must be char arrays or strings"))?;
            let lower = token_text.trim().to_ascii_lowercase();
            style_tokens.push(token);

            if looks_like_option_name(&lower) {
                if idx >= args.len() {
                    return Err(plot_err("name-value arguments must come in pairs"));
                }
                style_tokens.push(args[idx].clone());
                idx += 1;
            }
        }

        let parsed_style = parse_line_style_args(&style_tokens, &inline_opts)?;
        if let Some(order) = parsed_style.line_style_order.clone() {
            line_style_order = Some(order);
        }
        plans.push(SeriesRenderPlan {
            data: series_input,
            appearance: parsed_style.appearance,
            requires_cpu: parsed_style.requires_cpu_fallback,
            line_style_explicit: parsed_style.line_style_explicit,
            label: parsed_style.label.clone(),
            source_y_arg_index: Some(y_arg_index),
        });
    }

    if plans.is_empty() {
        return Err(plot_err("expected at least one X/Y data pair"));
    }

    Ok((plans, line_style_order))
}

fn is_numeric_value(value: &Value) -> bool {
    matches!(
        value,
        Value::Tensor(_) | Value::GpuTensor(_) | Value::Num(_) | Value::Int(_) | Value::Bool(_)
    )
}

fn infer_plot_x_from_y(y: &Value) -> BuiltinResult<Value> {
    let len = match y {
        Value::GpuTensor(handle) => handle.shape.iter().copied().product::<usize>().max(1),
        other => {
            let tensor = Tensor::try_from(other)
                .map_err(|e| plotting_error(BUILTIN_NAME, format!("plot: {e}")))?;
            tensor.data.len().max(1)
        }
    };
    let data = (1..=len).map(|i| i as f64).collect::<Vec<_>>();
    Ok(Value::Tensor(Tensor {
        data,
        shape: vec![len],
        rows: len,
        cols: 1,
        dtype: runmat_builtins::NumericDType::F64,
    }))
}

fn plot_err(msg: impl Into<String>) -> RuntimeError {
    plotting_error(BUILTIN_NAME, format!("plot: {}", msg.into()))
}

fn apply_line_style_order(plans: &mut [SeriesRenderPlan], order: &[LineStyle]) {
    if order.is_empty() {
        return;
    }
    let mut index = 0usize;
    for plan in plans.iter_mut() {
        if !plan.line_style_explicit {
            plan.appearance.line_style = order[index % order.len()];
            index += 1;
        }
    }
}

fn apply_marker_metadata(plot: &mut LinePlot, appearance: &LineAppearance, point_count: usize) {
    let marker = marker_metadata_from_appearance(appearance).or_else(|| {
        if point_count == 1 {
            // MATLAB renders a lone point for plot(x, y) when x/y have length 1. If the user
            // didn't specify a marker, default to a small point marker so the plot isn't blank.
            let mut temp = appearance.clone();
            temp.marker = Some(MarkerAppearance {
                kind: MarkerKind::Point,
                size: Some(DEFAULT_LINE_MARKER_SIZE),
                edge_color: MarkerColor::Auto,
                face_color: MarkerColor::Auto,
            });
            marker_metadata_from_appearance(&temp)
        } else {
            None
        }
    });
    if let Some(marker) = marker {
        plot.set_marker(Some(marker));
    }
}

async fn build_line_gpu_plot_async(
    x: &GpuTensorHandle,
    y: &GpuTensorHandle,
    label: &str,
    appearance: &LineAppearance,
) -> BuiltinResult<LinePlot> {
    let api_provider_present = runmat_accelerate_api::provider().is_some();
    let api_provider_for_x_present = runmat_accelerate_api::provider_for_handle(x).is_some();
    let api_provider_for_y_present = runmat_accelerate_api::provider_for_handle(y).is_some();
    let shared_ctx_present = runmat_plot::shared_wgpu_context().is_some();
    trace!(
        "plot-gpu: attempt label={label:?} x(device_id={}, buffer_id={}, shape={:?}) y(device_id={}, buffer_id={}, shape={:?}) shared_ctx_present={} api_provider_present={} api_provider_for_x_present={} api_provider_for_y_present={}",
        x.device_id,
        x.buffer_id,
        x.shape,
        y.device_id,
        y.buffer_id,
        y.shape,
        shared_ctx_present,
        api_provider_present,
        api_provider_for_x_present,
        api_provider_for_y_present
    );
    let context = crate::builtins::plotting::gpu_helpers::ensure_shared_wgpu_context(BUILTIN_NAME)?;

    let x_ref = match runmat_accelerate_api::export_wgpu_buffer(x) {
        Some(buf) => {
            trace!(
                "plot-gpu: export_wgpu_buffer(X) ok len={} element_size={} precision={:?}",
                buf.len,
                buf.element_size,
                buf.precision
            );
            buf
        }
        None => {
            trace!(
                "plot-gpu: export_wgpu_buffer(X) FAILED (api_provider_present={} api_provider_for_x_present={} x_device_id={})",
                api_provider_present, api_provider_for_x_present, x.device_id
            );
            return Err(plotting_error(
                BUILTIN_NAME,
                "plot: unable to export GPU X data",
            ));
        }
    };
    let y_ref = match runmat_accelerate_api::export_wgpu_buffer(y) {
        Some(buf) => {
            trace!(
                "plot-gpu: export_wgpu_buffer(Y) ok len={} element_size={} precision={:?}",
                buf.len,
                buf.element_size,
                buf.precision
            );
            buf
        }
        None => {
            trace!(
                "plot-gpu: export_wgpu_buffer(Y) FAILED (api_provider_present={} api_provider_for_y_present={} y_device_id={})",
                api_provider_present, api_provider_for_y_present, y.device_id
            );
            return Err(plotting_error(
                BUILTIN_NAME,
                "plot: unable to export GPU Y data",
            ));
        }
    };

    if x_ref.len < 2 {
        return Err(plot_err("inputs must contain at least two elements"));
    }
    if x_ref.len != y_ref.len {
        return Err(plot_err("X and Y inputs must have identical lengths"));
    }
    if x_ref.precision != y_ref.precision {
        return Err(plot_err("X and Y gpuArrays must have matching precision"));
    }
    let len_u32 =
        u32::try_from(x_ref.len).map_err(|_| plot_err("point count exceeds supported range"))?;
    let scalar = ScalarType::from_is_f64(x_ref.precision == ProviderPrecision::F64);

    let inputs = runmat_plot::gpu::line::LineGpuInputs {
        x_buffer: x_ref.buffer.clone(),
        y_buffer: y_ref.buffer.clone(),
        len: len_u32,
        scalar,
    };
    let marker_meta = marker_metadata_from_appearance(appearance);

    let marker_gpu_vertices = if let Some(marker) = marker_meta.as_ref() {
        let marker_params = runmat_plot::gpu::line::LineGpuParams {
            color: marker.face_color,
            half_width_data: 0.0,
            thick: false,
            line_style: LineStyle::Solid,
            marker_size: marker.size.max(1.0),
        };
        Some(
            runmat_plot::gpu::line::pack_marker_vertices_from_xy(
                &context.device,
                &context.queue,
                &inputs,
                &marker_params,
            )
            .map_err(|e| {
                plotting_error(
                    BUILTIN_NAME,
                    format!("plot: failed to build marker vertices: {e}"),
                )
            })?,
        )
    } else {
        None
    };

    let bounds = super::gpu_helpers::gpu_xy_bounds_async(x, y, "plot").await?;
    let gpu_style = LineGpuStyle {
        color: appearance.color,
        line_width: appearance.line_width,
        line_style: appearance.line_style,
        marker: marker_meta.clone(),
    };
    let mut plot = LinePlot::from_gpu_xy(inputs, gpu_style, bounds, marker_gpu_vertices);
    plot = plot.with_label(label);
    Ok(plot)
}

#[derive(Debug)]
struct PlotSeriesInput {
    x: NumericInput,
    y: NumericInput,
}

#[derive(Debug)]
struct SeriesRenderPlan {
    data: PlotSeriesInput,
    appearance: LineAppearance,
    requires_cpu: bool,
    line_style_explicit: bool,
    label: Option<String>,
    source_y_arg_index: Option<usize>,
}

impl PlotSeriesInput {
    fn new(x: Value, y: Value) -> BuiltinResult<Self> {
        Ok(Self {
            x: NumericInput::from_value(x, BUILTIN_NAME)?,
            y: NumericInput::from_value(y, BUILTIN_NAME)?,
        })
    }

    fn gpu_handles(&self) -> Option<(&GpuTensorHandle, &GpuTensorHandle)> {
        match (self.x.gpu_handle(), self.y.gpu_handle()) {
            (Some(x), Some(y)) => Some((x, y)),
            _ => None,
        }
    }

    async fn into_tensors_async(self, name: &'static str) -> BuiltinResult<(Tensor, Tensor)> {
        let x = self.x.into_tensor_async(name).await?;
        let y = self.y.into_tensor_async(name).await?;
        Ok((x, y))
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::builtins::plotting::get::get_builtin;
    use crate::builtins::plotting::set::set_builtin;
    use crate::builtins::plotting::state::PlotTestLockGuard;
    use crate::builtins::plotting::state::{
        clear_figure, current_axes_handle_for_figure, reset_hold_state_for_run,
    };
    use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
    use crate::builtins::plotting::{clone_figure, configure_subplot, current_figure_handle};
    use crate::RuntimeError;
    use futures::executor::block_on;
    use runmat_builtins::{ResolveContext, Type};
    use runmat_plot::plots::PlotElement;

    fn setup_plot_tests() -> PlotTestLockGuard {
        let guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);
        guard
    }

    fn tensor_from(data: &[f64]) -> Tensor {
        Tensor {
            data: data.to_vec(),
            shape: vec![data.len()],
            rows: data.len(),
            cols: 1,
            dtype: runmat_builtins::NumericDType::F64,
        }
    }

    fn tensor_with_shape(data: &[f64], shape: Vec<usize>) -> Tensor {
        Tensor::new(data.to_vec(), shape).unwrap()
    }

    fn assert_plotting_unavailable(err: &RuntimeError) {
        let lower = err.to_string().to_lowercase();
        assert!(
            lower.contains("plotting is unavailable") || lower.contains("non-main thread"),
            "unexpected error: {err}"
        );
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn build_plot_requires_equal_lengths() {
        let _guard = setup_plot_tests();
        assert!(build_line_plot(
            vec![1.0, 2.0],
            vec![1.0],
            "Series 1",
            &LineAppearance::default()
        )
        .is_err());
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn plot_builtin_produces_figure_even_without_backend() {
        let _guard = setup_plot_tests();
        let result = block_on(plot_builtin(vec![
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::Tensor(tensor_from(&[0.0, 1.0])),
        ]));
        if let Err(flow) = result {
            assert_plotting_unavailable(&flow);
        }
    }

    #[test]
    fn plot_builtin_accepts_matching_numel_vectors_with_different_shapes() {
        let _guard = setup_plot_tests();
        let cases = [
            (
                tensor_with_shape(&[1.0, 2.0, 3.0, 4.0], vec![1, 4]),
                tensor_with_shape(&[10.0, 20.0, 30.0, 40.0], vec![4, 1]),
            ),
            (
                tensor_with_shape(&[1.0, 2.0, 3.0, 4.0], vec![4, 1]),
                tensor_with_shape(&[10.0, 20.0, 30.0, 40.0], vec![1, 4]),
            ),
        ];

        for (x, y) in cases {
            let _ = clear_figure(None);
            block_on(plot_builtin(vec![Value::Tensor(x), Value::Tensor(y)]))
                .expect("plot should accept row/column vector pairs with matching numel");

            let fig = clone_figure(current_figure_handle()).expect("figure exists");
            let PlotElement::Line(line) = fig.plots().next().expect("plot created") else {
                panic!("expected line plot");
            };
            assert_eq!(line.x_data, vec![1.0, 2.0, 3.0, 4.0]);
            assert_eq!(line.y_data, vec![10.0, 20.0, 30.0, 40.0]);
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn plot_builtin_infers_label_from_callsite() {
        let _guard = setup_plot_tests();
        let source = "plot(a, b);";
        let _source_guard = crate::source_context::replace_current_source(Some(source));
        let spans = vec![
            runmat_hir::Span { start: 5, end: 6 }, // "a"
            runmat_hir::Span { start: 8, end: 9 }, // "b"
        ];
        let _callsite_guard = crate::callsite::push_callsite(None, Some(spans));

        let _ = block_on(plot_builtin(vec![
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::Tensor(tensor_from(&[0.0, 1.0])),
        ]));

        let handle = current_figure_handle();
        let fig = clone_figure(handle).expect("figure exists");
        let entries = fig.legend_entries();
        assert!(!entries.is_empty(), "expected legend entries");
        assert_eq!(entries[0].label, "b");
    }

    #[test]
    fn plot_returns_handle_and_supports_line_properties() {
        let _guard = setup_plot_tests();
        let handle = block_on(plot_builtin(vec![
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::Tensor(tensor_from(&[1.0, 2.0])),
        ]))
        .expect("plot should return a handle");
        assert_eq!(
            get_builtin(vec![Value::Num(handle), Value::String("Type".into())]).unwrap(),
            Value::String("line".into())
        );
        set_builtin(vec![
            Value::Num(handle),
            Value::String("LineWidth".into()),
            Value::Num(2.5),
            Value::String("DisplayName".into()),
            Value::String("Series A".into()),
        ])
        .unwrap();
        assert_eq!(
            get_builtin(vec![Value::Num(handle), Value::String("LineWidth".into())]).unwrap(),
            Value::Num(2.5)
        );
        assert_eq!(
            get_builtin(vec![
                Value::Num(handle),
                Value::String("DisplayName".into())
            ])
            .unwrap(),
            Value::String("Series A".into())
        );
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn parse_series_specs_handles_interleaved_styles() {
        let _guard = setup_plot_tests();
        let args = vec![
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::Tensor(tensor_from(&[1.0, 2.0])),
            Value::String("r".into()),
            Value::Tensor(tensor_from(&[2.0, 3.0])),
            Value::Tensor(tensor_from(&[3.0, 4.0])),
            Value::String("--".into()),
        ];
        let (plans, order) = parse_series_specs(args).expect("series parsed");
        assert_eq!(plans.len(), 2);
        assert!(order.is_none());
        assert_eq!(plans[1].appearance.line_style, LineStyle::Dashed);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn parse_series_specs_errors_on_incomplete_pair() {
        let _guard = setup_plot_tests();
        let args = vec![
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::String("linewidth".into()),
        ];
        let err = parse_series_specs(args).unwrap_err();
        assert!(err
            .to_string()
            .contains("name-value arguments must come in pairs"));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn parse_series_specs_rejects_style_before_data() {
        let _guard = setup_plot_tests();
        let args = vec![Value::String("linewidth".into()), Value::Num(2.0)];
        let err = parse_series_specs(args).unwrap_err();
        assert!(err.to_string().contains("expected numeric X data"));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn parse_series_specs_extracts_line_style_order() {
        let _guard = setup_plot_tests();
        let args = vec![
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::Tensor(tensor_from(&[1.0, 2.0])),
            Value::String("LineStyleOrder".into()),
            Value::StringArray(runmat_builtins::StringArray {
                data: vec!["--".into(), ":".into()],
                shape: vec![1, 2],
                rows: 1,
                cols: 2,
            }),
        ];
        let (mut plans, order) = parse_series_specs(args).expect("parsed");
        assert!(plans.len() == 1);
        assert!(order.is_some());
        apply_line_style_order(&mut plans, order.as_ref().unwrap());
        assert_eq!(plans[0].appearance.line_style, LineStyle::Dashed);
    }

    #[test]
    fn plot_type_is_numeric_handle() {
        assert_eq!(
            handle_scalar_type(
                &[Type::tensor(), Type::tensor()],
                &ResolveContext::new(Vec::new())
            ),
            Type::Num
        );
    }

    #[test]
    fn parse_series_specs_supports_plot_y_shorthand() {
        let _guard = setup_plot_tests();
        let args = vec![Value::Tensor(tensor_from(&[10.0, 20.0, 30.0]))];
        let (plans, order) = parse_series_specs(args).expect("parsed");
        assert!(order.is_none());
        assert_eq!(plans.len(), 1);
        let (x_tensor, y_tensor) = futures::executor::block_on(
            plans
                .into_iter()
                .next()
                .unwrap()
                .data
                .into_tensors_async("plot"),
        )
        .unwrap();
        assert_eq!(x_tensor.data, vec![1.0, 2.0, 3.0]);
        assert_eq!(y_tensor.data, vec![10.0, 20.0, 30.0]);
    }

    #[test]
    fn parse_series_specs_supports_plot_y_with_style() {
        let _guard = setup_plot_tests();
        let args = vec![
            Value::Tensor(tensor_from(&[1.0, 2.0])),
            Value::String("r--".into()),
        ];
        let (plans, order) = parse_series_specs(args).expect("parsed");
        assert!(order.is_none());
        assert_eq!(plans.len(), 1);
        assert_eq!(plans[0].appearance.line_style, LineStyle::Dashed);
    }

    #[test]
    fn plot_builtin_supports_plot_y_shorthand_end_to_end() {
        let _guard = setup_plot_tests();
        let result = block_on(plot_builtin(vec![Value::Tensor(tensor_from(&[
            5.0, 6.0, 7.0,
        ]))]));
        if let Err(flow) = result {
            assert_plotting_unavailable(&flow);
        }
        let handle = current_figure_handle();
        let fig = clone_figure(handle).expect("figure exists");
        let first_plot = fig.plots().next().expect("plot created");
        let PlotElement::Line(line) = first_plot else {
            panic!("expected line plot");
        };
        assert_eq!(line.x_data, vec![1.0, 2.0, 3.0]);
        assert_eq!(line.y_data, vec![5.0, 6.0, 7.0]);
    }

    #[test]
    fn plot_builtin_accepts_leading_axes_handle() {
        let _guard = setup_plot_tests();
        configure_subplot(1, 2, 1).unwrap();
        let fig_handle = current_figure_handle();
        let ax = current_axes_handle_for_figure(fig_handle).unwrap();
        block_on(plot_builtin(vec![
            Value::Num(ax),
            Value::Tensor(tensor_from(&[0.0, 1.0])),
            Value::Tensor(tensor_from(&[1.0, 2.0])),
        ]))
        .unwrap();
        let fig = clone_figure(fig_handle).unwrap();
        assert_eq!(fig.plot_axes_indices(), &[1]);
    }

    #[test]
    fn plot_builtin_accepts_scalar_point_with_style_args() {
        let _guard = setup_plot_tests();
        block_on(plot_builtin(vec![
            Value::Num(0.0),
            Value::Num(1.5),
            Value::String("o".into()),
            Value::String("LineWidth".into()),
            Value::Num(1.5),
        ]))
        .unwrap();
        let fig = clone_figure(current_figure_handle()).unwrap();
        let PlotElement::Line(line) = fig.plots().next().unwrap() else {
            panic!("expected line")
        };
        assert_eq!(line.x_data, vec![0.0]);
        assert_eq!(line.y_data, vec![1.5]);
    }
}
