// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use crate::common::{self, Result, SourceFileVersion};
use crate::util;
use i_slint_compiler::diagnostics::Spanned;
use i_slint_compiler::expression_tree::{Expression, Unit};
use i_slint_compiler::langtype::{ElementType, Type};
use i_slint_compiler::object_tree::{Element, ElementRc, PropertyDeclaration, PropertyVisibility};
use i_slint_compiler::parser::{
    SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize, syntax_nodes,
};
use lsp_types::Url;
use smol_str::{SmolStr, ToSmolStr};

use std::collections::HashSet;
use std::path::PathBuf;

#[derive(Clone, Debug)]
pub enum CodeBlockOrExpression {
    CodeBlock(syntax_nodes::CodeBlock),
    Expression(syntax_nodes::Expression),
}

impl CodeBlockOrExpression {
    pub fn new(node: SyntaxNode) -> Option<Self> {
        match node.kind() {
            SyntaxKind::CodeBlock => Some(Self::CodeBlock(node.into())),
            SyntaxKind::Expression => Some(Self::Expression(node.into())),
            _ => None,
        }
    }

    pub fn expression(&self) -> Option<syntax_nodes::Expression> {
        match self {
            CodeBlockOrExpression::CodeBlock(_) => None,
            CodeBlockOrExpression::Expression(expr) => Some(expr.clone()),
        }
    }
}

impl std::ops::Deref for CodeBlockOrExpression {
    type Target = SyntaxNode;
    fn deref(&self) -> &Self::Target {
        match self {
            CodeBlockOrExpression::CodeBlock(cb) => cb,
            CodeBlockOrExpression::Expression(expr) => expr,
        }
    }
}

#[derive(Clone, Debug)]
pub struct DefinitionInformation {
    pub property_definition_range: TextRange,
    pub selection_range: TextRange,
    pub code_block_or_expression: CodeBlockOrExpression,
}

#[derive(Clone, Debug)]
pub struct DeclarationInformation {
    pub path: PathBuf,
    pub start_position: TextSize,
}

#[derive(Clone, Debug)]
pub struct PropertyInformation {
    pub name: SmolStr,
    pub priority: u32,
    pub ty: Type,
    pub visibility: PropertyVisibility,
    pub declared_at: Option<DeclarationInformation>,
    /// Range of the binding in the element source file, if it exist
    pub defined_at: Option<DefinitionInformation>,
    /// Value of the property, which can be the default set from the base
    pub default_value: Option<Expression>,
    pub group: SmolStr,
    pub group_priority: u32,
}

#[derive(Clone, Debug)]
pub struct ElementInformation {
    pub id: SmolStr,
    pub component_name: SmolStr,
    pub type_name: SmolStr,
    pub offset: TextSize,
}

#[derive(Clone, Debug)]
pub struct QueryPropertyResponse {
    pub element_rc_node: common::ElementRcNode,
    pub properties: Vec<PropertyInformation>,
    pub element: Option<ElementInformation>,
    pub source_uri: String,
    pub source_version: i32,
}

const HIGH_PRIORITY: u32 = 100;
const DEFAULT_PRIORITY: u32 = 1000;

// This returns defined reserved properties such as x, y, width, height,
// accessiblity properties or layout properties
fn get_reserved_properties<'a>(
    group: &'a str,
    group_priority: u32,
    properties: impl Iterator<Item = (&'static str, Type)> + 'a,
) -> impl Iterator<Item = PropertyInformation> + 'a {
    properties.filter(move |(_, t)| !matches!(t, Type::Callback { .. })).map(move |p| {
        PropertyInformation {
            name: p.0.into(),
            priority: DEFAULT_PRIORITY,
            ty: p.1,
            visibility: PropertyVisibility::InOut,
            declared_at: None,
            defined_at: None,
            default_value: None,
            group: group.into(),
            group_priority,
        }
    })
}

fn property_is_editable(property: &PropertyDeclaration, is_local_element: bool) -> bool {
    if !property.property_type.is_property_type() {
        // Filter away the callbacks
        return false;
    }
    if matches!(property.visibility, PropertyVisibility::Output | PropertyVisibility::Private)
        && !is_local_element
    {
        // Skip properties that cannot be set because of visibility rules
        return false;
    }
    if property.type_node().is_none() {
        return false;
    }

    true
}

fn add_element_properties(
    element: &Element,
    group: &str,
    group_priority: u32,
    is_local_element: bool,
    result: &mut Vec<PropertyInformation>,
) {
    result.extend(element.property_declarations.iter().filter_map(move |(name, value)| {
        if !property_is_editable(value, is_local_element) {
            return None;
        }

        let declared_at = value.node.as_ref().map(|n| {
            let decl = syntax_nodes::PropertyDeclaration::new(n.clone());

            let ty_node: SyntaxNode =
                decl.as_ref().and_then(|d| d.Type()).map(|t| t.into()).unwrap_or(n.clone());

            DeclarationInformation {
                path: n.source_file.path().to_path_buf(),
                start_position: ty_node.text_range().start(),
            }
        });
        Some(PropertyInformation {
            name: name.clone(),
            priority: DEFAULT_PRIORITY,
            ty: value.property_type.clone(),
            visibility: value.visibility,
            declared_at,
            defined_at: None,
            default_value: None,
            group: group.into(),
            group_priority,
        })
    }))
}

/// Move left from the start of a `token` to include white-space and comments that go with it.
fn left_extend(token: SyntaxToken) -> SyntaxToken {
    let mut current_token = token.prev_token();
    let mut start_token = token.clone();
    let mut last_comment = token;

    // Walk backwards:
    while let Some(t) = current_token {
        if t.kind() == SyntaxKind::Whitespace {
            let lbs = t.text().matches('\n').count();
            if lbs >= 1 {
                start_token = last_comment.clone();
            }
            if lbs >= 2 {
                break;
            }
            current_token = t.prev_token();
            continue;
        }
        if t.kind() == SyntaxKind::Comment {
            last_comment = t.clone();
            current_token = t.prev_token();
            continue;
        }
        break;
    }

    start_token
}

/// Move right from the end of the `token` to include white-space and comments that go with it.
fn right_extend(token: SyntaxToken) -> SyntaxToken {
    let mut current_token = token.next_token();
    let mut end_token = token.clone();
    let mut last_comment = token;

    // Walk forwards:
    while let Some(t) = current_token {
        if t.kind() == SyntaxKind::RBrace {
            // All comments between us and a `}` belong to us!
            end_token = last_comment;
            break;
        }
        if t.kind() == SyntaxKind::Whitespace {
            let lbs = t.text().matches('\n').count();
            if lbs > 0 {
                // comments in the current line belong to us, *if* there is a linebreak
                end_token = last_comment;
                break;
            }
            current_token = t.next_token();
            continue;
        }
        if t.kind() == SyntaxKind::Comment {
            last_comment = t.clone();
            current_token = t.next_token();
            continue;
        }

        // in all other cases: Leave the comment to the following token!
        break;
    }

    end_token
}

fn find_code_block_or_expression(
    element: &syntax_nodes::Element,
    offset: u32,
) -> Option<DefinitionInformation> {
    let mut selection_range = None;
    let mut code_block_or_expression = None;
    let mut property_definition_range = None;

    if let Some(token) = element.token_at_offset(offset.into()).right_biased() {
        for ancestor in token.parent_ancestors() {
            if ancestor.kind() == SyntaxKind::BindingExpression {
                // The BindingExpression contains leading and trailing whitespace + `;`
                if let Some(child) = ancestor.first_child() {
                    code_block_or_expression = CodeBlockOrExpression::new(child);
                }
                continue;
            }
            if (ancestor.kind() == SyntaxKind::Binding)
                || (ancestor.kind() == SyntaxKind::PropertyDeclaration)
            {
                property_definition_range = Some(ancestor.text_range());
                selection_range = Some(TextRange::new(
                    left_extend(ancestor.first_token()?).text_range().start(),
                    right_extend(ancestor.last_token()?).text_range().end(),
                ))
                .or(property_definition_range);
                break;
            }
            if ancestor.kind() == SyntaxKind::Element {
                // There should have been a binding before the element!
                break;
            }
        }
    }
    Some(DefinitionInformation {
        property_definition_range: property_definition_range?,
        selection_range: selection_range?,
        code_block_or_expression: code_block_or_expression?,
    })
}

fn find_property_binding_offset(
    element: &common::ElementRcNode,
    property_name: &str,
) -> Option<u32> {
    let element_range = element.with_element_node(|node| node.text_range());

    let element = element.element.borrow();

    if let Some(v) = element.bindings.get(property_name) {
        if let Some(span) = &v.borrow().span {
            let offset = span.span().offset as u32;
            if element.source_file().map(|sf| sf.path())
                == span.source_file.as_ref().map(|sf| sf.path())
                && element_range.contains(offset.into())
            {
                return Some(offset);
            }
        }
    }

    None
}

#[derive(Debug)]
pub enum LayoutKind {
    None,
    HorizontalBox,
    VerticalBox,
    GridLayout,
}

fn insert_property_definitions(
    element: &common::ElementRcNode,
    mut properties: Vec<PropertyInformation>,
) -> Vec<PropertyInformation> {
    fn binding_value(element: &ElementRc, prop: &str, count: &mut usize) -> Expression {
        // prevent infinite recursion while visiting the two-way bindings
        *count += 1;
        if *count > 10 {
            return Expression::Invalid;
        }

        if let Some(binding) = element.borrow().bindings.get(prop) {
            let e = binding.borrow().expression.clone();
            if !matches!(e, Expression::Invalid) {
                return e;
            }
            for twb in &binding.borrow().two_way_bindings {
                let mut e = binding_value(&twb.property.element(), twb.property.name(), count);
                if !matches!(e, Expression::Invalid) {
                    for f in &twb.field_access {
                        e = Expression::StructFieldAccess { base: e.into(), name: f.clone() }
                    }
                    return e;
                }
            }
        }
        match &element.borrow().base_type {
            ElementType::Component(c) => binding_value(&c.root_element, prop, &mut 0),
            ElementType::Builtin(b) => b
                .properties
                .get(prop)
                .and_then(|p| p.default_value.expr(element))
                .unwrap_or_default(),
            _ => Expression::Invalid,
        }
    }

    for prop_info in properties.iter_mut() {
        if let Some(offset) = find_property_binding_offset(element, prop_info.name.as_str()) {
            prop_info.defined_at =
                element.with_element_node(|node| find_code_block_or_expression(node, offset));
        }
        let def_val = binding_value(&element.element, &prop_info.name, &mut 0);
        if !matches!(def_val, Expression::Invalid) {
            prop_info.default_value = Some(def_val);
        }
    }
    properties
}

pub(super) fn get_properties(
    element: &common::ElementRcNode,
    in_layout: LayoutKind,
) -> Vec<PropertyInformation> {
    let mut result = Vec::new();
    add_element_properties(&element.element.borrow(), "", 0, true, &mut result);

    let mut current_element = element.element.clone();
    let mut depth = 0u32;

    let geometry_prop = HashSet::from(["x", "y", "width", "height"]);

    loop {
        depth += 10;
        let base_type = current_element.borrow().base_type.clone();
        match base_type {
            ElementType::Component(c) => {
                current_element = c.root_element.clone();
                add_element_properties(&current_element.borrow(), &c.id, depth, false, &mut result);
                continue;
            }
            ElementType::Builtin(b) => {
                result.extend(b.properties.iter().filter_map(|(k, t)| {
                    if geometry_prop.contains(k.as_str()) {
                        // skip geometry property because they are part of the reserved ones
                        return None;
                    }
                    if !t.ty.is_property_type() {
                        // skip callbacks and other functions
                        return None;
                    }
                    if t.property_visibility == PropertyVisibility::Output {
                        // Skip output-only properties
                        return None;
                    }

                    let mut priority = DEFAULT_PRIORITY;

                    if b.name == "Text" && k == "text" {
                        priority = HIGH_PRIORITY;
                    }
                    if b.name == "TextInput"
                        && [SmolStr::new_static("text"), SmolStr::new_static("placeholder")]
                            .contains(k)
                    {
                        priority = HIGH_PRIORITY;
                    }
                    if b.name == "Image" && k == "source" {
                        priority = HIGH_PRIORITY;
                    }

                    Some(PropertyInformation {
                        name: k.clone(),
                        priority,
                        ty: t.ty.clone(),
                        visibility: t.property_visibility,
                        declared_at: None,
                        defined_at: None,
                        default_value: t.default_value.expr(&current_element),
                        group: b.name.clone(),
                        group_priority: depth,
                    })
                }));

                if b.name == "Rectangle" {
                    result.push(PropertyInformation {
                        name: "clip".into(),
                        priority: DEFAULT_PRIORITY,
                        ty: Type::Bool,
                        visibility: PropertyVisibility::InOut,
                        declared_at: None,
                        defined_at: None,
                        default_value: Some(Expression::BoolLiteral(false)),
                        group: b.name.clone(),
                        group_priority: depth,
                    });

                    result.extend(get_reserved_properties(
                        &b.name,
                        depth,
                        i_slint_compiler::typeregister::RESERVED_DROP_SHADOW_PROPERTIES
                            .iter()
                            .cloned(),
                    ));
                }

                result.push(PropertyInformation {
                    name: "opacity".into(),
                    priority: DEFAULT_PRIORITY,
                    ty: Type::Float32,
                    visibility: PropertyVisibility::InOut,
                    declared_at: None,
                    defined_at: None,
                    default_value: Some(Expression::NumberLiteral(1.0, Unit::None)),
                    group: b.name.clone(),
                    group_priority: depth,
                });
                result.push(PropertyInformation {
                    name: "visible".into(),
                    priority: DEFAULT_PRIORITY,
                    ty: Type::Bool,
                    visibility: PropertyVisibility::InOut,
                    declared_at: None,
                    defined_at: None,
                    default_value: Some(Expression::BoolLiteral(true)),
                    group: b.name.clone(),
                    group_priority: depth,
                });

                let transform_origin = i_slint_compiler::typeregister::transform_origin_property();
                result.extend(get_reserved_properties(
                    &b.name,
                    depth,
                    i_slint_compiler::typeregister::RESERVED_TRANSFORM_PROPERTIES
                        .iter()
                        .cloned()
                        .chain(std::iter::once((transform_origin.0, transform_origin.1.into()))),
                ));

                if matches!(b.name.as_str(), "GridLayout" | "HorizontalLayout" | "VerticalLayout") {
                    // Add the padding that is otherwise filtered out
                    result.extend(get_reserved_properties(
                        &b.name,
                        depth,
                        i_slint_compiler::typeregister::RESERVED_LAYOUT_PROPERTIES
                            .iter()
                            .filter(|x| x.0.starts_with("padding"))
                            .cloned(),
                    ));
                }
            }
            ElementType::Global => {
                break;
            }

            _ => {}
        }

        result.extend(
            get_reserved_properties(
                "geometry",
                depth + 1000,
                i_slint_compiler::typeregister::RESERVED_GEOMETRY_PROPERTIES.iter().cloned(),
            )
            .filter(|p| match in_layout {
                LayoutKind::None => true,
                LayoutKind::HorizontalBox => p.name.as_str() != "x",
                LayoutKind::VerticalBox => p.name.as_str() != "y",
                LayoutKind::GridLayout => !["x", "y"].contains(&p.name.as_str()),
            })
            .map(|mut p| {
                match p.name.as_str() {
                    "x" => p.priority = 200,
                    "y" => p.priority = 300,
                    "width" => p.priority = 400,
                    "height" => p.priority = 500,
                    _ => { /* do nothing */ }
                }
                p
            }),
        );

        result.extend(
            get_reserved_properties(
                "layout",
                depth + 2000,
                i_slint_compiler::typeregister::RESERVED_LAYOUT_PROPERTIES
                    .iter()
                    // padding for non-layout items is not yet implemented
                    .filter(|x| !x.0.starts_with("padding"))
                    .cloned(),
            )
            .map(|mut p| {
                match p.name.as_str() {
                    "min-width" => p.priority = 200,
                    "min-height" => p.priority = 250,
                    "preferred-width" => p.priority = 300,
                    "preferred-height" => p.priority = 350,
                    "max-width" => p.priority = 400,
                    "max-height" => p.priority = 450,
                    "horizontal-stretch" => p.priority = 500,
                    "vertical-stretch" => p.priority = 550,
                    _ => { /* do nothing */ }
                }
                p
            }),
        );
        if matches!(in_layout, LayoutKind::GridLayout) {
            result.extend(get_reserved_properties(
                "layout",
                depth + 2000,
                i_slint_compiler::typeregister::RESERVED_GRIDLAYOUT_PROPERTIES.iter().cloned(),
            ));
        }
        result.push(PropertyInformation {
            name: "accessible-role".into(),
            priority: DEFAULT_PRIORITY - 100,
            ty: Type::Enumeration(
                i_slint_compiler::typeregister::BUILTIN.with(|e| e.enums.AccessibleRole.clone()),
            ),
            visibility: PropertyVisibility::InOut,
            declared_at: None,
            defined_at: None,
            default_value: None,
            group: "accessibility".into(),
            group_priority: depth + 10000,
        });
        if current_element.borrow().is_binding_set("accessible-role", true) {
            result.extend(get_reserved_properties(
                "accessibility",
                depth + 10000,
                i_slint_compiler::typeregister::reserved_accessibility_properties(),
            ));
        }
        break;
    }

    result.sort_by_key(|p| p.name.clone());

    insert_property_definitions(element, result)
}

fn find_block_range(element: &common::ElementRcNode) -> Option<TextRange> {
    element.with_element_node(|node| {
        let open_brace = node.child_token(SyntaxKind::LBrace)?;
        let close_brace = node.child_token(SyntaxKind::RBrace)?;

        Some(TextRange::new(open_brace.text_range().start(), close_brace.text_range().end()))
    })
}

fn get_element_information(element: &common::ElementRcNode) -> ElementInformation {
    let offset = element.with_element_node(|n| n.text_range().start());
    let e = element.element.borrow();
    let component_name = element.with_element_node(|n| {
        if let Some(c) = n.parent().and_then(|p| syntax_nodes::Component::new(p.clone())) {
            c.DeclaredIdentifier().text().to_smolstr()
        } else {
            SmolStr::default()
        }
    });
    let type_name = if matches!(&e.base_type, ElementType::Builtin(b) if b.name == "Empty") {
        SmolStr::default()
    } else {
        e.base_type.to_smolstr()
    };
    ElementInformation { id: e.id.clone(), component_name, type_name, offset }
}

pub(crate) fn query_properties(
    uri: &Url,
    source_version: SourceFileVersion,
    element: &common::ElementRcNode,
    in_layout: LayoutKind,
) -> Result<QueryPropertyResponse> {
    Ok(QueryPropertyResponse {
        element_rc_node: element.clone(),
        properties: get_properties(element, in_layout),
        element: Some(get_element_information(element)),
        source_uri: uri.to_string(),
        source_version: source_version.unwrap_or(i32::MIN),
    })
}

fn get_property_information(
    properties: &[PropertyInformation],
    property_name: &str,
) -> Result<PropertyInformation> {
    if let Some(property) = properties.iter().find(|pi| pi.name == property_name) {
        Ok(property.clone())
    } else {
        Err(format!("Element has no property with name {property_name}").into())
    }
}

fn create_text_document_edit_for_set_binding_on_existing_property(
    uri: Url,
    version: SourceFileVersion,
    property: &PropertyInformation,
    new_expression: String,
    format: common::ByteFormat,
) -> Option<lsp_types::TextDocumentEdit> {
    property.defined_at.as_ref().map(|defined_at| {
        let range = util::node_to_lsp_range(&defined_at.code_block_or_expression, format);
        let edit = lsp_types::TextEdit { range, new_text: new_expression };
        common::create_text_document_edit(uri, version, vec![edit])
    })
}

enum InsertPosition {
    Before,
    After,
}

fn find_insert_position_relative_to_defined_properties(
    properties: &[PropertyInformation],
    property_name: &str,
) -> Option<(TextRange, InsertPosition)> {
    let mut previous_property = None;
    let mut property_index = usize::MAX;

    for (i, p) in properties.iter().enumerate() {
        if p.name == property_name {
            property_index = i;
        } else if let Some(defined_at) = &p.defined_at {
            if property_index == usize::MAX {
                previous_property = Some((i, defined_at.selection_range.end()));
            } else {
                if let Some((pi, _)) = previous_property {
                    if (i - property_index) >= (property_index - pi) {
                        break;
                    }
                }
                let p = defined_at.selection_range.start();
                return Some((TextRange::new(p, p), InsertPosition::Before));
            }
        }
    }

    previous_property.map(|(_, pp)| (TextRange::new(pp, pp), InsertPosition::After))
}

fn find_insert_range_for_property(
    block_range: &Option<TextRange>,
    properties: &[PropertyInformation],
    property_name: &str,
) -> Option<(TextRange, InsertPosition)> {
    find_insert_position_relative_to_defined_properties(properties, property_name).or_else(|| {
        // No properties defined yet:
        block_range.map(|r| {
            // Right after the leading `{`...
            let pos = r.start().checked_add(1.into()).unwrap_or(r.start());
            (TextRange::new(pos, pos), InsertPosition::After)
        })
    })
}

fn create_text_document_edit_for_set_binding_on_known_property(
    uri: Url,
    version: SourceFileVersion,
    element: &common::ElementRcNode,
    properties: &[PropertyInformation],
    property_name: &str,
    new_expression: &str,
    format: common::ByteFormat,
) -> Option<lsp_types::TextDocumentEdit> {
    let block_range = find_block_range(element);

    find_insert_range_for_property(&block_range, properties, property_name).map(
        |(range, insert_type)| {
            let source_file = element.with_element_node(|n| n.source_file.clone());
            let indent = util::find_element_indent(element).unwrap_or_default();
            let edit = lsp_types::TextEdit {
                range: util::text_range_to_lsp_range(&source_file, range, format),
                new_text: match insert_type {
                    InsertPosition::Before => {
                        format!("{property_name}: {new_expression};\n{indent}    ")
                    }
                    InsertPosition::After => {
                        format!("\n{indent}    {property_name}: {new_expression};")
                    }
                },
            };
            common::create_text_document_edit(uri, version, vec![edit])
        },
    )
}

pub fn set_binding(
    uri: Url,
    version: SourceFileVersion,
    element: &common::ElementRcNode,
    property_name: &str,
    new_expression: String,
    format: common::ByteFormat,
) -> Option<lsp_types::WorkspaceEdit> {
    set_binding_impl(uri, version, element, property_name, new_expression, format)
        .map(|edit| common::create_workspace_edit_from_text_document_edits(vec![edit]))
}

pub fn set_binding_impl(
    uri: Url,
    version: SourceFileVersion,
    element: &common::ElementRcNode,
    property_name: &str,
    new_expression: String,
    format: common::ByteFormat,
) -> Option<lsp_types::TextDocumentEdit> {
    let properties = get_properties(element, LayoutKind::None);
    let property = get_property_information(&properties, property_name).ok()?;

    if property.defined_at.is_some() {
        // Change an already defined property:
        create_text_document_edit_for_set_binding_on_existing_property(
            uri,
            version,
            &property,
            new_expression,
            format,
        )
    } else {
        // Add a new definition to a known property:
        create_text_document_edit_for_set_binding_on_known_property(
            uri,
            version,
            element,
            &properties,
            property_name,
            &new_expression,
            format,
        )
    }
}

#[cfg(any(feature = "preview-external", feature = "preview-engine"))]
pub fn set_bindings(
    uri: Url,
    version: SourceFileVersion,
    element: &common::ElementRcNode,
    properties: &[crate::common::PropertyChange],
    format: common::ByteFormat,
) -> Option<lsp_types::WorkspaceEdit> {
    let edits = properties
        .iter()
        .filter_map(|p| {
            set_binding_impl(uri.clone(), version, element, &p.name, p.value.clone(), format)
        })
        .collect::<Vec<_>>();

    (edits.len() == properties.len())
        .then_some(common::create_workspace_edit_from_text_document_edits(edits))
}

#[cfg(any(feature = "preview-external", feature = "preview-engine"))]
fn element_at_source_code_position(
    document_cache: &common::DocumentCache,
    position: &common::VersionedPosition,
) -> Result<common::ElementRcNode> {
    if &document_cache.document_version(position.url()) != position.version() {
        return Err("Document version mismatch.".into());
    }

    let doc = document_cache
        .get_document(position.url())
        .ok_or_else(|| "Document not found".to_string())?;

    let source_file = doc
        .node
        .as_ref()
        .map(|n| n.source_file.clone())
        .ok_or_else(|| "Document had no node".to_string())?;
    let element_position =
        util::text_size_to_lsp_position(&source_file, position.offset(), document_cache.format);

    Ok(document_cache.element_at_position(position.url(), &element_position).ok_or_else(|| {
        format!("No element found at the given start position {:?}", &element_position)
    })?)
}

#[cfg(any(feature = "preview-external", feature = "preview-engine"))]
pub fn update_element_properties(
    document_cache: &common::DocumentCache,
    position: common::VersionedPosition,
    properties: Vec<common::PropertyChange>,
) -> Option<lsp_types::WorkspaceEdit> {
    let element = element_at_source_code_position(document_cache, &position).ok()?;

    set_bindings(
        position.url().clone(),
        *position.version(),
        &element,
        &properties,
        document_cache.format,
    )
}

fn create_workspace_edit_for_remove_binding(
    uri: Url,
    version: SourceFileVersion,
    range: lsp_types::Range,
) -> lsp_types::WorkspaceEdit {
    let edit = lsp_types::TextEdit { range, new_text: String::new() };
    common::create_workspace_edit(uri.clone(), version, vec![edit])
}

pub fn remove_binding(
    uri: Url,
    version: SourceFileVersion,
    element: &common::ElementRcNode,
    property_name: &str,
    format: common::ByteFormat,
) -> Result<lsp_types::WorkspaceEdit> {
    let source_file = element.with_element_node(|node| node.source_file.clone());

    let token = find_property_binding_offset(element, property_name)
        .and_then(|offset| {
            element.with_element_node(|node| node.token_at_offset(offset.into()).right_biased())
        })
        .ok_or("Could not find property to delete.")?;

    for ancestor in token.parent_ancestors() {
        if ancestor.kind() == SyntaxKind::PropertyDeclaration {
            let prop_decl = syntax_nodes::PropertyDeclaration::from(ancestor.clone());
            let binding =
                prop_decl.BindingExpression().ok_or("property declaration has no binding")?;
            let colon = ancestor
                .child_token(SyntaxKind::Colon)
                .ok_or("property peclaration has no colon")?;
            let start = colon.text_range().start();
            if let Some(semi_colon) = binding.child_token(SyntaxKind::Semicolon) {
                let end = semi_colon.text_range().start();
                let range =
                    util::text_range_to_lsp_range(&source_file, TextRange::new(start, end), format);
                let edit = lsp_types::TextEdit { range, new_text: String::new() };
                return Ok(common::create_workspace_edit(uri.clone(), version, vec![edit]));
            } else if let Some(closing_brace) =
                binding.CodeBlock().and_then(|cb| cb.child_token(SyntaxKind::RBrace))
            {
                let end = closing_brace.text_range().end();
                let range =
                    util::text_range_to_lsp_range(&source_file, TextRange::new(start, end), format);
                let edit = lsp_types::TextEdit { range, new_text: ";".into() };
                return Ok(common::create_workspace_edit(uri.clone(), version, vec![edit]));
            } else {
                return Err("Could not find end of range to delete.".into());
            }
        } else if ancestor.kind() == SyntaxKind::Binding {
            let start = {
                let token = left_extend(ancestor.first_token().ok_or("empty binding")?);
                let start = token.text_range().start();
                token
                    .prev_token()
                    .and_then(|t| {
                        if t.kind() == SyntaxKind::Whitespace && t.text().contains('\n') {
                            let to_sub =
                                t.text().split('\n').next_back().unwrap_or_default().len() as u32;
                            start.checked_sub(to_sub.into())
                        } else {
                            None
                        }
                    })
                    .unwrap_or(start)
            };
            let end = {
                let token = right_extend(ancestor.last_token().ok_or("empty binding")?);
                let end = token.text_range().end();
                token
                    .next_token()
                    .and_then(|t| {
                        if t.kind() == SyntaxKind::Whitespace && t.text().contains('\n') {
                            let to_add =
                                t.text().split('\n').next().unwrap_or_default().len() as u32;
                            end.checked_add((to_add + 1/* <cr> */).into())
                        } else {
                            None
                        }
                    })
                    .unwrap_or(end)
            };

            let range =
                util::text_range_to_lsp_range(&source_file, TextRange::new(start, end), format);
            return Ok(create_workspace_edit_for_remove_binding(uri, version, range));
        }
        if ancestor.kind() == SyntaxKind::Element {
            // There should have been a binding before the element!
            break;
        }
    }
    Err("Could not find range to delete.".into())
}

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

    use crate::language::test::{complex_document_cache, loaded_document_cache};

    fn find_property<'a>(
        properties: &'a [PropertyInformation],
        name: &'_ str,
    ) -> Option<&'a PropertyInformation> {
        properties.iter().find(|p| p.name == name)
    }

    pub fn properties_at_position_in_cache(
        line: u32,
        character: u32,
        document_cache: &common::DocumentCache,
        url: &lsp_types::Url,
    ) -> Option<(common::ElementRcNode, Vec<PropertyInformation>)> {
        let element =
            document_cache.element_at_position(url, &lsp_types::Position { line, character })?;
        Some((element.clone(), get_properties(&element, LayoutKind::None)))
    }

    fn properties_at_position(
        line: u32,
        character: u32,
    ) -> Option<(
        common::ElementRcNode,
        Vec<PropertyInformation>,
        common::DocumentCache,
        lsp_types::Url,
    )> {
        let (dc, url, _) = complex_document_cache();
        if let Some((e, p)) = properties_at_position_in_cache(line, character, &dc, &url) {
            Some((e, p, dc, url))
        } else {
            None
        }
    }

    #[test]
    fn test_get_properties() {
        let (_, result, _, _) = properties_at_position(6, 4).unwrap();

        // Property of element:
        assert_eq!(&find_property(&result, "elapsed-time").unwrap().ty, &Type::Duration);
        // Property of base type:
        assert_eq!(&find_property(&result, "no-frame").unwrap().ty, &Type::Bool);
        // reserved properties:
        assert_eq!(
            &find_property(&result, "accessible-role").unwrap().ty.to_string(),
            "enum AccessibleRole"
        );
        // Accessible property should not be present since the role is none
        assert!(find_property(&result, "accessible-label").is_none());
        assert!(find_property(&result, "accessible-action-default").is_none());

        // Poke deeper:
        let (_, result, dc, _) = properties_at_position(21, 30).unwrap();
        let property = find_property(&result, "background").unwrap();

        let def_at = property.defined_at.as_ref().unwrap();
        let def_range = util::node_to_lsp_range(&def_at.code_block_or_expression, dc.format);
        assert_eq!(def_range.end.line, def_range.start.line);
        // -1 because the lsp range end location is exclusive.
        assert_eq!(
            (def_range.end.character - def_range.start.character) as usize,
            "lightblue".len()
        );

        // On a Button
        let (_, result, _, _) = properties_at_position(48, 4).unwrap();

        assert_eq!(&find_property(&result, "text").unwrap().ty, &Type::String);
        // Accessible property should not be present since the role is button
        assert_eq!(find_property(&result, "accessible-label").unwrap().ty, Type::String);
        // No callbacks
        assert!(find_property(&result, "accessible-action-default").is_none());
        assert!(find_property(&result, "clicked").is_none());
    }

    #[test]
    fn test_element_information() {
        let (document_cache, url, _) = complex_document_cache();
        let element =
            document_cache.element_at_position(&url, &lsp_types::Position::new(33, 4)).unwrap();

        let result = get_element_information(&element);

        let o = util::text_size_to_lsp_position(
            &element.with_element_node(|n| n.source_file.clone()),
            result.offset,
            document_cache.format,
        );
        assert_eq!(o.line, 32);
        assert_eq!(o.character, 12);

        assert_eq!(result.type_name.to_string(), "Text");
    }

    #[test]
    fn test_element_information_empty() {
        let (document_cache, url, _) = loaded_document_cache(
            "component FooBar { property <int> foo; btn := Button {} }".into(),
        );
        let element =
            document_cache.element_at_position(&url, &lsp_types::Position::new(1, 19)).unwrap();
        let result = get_element_information(&element);
        assert_eq!(result.type_name.to_string(), "");
        assert_eq!(result.id, "root");

        let element =
            document_cache.element_at_position(&url, &lsp_types::Position::new(1, 39)).unwrap();
        let result = get_element_information(&element);
        // Because `Button` is not defined in this scope
        assert_eq!(result.type_name.to_string(), "<error>");
        assert_eq!(result.id, "btn");
    }

    fn delete_range_test(
        content: String,
        pos_l: u32,
        pos_c: u32,
        sl: u32,
        sc: u32,
        el: u32,
        ec: u32,
    ) {
        for (i, l) in content.split('\n').enumerate() {
            println!("{i:2}: {l}");
        }
        println!("-------------------------------------------------------------------");
        println!("   :           1         2         3         4         5");
        println!("   : 012345678901234567890123456789012345678901234567890123456789");

        let (dc, url, _) = loaded_document_cache(content);
        let source_file = dc.get_document(&url).unwrap().node.as_ref().unwrap().source_file.clone();

        let (_, result) = properties_at_position_in_cache(pos_l, pos_c, &dc, &url).unwrap();

        let p = find_property(&result, "text").unwrap();
        let definition = p.defined_at.as_ref().unwrap();

        assert_eq!(&definition.code_block_or_expression.text(), "\"text\"");

        let sel_range =
            util::text_range_to_lsp_range(&source_file, definition.selection_range, dc.format);
        println!(
            "Actual: (l: {}, c: {}) - (l: {}, c: {}) --- Expected: (l: {sl}, c: {sc}) - (l: {el}, c: {ec})",
            sel_range.start.line,
            sel_range.start.character,
            sel_range.end.line,
            sel_range.end.character,
        );

        assert_eq!(sel_range.start.line, sl);
        assert_eq!(sel_range.start.character, sc);
        assert_eq!(sel_range.end.line, el);
        assert_eq!(sel_range.end.character, ec);
    }

    #[test]
    fn test_get_property_delete_range_no_extend() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text { text: "text"; }
    }
}
            "#
            .to_string(),
            4,
            12,
            4,
            15,
            4,
            28,
        );
    }

    #[test]
    fn test_get_property_delete_range_line_extend_left_extra_indent() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
              // Cut
            text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            5,
            14,
            6,
            25,
        );
    }

    #[test]
    fn test_get_property_delete_range_line_extend_left_no_ws() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            /* Cut */text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            5,
            12,
            5,
            34,
        );
    }

    #[test]
    fn test_get_property_delete_range_extend_left_to_empty_line() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            font-size: 12px;
            // Keep

            // Cut
            text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            8,
            12,
            9,
            25,
        );
    }

    #[test]
    fn test_get_property_delete_range_extend_left_many_lines() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            font-size: 12px;
             // Keep

            // Cut
              // Cut
            // Cut
                  // Cut
            // Cut
            // Cut
            // Cut
            // Cut
            // Cut
            // Cut
            // Cut
            text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            8,
            12,
            19,
            25,
        );
    }

    #[test]
    fn test_get_property_delete_range_extend_left_multiline_comment() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            font-size: 12px;
          // Keep

            /* Cut
       Cut
            /* Cut
              ---  Cut */

            // Cut
            // Cut */
            text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            8,
            12,
            15,
            25,
        );
    }

    #[test]
    fn test_get_property_delete_range_extend_left_un_indented_property() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            font-size: 12px;

        /* Cut
       Cut

            /* Cut
              ---  Cut */
  Cut */
                // Cut
            // Cut
text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            7,
            8,
            15,
            13,
        );
    }

    #[test]
    fn test_get_property_delete_range_extend_left_leading_line_comment() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            font-size: 12px;
          // Cut
            /* Cut
       Cut

            /* Cut
              ---  Cut */
  Cut */
                // Cut
            // Cut
            /* cut */ text: "text";
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            6,
            10,
            15,
            35,
        );
    }

    #[test]
    fn test_get_property_delete_range_right_extend() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            text: "text"; // Cut
                // Keep
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            5,
            12,
            5,
            32,
        );
    }

    #[test]
    fn test_get_property_delete_range_right_extend_to_line_break() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text {
            text: "text"; /* Cut
                // Cut
                   Cut
                 *   Cut */

            // Keep
            font-size: 12px;
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            5,
            12,
            8,
            27,
        );
    }

    #[test]
    fn test_get_property_delete_range_no_right_extend() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow {
    VerticalBox {
        Text {
            text: "text";/*Keep*/ font_size: 12px;
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            5,
            12,
            5,
            25,
        );
    }

    #[test]
    fn test_get_property_delete_range_no_right_extend_with_ws() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow {
    VerticalBox {
        Text {
            text: "text";  /*Keep*/ font_size: 12px;
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            5,
            12,
            5,
            25,
        );
    }

    #[test]
    fn test_get_property_delete_range_right_extend_to_rbrace() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow {
    VerticalBox {
        Text { text: "text";/* Cut */}
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            4,
            15,
            4,
            37,
        );
    }

    #[test]
    fn test_get_property_delete_range_right_extend_to_rbrace_ws() {
        delete_range_test(
            r#"import { VerticalBox } from "std-widgets.slint";

component MainWindow inherits Window {
    VerticalBox {
        Text { text: "text";   /* Cut */    /* Cut */ }
        }
    }
}
            "#
            .to_string(),
            4,
            12,
            4,
            15,
            4,
            53,
        );
    }

    #[test]
    fn test_get_property_definition() {
        let (dc, url, _) = loaded_document_cache(
            r#"import { LineEdit, Button, Slider, HorizontalBox, VerticalBox } from "std-widgets.slint";

component Base1 {
    in-out property<int> foo = 42;
}

component Base2 inherits Base1 {
    foo: 23;
}

component MainWindow inherits Window {
    property <duration> total-time: slider.value * 1s;
    property <duration> elapsed-time;

    callback tick(duration);
    tick(passed-time) => {
        elapsed-time += passed-time;
        elapsed-time = min(elapsed-time, total-time);
    }

    VerticalBox {
        HorizontalBox {
            padding-left: 0;
            Text { text: "Elapsed Time:"; }
            base2 := Base2 {
                foo: 15;
                min-width: 200px;
                max-height: 30px;
                background: gray;
                Rectangle {
                    height: 100%;
                    width: parent.width * (elapsed-time/total-time);
                    background: lightblue;
                }
            }
        }
        Text{
            text: (total-time / 1s) + "s";
        }
        HorizontalBox {
            padding-left: 0;
            Text {
                text: "Duration:";
                vertical-alignment: center;
            }
            slider := Slider {
                maximum: 30s / 1s;
                value: 10s / 1s;
                changed(new-duration) => {
                    root.total-time = new-duration * 1s;
                    root.elapsed-time = min(root.elapsed-time, root.total-time);
                }
            }
        }
        Button {
            text: "Reset";
            clicked => {
                elapsed-time = 0
            }
        }
    }
}
            "#.to_string());

        let doc = dc.get_document(&url).unwrap();
        let source = &doc.node.as_ref().unwrap().source_file;
        let (l, c) =
            source.line_column(source.source().unwrap().find("base2 :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();

        let foo_property = find_property(&result, "foo").unwrap();

        assert_eq!(foo_property.ty, Type::Int32);

        let declaration = foo_property.declared_at.as_ref().unwrap();
        let start_position =
            util::text_size_to_lsp_position(source, declaration.start_position, dc.format);
        assert_eq!(declaration.path, source.path());
        assert_eq!(start_position.line, 3);
        assert_eq!(start_position.character, 20); // This should probably point to the start of
        // `property<int> foo = 42`, not to the `<`
        assert_eq!(foo_property.group, "Base1");
    }

    #[test]
    fn layout_padding() {
        let (dc, url, _) = loaded_document_cache(
            r#"import { LineEdit, Button, Slider, HorizontalBox, VerticalBox } from "std-widgets.slint";

component CustomL inherits HorizontalLayout {
    padding-left: 10px;
    @children
}

component MainWindow inherits Window {
    rect1 := Rectangle {
        lay1 := CustomL {
            Button { text: "Button"; }
            err := Error {}
            Rectangle {}
            lay2 := VerticalLayout {
                Slider { value: 0.5; }
                Button { text: "Button"; }
            }
            lay3 := VerticalBox {
                slider2 := Slider { value: 0.5; }
                Rectangle {}
            }
        }
    }
}
            "#.to_string());

        let doc = dc.get_document(&url).unwrap();
        let source = &doc.node.as_ref().unwrap().source_file;
        let (l, c) =
            source.line_column(source.source().unwrap().find("lay1 :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();
        let property = find_property(&result, "padding").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);
        let property = find_property(&result, "padding-left").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);
        let property = find_property(&result, "padding-top").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);

        let (l, c) =
            source.line_column(source.source().unwrap().find("lay2 :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();
        let property = find_property(&result, "padding").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);
        let property = find_property(&result, "padding-left").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);
        let property = find_property(&result, "padding-top").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);

        let (l, c) =
            source.line_column(source.source().unwrap().find("lay3 :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();
        let property = find_property(&result, "padding").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);
        let property = find_property(&result, "padding-left").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);
        let property = find_property(&result, "padding-top").unwrap();
        assert_eq!(property.ty, Type::LogicalLength);

        let (l, c) =
            source.line_column(source.source().unwrap().find("rect1 :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();
        assert!(find_property(&result, "padding").is_none());
        assert!(find_property(&result, "padding-left").is_none());
        assert!(find_property(&result, "padding-top").is_none());

        let (l, c) =
            source.line_column(source.source().unwrap().find("slider2 :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();
        assert!(find_property(&result, "padding").is_none());
        assert!(find_property(&result, "padding-left").is_none());
        assert!(find_property(&result, "padding-top").is_none());

        let (l, c) =
            source.line_column(source.source().unwrap().find("err :=").unwrap(), dc.format);
        let (_, result) = properties_at_position_in_cache(l as u32, c as u32, &dc, &url).unwrap();
        assert!(dbg!(find_property(&result, "padding")).is_none());
        assert!(find_property(&result, "padding-left").is_none());
        assert!(find_property(&result, "padding-top").is_none());
    }

    #[test]
    fn test_invalid_properties() {
        let (dc, url, _) = loaded_document_cache(
            r#"
global SomeGlobal := {
    property <int> glob: 77;
}

component SomeRect inherits Rectangle {
    component foo inherits InvalidType {
        property <int> abcd: 41;
        width: 45px;
    }
}
            "#
            .to_string(),
        );

        let (element_node, result) = properties_at_position_in_cache(1, 25, &dc, &url).unwrap();
        let source = element_node.with_element_node(|n| n.source_file.clone());

        let glob_property = find_property(&result, "glob").unwrap();
        assert_eq!(glob_property.ty, Type::Int32);
        let declaration = glob_property.declared_at.as_ref().unwrap();
        let start_position =
            util::text_size_to_lsp_position(&source, declaration.start_position, dc.format);
        assert_eq!(declaration.path, source.path());
        assert_eq!(start_position.line, 2);
        assert_eq!(glob_property.group, "");
        assert!(find_property(&result, "width").is_none());

        let (_, result) = properties_at_position_in_cache(8, 4, &dc, &url).unwrap();
        let abcd_property = find_property(&result, "abcd").unwrap();
        assert_eq!(abcd_property.ty, Type::Int32);
        let declaration = abcd_property.declared_at.as_ref().unwrap();
        let start_position =
            util::text_size_to_lsp_position(&source, declaration.start_position, dc.format);
        assert_eq!(declaration.path, source.path());
        assert_eq!(start_position.line, 7);
        assert_eq!(abcd_property.group, "");

        let x_property = find_property(&result, "x").unwrap();
        assert_eq!(x_property.ty, Type::LogicalLength);
        assert!(x_property.defined_at.is_none());
        assert_eq!(x_property.group, "geometry");

        let width_property = find_property(&result, "width").unwrap();
        assert_eq!(width_property.ty, Type::LogicalLength);
        let definition = width_property.defined_at.as_ref().unwrap();
        let expression_range =
            util::node_to_lsp_range(&definition.code_block_or_expression, dc.format);
        assert_eq!(expression_range.start.line, 8);
        assert_eq!(width_property.group, "geometry");
    }

    #[test]
    fn test_invalid_property_panic() {
        let (dc, url, _) =
            loaded_document_cache(r#"export component Demo { Text { text: } }"#.to_string());

        let (_, result) = properties_at_position_in_cache(0, 35, &dc, &url).unwrap();

        let prop = find_property(&result, "text").unwrap();
        assert!(prop.defined_at.is_none()); // The property has no valid definition at this time
    }

    #[test]
    fn test_codeblock_property_declaration() {
        let (dc, url, _) = loaded_document_cache(
            r#"
component Base {
    property <int> a1: { 1 + 1 }
    property <int> a2: { 1 + 2; }
    property <int> a3: { 1 + 3 };
    property <int> a4: { 1 + 4; };
    in property <int> b: {
        if (something) { return 42; }
        return 1 + 2;
    }
}
            "#
            .to_string(),
        );

        let (_, result) = properties_at_position_in_cache(3, 0, &dc, &url).unwrap();
        assert_eq!(find_property(&result, "a1").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a1")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 1 }"
        );
        assert_eq!(find_property(&result, "a2").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a2")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 2; }"
        );
        assert_eq!(find_property(&result, "a3").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a3")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 3 }"
        );
        assert_eq!(find_property(&result, "a4").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a4")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 4; }"
        );
        assert_eq!(find_property(&result, "b").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "b")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{\n        if (something) { return 42; }\n        return 1 + 2;\n    }"
        );
    }

    #[test]
    fn test_codeblock_property_definitions() {
        let (dc, url, _) = loaded_document_cache(
            r#"
component Base {
    in property <int> a1;
    in property <int> a2;
    in property <int> a3;
    in property <int> a4;
    in property <int> b;
}
component MyComp {
    Base {
        a1: { 1 + 1 }
        a2: { 1 + 2; }
        a3: { 1 + 3 };
        a4: { 1 + 4; };
        b: {
            if (something) { return 42; }
            return 1 + 2;
        }
    }
}
            "#
            .to_string(),
        );

        let (_, result) = properties_at_position_in_cache(11, 1, &dc, &url).unwrap();
        assert_eq!(find_property(&result, "a1").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a1")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 1 }"
        );
        assert_eq!(find_property(&result, "a2").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a2")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 2; }"
        );
        assert_eq!(find_property(&result, "a3").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a3")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 3 }"
        );
        assert_eq!(find_property(&result, "a4").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "a4")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{ 1 + 4; }"
        );
        assert_eq!(find_property(&result, "b").unwrap().ty, Type::Int32);
        assert_eq!(
            find_property(&result, "b")
                .unwrap()
                .defined_at
                .as_ref()
                .unwrap()
                .code_block_or_expression
                .text(),
            "{\n            if (something) { return 42; }\n            return 1 + 2;\n        }",
        );
    }

    #[test]
    fn test_output_properties() {
        let (dc, url, _) = loaded_document_cache(
            r#"
component Base {
    property <int> a: 1;
    in property <int> b: 2;
    out property <int> c: 3;
    in-out property <int> d: 4;
}

component MyComp {
    Base {

    }
    TouchArea {

    }
}
            "#
            .to_string(),
        );

        let (_, result) = properties_at_position_in_cache(3, 0, &dc, &url).unwrap();
        assert_eq!(find_property(&result, "a").unwrap().ty, Type::Int32);
        assert_eq!(find_property(&result, "b").unwrap().ty, Type::Int32);
        assert_eq!(find_property(&result, "c").unwrap().ty, Type::Int32);
        assert_eq!(find_property(&result, "d").unwrap().ty, Type::Int32);

        let (_, result) = properties_at_position_in_cache(10, 0, &dc, &url).unwrap();
        assert!(find_property(&result, "a").is_none());
        assert_eq!(find_property(&result, "b").unwrap().ty, Type::Int32);
        assert!(find_property(&result, "c").is_none());
        assert_eq!(find_property(&result, "d").unwrap().ty, Type::Int32);

        let (_, result) = properties_at_position_in_cache(13, 0, &dc, &url).unwrap();
        assert_eq!(find_property(&result, "enabled").unwrap().ty, Type::Bool);
        assert!(find_property(&result, "pressed").is_none());
    }

    fn set_binding_helper(
        property_name: &str,
        new_value: &str,
    ) -> Option<lsp_types::WorkspaceEdit> {
        let (element, _, dc, url) = properties_at_position(18, 15).unwrap();
        set_binding(url, None, &element, property_name, new_value.to_string(), dc.format)
    }

    #[test]
    fn test_set_binding_valid_expression_unknown_property() {
        let edit = set_binding_helper("foobar", "1 + 2");

        assert_eq!(edit, None);
    }

    #[test]
    fn test_set_binding_valid_expression_undefined_property() {
        let edit = set_binding_helper("x", "30px");

        let edit = edit.unwrap();
        let dcs = if let Some(lsp_types::DocumentChanges::Edits(e)) = &edit.document_changes {
            e
        } else {
            unreachable!();
        };
        assert_eq!(dcs.len(), 1_usize);

        let tcs = &dcs[0].edits;
        assert_eq!(tcs.len(), 1_usize);

        let tc = if let lsp_types::OneOf::Left(tc) = &tcs[0] {
            tc
        } else {
            unreachable!();
        };
        assert_eq!(&tc.new_text, "\n                x: 30px;");
        assert_eq!(tc.range.start, lsp_types::Position { line: 17, character: 33 });
        assert_eq!(tc.range.end, lsp_types::Position { line: 17, character: 33 });
    }

    #[test]
    fn test_set_binding_valid() {
        let edit = set_binding_helper("min-width", "5px");

        let edit = edit.unwrap();
        let dcs = if let Some(lsp_types::DocumentChanges::Edits(e)) = &edit.document_changes {
            e
        } else {
            unreachable!();
        };
        assert_eq!(dcs.len(), 1_usize);

        let tcs = &dcs[0].edits;
        assert_eq!(tcs.len(), 1_usize);

        let tc = if let lsp_types::OneOf::Left(tc) = &tcs[0] {
            tc
        } else {
            unreachable!();
        };
        assert_eq!(&tc.new_text, "5px");
        assert_eq!(tc.range.start, lsp_types::Position { line: 17, character: 27 });
        assert_eq!(tc.range.end, lsp_types::Position { line: 17, character: 32 });
    }

    #[test]
    fn test_remove_binding() {
        let source = r#"
component Foo inherits Window {
    width: 30px;
    background: red;
    Foo { background: blue; }
}
"#;
        let (dc, uri, _) = crate::language::test::loaded_document_cache(source.into());
        let elem = dc
            .element_at_offset(&uri, TextSize::new(source.find("Window").unwrap() as u32))
            .unwrap();
        let edit = remove_binding(uri.clone(), None, &elem, "background", dc.format).unwrap();

        let applied = crate::common::text_edit::apply_workspace_edit(&dc, &edit).unwrap();
        assert_eq!(
            applied.first().unwrap().contents,
            r#"
component Foo inherits Window {
    width: 30px;
    Foo { background: blue; }
}
"#
        );

        let elem = dc
            .element_at_offset(&uri, TextSize::new(source.find("Foo {").unwrap() as u32))
            .unwrap();
        let edit = remove_binding(uri.clone(), None, &elem, "background", dc.format).unwrap();

        let applied = crate::common::text_edit::apply_workspace_edit(&dc, &edit).unwrap();
        assert_eq!(
            applied.first().unwrap().contents,
            r#"
component Foo inherits Window {
    width: 30px;
    background: red;
    Foo {  }
}
"#
        );
    }

    #[test]
    fn test_remove_binding_in_declaration() {
        let source = r#"
component Foo inherits Window {
    property <int> test1: 45 + 78; // comment
    property <int> test2: {
        return 4;
    }
    property <{x: int}> test3: { return { x: 0 }; };
    background: violet;
}
"#;
        let (dc, uri, _) = crate::language::test::loaded_document_cache(source.into());
        let elem = dc
            .element_at_offset(&uri, TextSize::new(source.find("Window").unwrap() as u32))
            .unwrap();
        let edit = remove_binding(uri.clone(), None, &elem, "test1", dc.format).unwrap();
        let applied = crate::common::text_edit::apply_workspace_edit(&dc, &edit).unwrap();
        assert_eq!(
            applied.first().unwrap().contents,
            r#"
component Foo inherits Window {
    property <int> test1; // comment
    property <int> test2: {
        return 4;
    }
    property <{x: int}> test3: { return { x: 0 }; };
    background: violet;
}
"#
        );

        let edit = remove_binding(uri.clone(), None, &elem, "test2", dc.format).unwrap();
        let applied = crate::common::text_edit::apply_workspace_edit(&dc, &edit).unwrap();
        assert_eq!(
            applied.first().unwrap().contents,
            r#"
component Foo inherits Window {
    property <int> test1: 45 + 78; // comment
    property <int> test2;
    property <{x: int}> test3: { return { x: 0 }; };
    background: violet;
}
"#
        );

        let edit = remove_binding(uri.clone(), None, &elem, "test3", dc.format).unwrap();
        let applied = crate::common::text_edit::apply_workspace_edit(&dc, &edit).unwrap();
        assert_eq!(
            applied.first().unwrap().contents,
            r#"
component Foo inherits Window {
    property <int> test1: 45 + 78; // comment
    property <int> test2: {
        return 4;
    }
    property <{x: int}> test3;
    background: violet;
}
"#
        );
    }
}
