//! MATLAB-compatible `patch` builtin.

use glam::{Vec3, Vec4};
use runmat_builtins::{StructValue, Tensor, Value};
use runmat_macros::runtime_builtin;
use runmat_plot::plots::{PatchEdgeColorMode, PatchFaceColorMode, PatchPlot};

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

use super::common::gather_tensor_from_gpu;
use super::op_common::{apply_axes_target, split_leading_axes_handle};
use super::plotting_error;
use super::state::{render_active_plot, PlotRenderOptions};
use super::style::{
    parse_color_value, value_as_bool, value_as_f64, value_as_string, LineStyleParseOptions,
};

const BUILTIN_NAME: &str = "patch";

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::plotting::patch")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "patch",
    op_kind: GpuOpKind::PlotRender,
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::GatherImmediately,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes: "patch is a plotting sink. Initial implementation gathers gpuArray coordinate inputs, triangulates on the host, then renders through the shared GPU renderer.",
};

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

#[derive(Clone, Debug)]
struct PatchOptions {
    x_data: Option<Tensor>,
    y_data: Option<Tensor>,
    z_data: Option<Tensor>,
    faces: Option<Tensor>,
    vertices: Option<Tensor>,
    face_color: Vec4,
    edge_color: Vec4,
    face_color_mode: PatchFaceColorMode,
    edge_color_mode: PatchEdgeColorMode,
    face_alpha: f32,
    edge_alpha: f32,
    line_width: f32,
    label: Option<String>,
    visible: bool,
}

impl Default for PatchOptions {
    fn default() -> Self {
        Self {
            x_data: None,
            y_data: None,
            z_data: None,
            faces: None,
            vertices: None,
            face_color: Vec4::new(0.0, 0.447, 0.741, 1.0),
            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
            face_color_mode: PatchFaceColorMode::Color,
            edge_color_mode: PatchEdgeColorMode::Color,
            face_alpha: 1.0,
            edge_alpha: 1.0,
            line_width: 0.5,
            label: None,
            visible: true,
        }
    }
}

#[runtime_builtin(
    name = "patch",
    category = "plotting",
    summary = "Create MATLAB-compatible colored polygon patches.",
    keywords = "patch,plotting,polygon,faces,vertices",
    sink = true,
    suppress_auto_output = true,
    type_resolver(handle_scalar_type),
    builtin_path = "crate::builtins::plotting::patch"
)]
pub fn patch_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 plot = Some(parse_patch_plot(args)?);
    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,
        PlotRenderOptions {
            title: "Patch",
            x_label: "X",
            y_label: "Y",
            ..Default::default()
        },
        move |figure, axes| {
            let patch = plot.take().expect("patch plot consumed once");
            let plot_index = figure.add_patch_plot_on_axes(patch, axes);
            *plot_index_slot.borrow_mut() = Some((axes, plot_index));
            Ok(())
        },
    );
    let Some((axes, plot_index)) = *plot_index_out.borrow() else {
        return render_result.map(|_| f64::NAN);
    };
    let handle =
        crate::builtins::plotting::state::register_patch_handle(figure_handle, axes, 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)
}

pub(super) fn parse_patch_plot(args: Vec<Value>) -> BuiltinResult<PatchPlot> {
    if args.is_empty() {
        return Err(plotting_error(BUILTIN_NAME, "patch: expected input data"));
    }
    let mut opts = PatchOptions::default();
    let mut remaining = if let Some(Value::Struct(st)) = args.first() {
        apply_struct_options(&mut opts, st)?;
        args[1..].to_vec()
    } else {
        args
    };

    if remaining.first().and_then(value_as_string).is_some() {
        apply_property_pairs(&mut opts, &remaining)?;
    } else {
        apply_positional_data(&mut opts, &mut remaining)?;
        apply_property_pairs(&mut opts, &remaining)?;
    }

    let (vertices, faces) = if let (Some(faces), Some(vertices)) = (&opts.faces, &opts.vertices) {
        (vertices_from_tensor(vertices)?, faces_from_tensor(faces)?)
    } else {
        vertices_faces_from_xyz(&opts)?
    };

    let mut plot = PatchPlot::new(vertices, faces)
        .map_err(|err| plotting_error(BUILTIN_NAME, format!("patch: {err}")))?;
    plot.set_face_color(opts.face_color);
    plot.set_edge_color(opts.edge_color);
    plot.set_face_color_mode(opts.face_color_mode);
    plot.set_edge_color_mode(opts.edge_color_mode);
    plot.set_face_alpha(opts.face_alpha);
    plot.set_edge_alpha(opts.edge_alpha);
    plot.set_line_width(opts.line_width);
    plot.set_label(opts.label);
    plot.set_visible(opts.visible);
    Ok(plot)
}

fn apply_positional_data(opts: &mut PatchOptions, args: &mut Vec<Value>) -> BuiltinResult<()> {
    if args.len() < 2 {
        apply_property_pairs(opts, args)?;
        args.clear();
        return Ok(());
    }
    if args.len() >= 3
        && !is_property_name(&args[2])
        && value_matches_coordinate_values(&args[0], &args[1], &args[2])
        && !is_color_literal(&args[2])
    {
        opts.z_data = Some(tensor_from_value(args.remove(2))?);
    }
    opts.x_data = Some(tensor_from_value(args.remove(0))?);
    opts.y_data = Some(tensor_from_value(args.remove(0))?);

    if args.first().is_none_or(is_property_name) {
        return Ok(());
    }

    if opts.z_data.is_some() {
        apply_color_argument(opts, &args.remove(0));
        return Ok(());
    }

    if args.len() >= 2 && !is_property_name(&args[1]) && !is_color_literal(&args[0]) {
        opts.z_data = Some(tensor_from_value(args.remove(0))?);
        apply_color_argument(opts, &args.remove(0));
    } else {
        let value = args.remove(0);
        if value_matches_coordinate_shape(opts, &value) && !is_color_literal(&value)
            || !apply_color_argument(opts, &value)
        {
            opts.z_data = Some(tensor_from_value(value)?);
        }
    }
    Ok(())
}

fn value_matches_coordinate_values(x: &Value, y: &Value, value: &Value) -> bool {
    let (Ok(x), Ok(y), Ok(tensor)) = (
        Tensor::try_from(x),
        Tensor::try_from(y),
        Tensor::try_from(value),
    ) else {
        return false;
    };
    tensor.rows == x.rows && tensor.cols == x.cols && tensor.rows == y.rows && tensor.cols == y.cols
}

fn value_matches_coordinate_shape(opts: &PatchOptions, value: &Value) -> bool {
    let (Some(x), Some(y)) = (&opts.x_data, &opts.y_data) else {
        return false;
    };
    let Ok(tensor) = Tensor::try_from(value) else {
        return false;
    };
    tensor.rows == x.rows && tensor.cols == x.cols && tensor.rows == y.rows && tensor.cols == y.cols
}

fn is_color_literal(value: &Value) -> bool {
    if value_as_string(value).is_some() {
        return parse_color_value(&LineStyleParseOptions::generic(BUILTIN_NAME), value).is_ok();
    }
    let Ok(tensor) = Tensor::try_from(value) else {
        return false;
    };
    tensor.rows == 1
        && tensor.cols == 3
        && parse_color_value(&LineStyleParseOptions::generic(BUILTIN_NAME), value).is_ok()
}

fn apply_struct_options(opts: &mut PatchOptions, st: &StructValue) -> BuiltinResult<()> {
    for (key, value) in &st.fields {
        apply_property(opts, key, value)?;
    }
    Ok(())
}

fn apply_property_pairs(opts: &mut PatchOptions, args: &[Value]) -> BuiltinResult<()> {
    if args.is_empty() {
        return Ok(());
    }
    if !args.len().is_multiple_of(2) {
        return Err(plotting_error(
            BUILTIN_NAME,
            "patch: property/value arguments must come in pairs",
        ));
    }
    for pair in args.chunks_exact(2) {
        let key = value_as_string(&pair[0])
            .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: property names must be strings"))?;
        apply_property(opts, &key, &pair[1])?;
    }
    Ok(())
}

fn apply_property(opts: &mut PatchOptions, key: &str, value: &Value) -> BuiltinResult<()> {
    match key.trim().to_ascii_lowercase().as_str() {
        "xdata" => opts.x_data = Some(tensor_from_value(value.clone())?),
        "ydata" => opts.y_data = Some(tensor_from_value(value.clone())?),
        "zdata" => opts.z_data = Some(tensor_from_value(value.clone())?),
        "faces" => opts.faces = Some(tensor_from_value(value.clone())?),
        "vertices" => opts.vertices = Some(tensor_from_value(value.clone())?),
        "facecolor" | "color" => apply_face_color(opts, value)?,
        "edgecolor" => apply_edge_color(opts, value)?,
        "facealpha" => {
            opts.face_alpha = value_as_f64(value)
                .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: FaceAlpha must be numeric"))?
                .clamp(0.0, 1.0) as f32;
        }
        "edgealpha" => {
            opts.edge_alpha = value_as_f64(value)
                .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: EdgeAlpha must be numeric"))?
                .clamp(0.0, 1.0) as f32;
        }
        "linewidth" => {
            opts.line_width = value_as_f64(value)
                .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: LineWidth must be numeric"))?
                .max(0.0) as f32;
        }
        "displayname" => opts.label = value_as_string(value),
        "visible" => {
            opts.visible = value_as_bool(value)
                .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: Visible must be on/off"))?;
        }
        _ => {}
    }
    Ok(())
}

fn tensor_from_value(value: Value) -> BuiltinResult<Tensor> {
    match value {
        Value::GpuTensor(handle) => gather_tensor_from_gpu(handle, BUILTIN_NAME),
        other => Tensor::try_from(&other)
            .map_err(|err| plotting_error(BUILTIN_NAME, format!("patch: {err}"))),
    }
}

fn apply_color_argument(opts: &mut PatchOptions, value: &Value) -> bool {
    if let Ok(color) = parse_color_value(&LineStyleParseOptions::generic(BUILTIN_NAME), value) {
        opts.face_color = color;
        opts.face_color_mode = PatchFaceColorMode::Color;
        return true;
    }
    false
}

fn apply_face_color(opts: &mut PatchOptions, value: &Value) -> BuiltinResult<()> {
    if let Some(text) = value_as_string(value) {
        match text.trim().to_ascii_lowercase().as_str() {
            "none" => {
                opts.face_color_mode = PatchFaceColorMode::None;
                return Ok(());
            }
            "flat" | "interp" => {
                opts.face_color_mode = PatchFaceColorMode::Flat;
                return Ok(());
            }
            _ => {}
        }
    }
    opts.face_color = parse_color_value(&LineStyleParseOptions::generic(BUILTIN_NAME), value)?;
    opts.face_color_mode = PatchFaceColorMode::Color;
    Ok(())
}

fn apply_edge_color(opts: &mut PatchOptions, value: &Value) -> BuiltinResult<()> {
    if let Some(text) = value_as_string(value) {
        if text.trim().eq_ignore_ascii_case("none") {
            opts.edge_color_mode = PatchEdgeColorMode::None;
            return Ok(());
        }
    }
    opts.edge_color = parse_color_value(&LineStyleParseOptions::generic(BUILTIN_NAME), value)?;
    opts.edge_color_mode = PatchEdgeColorMode::Color;
    Ok(())
}

fn vertices_from_tensor(tensor: &Tensor) -> BuiltinResult<Vec<Vec3>> {
    if tensor.cols != 2 && tensor.cols != 3 {
        return Err(plotting_error(
            BUILTIN_NAME,
            "patch: Vertices must be an N-by-2 or N-by-3 matrix",
        ));
    }
    let mut out = Vec::with_capacity(tensor.rows);
    for row in 0..tensor.rows {
        let x = tensor.data[row];
        let y = tensor.data[row + tensor.rows];
        let z = if tensor.cols >= 3 {
            tensor.data[row + 2 * tensor.rows]
        } else {
            0.0
        };
        out.push(Vec3::new(x as f32, y as f32, z as f32));
    }
    Ok(out)
}

fn faces_from_tensor(tensor: &Tensor) -> BuiltinResult<Vec<Vec<usize>>> {
    if tensor.rows == 0 || tensor.cols == 0 {
        return Err(plotting_error(
            BUILTIN_NAME,
            "patch: Faces must not be empty",
        ));
    }
    let mut faces = Vec::with_capacity(tensor.rows);
    for row in 0..tensor.rows {
        let mut face = Vec::new();
        for col in 0..tensor.cols {
            let value = tensor.data[row + col * tensor.rows];
            if value.is_nan() {
                continue;
            }
            if value < 1.0 || value.fract() != 0.0 {
                return Err(plotting_error(
                    BUILTIN_NAME,
                    "patch: Faces must contain positive integer vertex indices",
                ));
            }
            face.push(value as usize - 1);
        }
        if face.len() >= 3 {
            faces.push(face);
        }
    }
    Ok(faces)
}

fn vertices_faces_from_xyz(opts: &PatchOptions) -> BuiltinResult<(Vec<Vec3>, Vec<Vec<usize>>)> {
    let x = opts
        .x_data
        .as_ref()
        .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: missing XData"))?;
    let y = opts
        .y_data
        .as_ref()
        .ok_or_else(|| plotting_error(BUILTIN_NAME, "patch: missing YData"))?;
    if x.rows != y.rows || x.cols != y.cols {
        return Err(plotting_error(
            BUILTIN_NAME,
            "patch: XData and YData must have the same size",
        ));
    }
    if let Some(z) = &opts.z_data {
        if z.rows != x.rows || z.cols != x.cols {
            return Err(plotting_error(
                BUILTIN_NAME,
                "patch: ZData must have the same size as XData and YData",
            ));
        }
    }
    if is_vector_tensor(x) && is_vector_tensor(y) {
        let x_values = x.data.clone();
        let y_values = y.data.clone();
        let z_values = opts.z_data.as_ref().map(|z| z.data.clone());
        if x_values.len() != y_values.len()
            || z_values
                .as_ref()
                .map(|z| z.len() != x_values.len())
                .unwrap_or(false)
        {
            return Err(plotting_error(
                BUILTIN_NAME,
                "patch: vector XData, YData, and ZData must have the same length",
            ));
        }
        let mut vertices = Vec::new();
        let mut face = Vec::new();
        for idx in 0..x_values.len() {
            let xv = x_values[idx];
            let yv = y_values[idx];
            let zv = z_values.as_ref().map(|z| z[idx]).unwrap_or(0.0);
            if xv.is_nan() || yv.is_nan() || zv.is_nan() {
                continue;
            }
            face.push(vertices.len());
            vertices.push(Vec3::new(xv as f32, yv as f32, zv as f32));
        }
        return Ok((vertices, vec![face]));
    }
    let mut vertices = Vec::new();
    let mut faces = Vec::new();
    for col in 0..x.cols {
        let mut face = Vec::new();
        for row in 0..x.rows {
            let idx = row + col * x.rows;
            let xv = x.data[idx];
            let yv = y.data[idx];
            let zv = opts.z_data.as_ref().map(|z| z.data[idx]).unwrap_or(0.0);
            if xv.is_nan() || yv.is_nan() || zv.is_nan() {
                continue;
            }
            face.push(vertices.len());
            vertices.push(Vec3::new(xv as f32, yv as f32, zv as f32));
        }
        if face.len() >= 3 {
            faces.push(face);
        }
    }
    Ok((vertices, faces))
}

fn is_vector_tensor(tensor: &Tensor) -> bool {
    tensor.rows == 1 || tensor.cols == 1
}

pub(super) fn is_property_name(value: &Value) -> bool {
    value_as_string(value)
        .map(|name| {
            matches!(
                name.trim().to_ascii_lowercase().as_str(),
                "xdata"
                    | "ydata"
                    | "zdata"
                    | "faces"
                    | "vertices"
                    | "facecolor"
                    | "edgecolor"
                    | "facealpha"
                    | "edgealpha"
                    | "linewidth"
                    | "displayname"
                    | "visible"
                    | "color"
            )
        })
        .unwrap_or(false)
}

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

    fn tensor(rows: usize, cols: usize, data: &[f64]) -> Value {
        Value::Tensor(Tensor {
            rows,
            cols,
            shape: vec![rows, cols],
            data: data.to_vec(),
            dtype: NumericDType::F64,
        })
    }

    #[test]
    fn patch_xyc_vector_builds_single_polygon() {
        let plot = parse_patch_plot(vec![
            tensor(3, 1, &[0.0, 1.0, 0.0]),
            tensor(3, 1, &[0.0, 0.0, 1.0]),
            Value::String("r".into()),
        ])
        .unwrap();
        assert_eq!(plot.faces().len(), 1);
        assert_eq!(plot.vertices().len(), 3);
        assert_eq!(plot.face_color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
    }

    #[test]
    fn patch_xy_rgb_row_vector_treats_third_argument_as_color() {
        let plot = parse_patch_plot(vec![
            tensor(1, 3, &[0.0, 1.0, 0.0]),
            tensor(1, 3, &[0.0, 0.0, 1.0]),
            tensor(1, 3, &[1.0, 0.0, 0.0]),
        ])
        .unwrap();
        assert_eq!(plot.vertices().len(), 3);
        assert!(plot.vertices().iter().all(|vertex| vertex.z == 0.0));
        assert_eq!(plot.face_color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
    }

    #[test]
    fn patch_matrix_columns_build_multiple_polygons() {
        let plot = parse_patch_plot(vec![
            tensor(4, 2, &[0.0, 1.0, 1.0, 0.0, 2.0, 3.0, 3.0, 2.0]),
            tensor(4, 2, &[0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0]),
            Value::String("g".into()),
        ])
        .unwrap();
        assert_eq!(plot.faces().len(), 2);
        assert_eq!(plot.vertices().len(), 8);
    }

    #[test]
    fn patch_xyz_without_color_preserves_z_data() {
        let plot = parse_patch_plot(vec![
            tensor(3, 1, &[0.0, 1.0, 0.0]),
            tensor(3, 1, &[0.0, 0.0, 1.0]),
            tensor(3, 1, &[0.25, 0.5, 0.75]),
        ])
        .unwrap();
        assert_eq!(plot.vertices().len(), 3);
        assert_eq!(plot.vertices()[0].z, 0.25);
        assert_eq!(plot.vertices()[1].z, 0.5);
        assert_eq!(plot.vertices()[2].z, 0.75);
    }

    #[test]
    fn patch_xyz_accepts_trailing_name_value_pairs() {
        let plot = parse_patch_plot(vec![
            tensor(3, 1, &[0.0, 1.0, 0.0]),
            tensor(3, 1, &[0.0, 0.0, 1.0]),
            tensor(3, 1, &[0.25, 0.5, 0.75]),
            Value::String("FaceColor".into()),
            Value::String("r".into()),
        ])
        .unwrap();
        assert_eq!(plot.vertices()[0].z, 0.25);
        assert_eq!(plot.vertices()[1].z, 0.5);
        assert_eq!(plot.vertices()[2].z, 0.75);
        assert_eq!(plot.face_color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
    }

    #[test]
    fn patch_xy_accepts_trailing_name_value_pairs() {
        let plot = parse_patch_plot(vec![
            tensor(3, 1, &[0.0, 1.0, 0.0]),
            tensor(3, 1, &[0.0, 0.0, 1.0]),
            Value::String("FaceColor".into()),
            Value::String("r".into()),
            Value::String("EdgeColor".into()),
            Value::String("none".into()),
        ])
        .unwrap();
        assert_eq!(plot.face_color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
        assert_eq!(plot.edge_color_mode(), PatchEdgeColorMode::None);
    }

    #[test]
    fn patch_faces_vertices_uses_one_based_faces() {
        let plot = parse_patch_plot(vec![
            Value::String("Faces".into()),
            tensor(1, 3, &[1.0, 2.0, 3.0]),
            Value::String("Vertices".into()),
            tensor(3, 2, &[0.0, 1.0, 0.0, 0.0, 0.0, 1.0]),
            Value::String("EdgeColor".into()),
            Value::String("none".into()),
        ])
        .unwrap();
        assert_eq!(plot.faces(), &[vec![0, 1, 2]]);
        assert_eq!(plot.edge_color_mode(), PatchEdgeColorMode::None);
    }

    #[test]
    fn patch_registers_as_dispatch_builtin_and_returns_handle() {
        unsafe {
            std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
        }
        let handle = crate::call_builtin(
            "patch",
            &[
                tensor(3, 1, &[0.0, 1.0, 0.0]),
                tensor(3, 1, &[0.0, 0.0, 1.0]),
                Value::String("b".into()),
            ],
        )
        .expect("patch builtin should dispatch");
        let Value::Num(handle) = handle else {
            panic!("expected numeric graphics handle");
        };
        let ty = crate::call_builtin("get", &[Value::Num(handle), Value::String("Type".into())])
            .expect("get patch type");
        assert_eq!(ty, Value::String("patch".into()));
    }

    #[test]
    fn patch_get_visible_tracks_set_visible() {
        unsafe {
            std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
        }
        let handle = crate::call_builtin(
            "patch",
            &[
                tensor(3, 1, &[0.0, 1.0, 0.0]),
                tensor(3, 1, &[0.0, 0.0, 1.0]),
                Value::String("b".into()),
            ],
        )
        .expect("patch builtin should dispatch");
        let Value::Num(handle) = handle else {
            panic!("expected numeric graphics handle");
        };
        crate::call_builtin(
            "set",
            &[
                Value::Num(handle),
                Value::String("Visible".into()),
                Value::Bool(false),
            ],
        )
        .expect("set patch visible");

        let visible = crate::call_builtin(
            "get",
            &[Value::Num(handle), Value::String("Visible".into())],
        )
        .expect("get patch visible");
        assert_eq!(visible, Value::Bool(false));

        let all = crate::call_builtin("get", &[Value::Num(handle)]).expect("get patch struct");
        let Value::Struct(st) = all else {
            panic!("expected patch property struct");
        };
        assert_eq!(st.fields.get("Visible"), Some(&Value::Bool(false)));
    }
}
