use std::{collections::BTreeMap, convert::TryFrom, ops::DerefMut, str::FromStr};

use url::Url;

use crate::types::{temporal, Class, Document, Fragment, Image, Item, KnownClass, PropertyValue};

/// Represents a property value in JF2 format.
///
/// JF2 properties can hold various types of values including strings,
/// [temporal values][temporal::Value], [nested objects][Object], lists, HTML content, images, and
/// URLs.
#[derive(PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(untagged)]
pub enum Property {
    /// A plain string value.
    String(String),

    /// A temporal value (date, time, datetime, etc.).
    Temporal(temporal::Value),

    /// A nested JF2 object.
    Subobject(Object),

    /// A list of property values.
    Multiple(PropertyList),

    /// HTML content with fallback text representation.
    Html {
        /// The raw HTML string.
        html: String,
        /// The plain text fallback.
        text: String,
    },

    /// An image with optional alt text and URL.
    Image {
        /// The alt text for the image, if present.
        alt: Option<String>,
        /// The URL of the image.
        url: Url,
    },

    /// Reference objects keyed by URL.
    References(BTreeMap<String, Object>),

    /// A URL value.
    Url(Url),
}

impl From<Url> for Property {
    fn from(v: Url) -> Self {
        Self::Url(v)
    }
}

impl From<temporal::Value> for Property {
    fn from(v: temporal::Value) -> Self {
        Self::Temporal(v)
    }
}

impl TryFrom<String> for Property {
    type Error = crate::Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        if let Ok(tv) = temporal::Value::from_str(&value) {
            Ok(Self::Temporal(tv))
        } else if let Ok(u) = Url::from_str(&value) {
            Ok(Self::Url(u))
        } else {
            Ok(Self::String(value))
        }
    }
}

impl Property {
    /// Returns the nested object if this property is a [`Subobject`].
    pub fn as_object(&self) -> Option<&Object> {
        if let Self::Subobject(v) = self {
            Some(v)
        } else {
            None
        }
    }

    /// Returns the string value if this property is a [`String`].
    pub fn as_string(&self) -> Option<&String> {
        if let Self::String(v) = self {
            Some(v)
        } else {
            None
        }
    }

    /// Returns the property list if this property is a [`Multiple`] value.
    pub fn as_list(&self) -> Option<&PropertyList> {
        if let Self::Multiple(v) = self {
            Some(v)
        } else {
            None
        }
    }

    /// Returns the HTML string if this property is an [`Html`] value.
    pub fn as_html(&self) -> Option<&String> {
        if let Self::Html { html, .. } = self {
            Some(html)
        } else {
            None
        }
    }

    /// Returns the text content if this property is an [`Html`] value.
    pub fn as_text(&self) -> Option<&String> {
        if let Self::Html { text, .. } = self {
            Some(text)
        } else {
            None
        }
    }

    fn into_list(self) -> Vec<Property> {
        if let Self::Multiple(list) = self {
            list.0
        } else {
            vec![self]
        }
    }

    fn flatten(self) -> Self {
        let list = self.into_list();

        if list.len() == 1 {
            list[0].to_owned()
        } else {
            Self::Multiple(list.into())
        }
    }

    /// Returns the URL if this property is a [`Url`].
    pub fn as_url(&self) -> Option<&Url> {
        if let Self::Url(v) = self {
            Some(v)
        } else {
            None
        }
    }

    /// Returns `true` if the property is a [`String`].
    ///
    /// [`String`]: Property::String
    pub fn is_string(&self) -> bool {
        matches!(self, Self::String(..))
    }

    /// Returns `true` if the property is a [`Url`].
    ///
    /// [`Url`]: Property::Url
    pub fn is_url(&self) -> bool {
        matches!(self, Self::Url(..))
    }

    /// Returns `true` if the property is a [`Temporal`] value.
    ///
    /// [`Temporal`]: Property::Temporal
    pub fn is_temporal(&self) -> bool {
        matches!(self, Self::Temporal(..))
    }

    fn is_empty(&self) -> bool {
        if let Self::Multiple(v) = self {
            v.is_empty()
        } else {
            false
        }
    }

    fn as_object_list(&self) -> Option<&BTreeMap<String, Object>> {
        if let Self::References(refs) = self {
            Some(refs)
        } else {
            None
        }
    }

    /// Returns the temporal value if this property is a [`Temporal`].
    pub fn as_temporal(&self) -> Option<&temporal::Value> {
        if let Self::Temporal(v) = self {
            Some(v)
        } else {
            None
        }
    }

    /// Creates a new Property from a temporal value.
    pub fn temporal(value: impl Into<temporal::Value>) -> Self {
        Self::Temporal(value.into())
    }

    /// Returns `true` if the property is [`Subobject`].
    ///
    /// [`Subobject`]: Property::Subobject
    pub fn is_object(&self) -> bool {
        matches!(self, Self::Subobject(..))
    }
}

impl From<time::OffsetDateTime> for Property {
    fn from(dt: time::OffsetDateTime) -> Self {
        let stamp: crate::types::temporal::Stamp = dt.into();
        Self::Temporal(crate::types::temporal::Value::Timestamp(stamp))
    }
}

/// A list of JF2 property values.
#[derive(PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, Default)]
pub struct PropertyList(Vec<Property>);

impl From<PropertyList> for Property {
    fn from(val: PropertyList) -> Self {
        Property::Multiple(val)
    }
}

impl FromIterator<Property> for PropertyList {
    fn from_iter<T: IntoIterator<Item = Property>>(iter: T) -> Self {
        Self(iter.into_iter().collect())
    }
}

impl From<Vec<Property>> for PropertyList {
    fn from(value: Vec<Property>) -> Self {
        Self(value)
    }
}

impl std::ops::Deref for PropertyList {
    type Target = Vec<Property>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::ops::DerefMut for PropertyList {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl TryFrom<PropertyValue> for Property {
    type Error = crate::Error;

    fn try_from(value: PropertyValue) -> Result<Self, Self::Error> {
        match value {
            PropertyValue::Plain(v) => Ok(Self::String(v.to_string())),
            PropertyValue::Url(u) => Ok(Self::Url((*u).clone())),
            PropertyValue::Temporal(t) => Ok(Self::Temporal(t)),
            PropertyValue::Fragment(Fragment {
                html, value: text, ..
            }) => Ok(Self::Html { html, text }),
            PropertyValue::Item(i) => i.try_into().map(Self::Subobject),
            PropertyValue::Image(Image { value: url, alt }) => Ok(Self::Image { alt, url }),
        }
    }
}

impl TryFrom<Property> for PropertyValue {
    type Error = crate::Error;

    fn try_from(value: Property) -> Result<Self, Self::Error> {
        match value {
            Property::String(s) => Ok(Self::Plain(crate::types::TextValue::new(s))),
            Property::Url(u) => Ok(Self::Url(crate::types::UrlValue::new(u))),
            Property::Temporal(t) => Ok(Self::Temporal(t)),
            Property::Html { html, text } => Ok(Self::Fragment(Fragment {
                html,
                value: text,
                #[cfg(feature = "per_element_lang")]
                lang: None,
                links: Vec::new(),
            })),
            Property::Subobject(obj) => Ok(Self::Item(obj.try_into()?)),
            Property::Image { alt, url } => Ok(Self::Image(Image { alt, value: url })),
            Property::Multiple(_) => Err(crate::Error::Types(crate::types::Error::NotAnObject)),
            Property::References(_) => Err(crate::Error::Types(crate::types::Error::NotAnObject)),
        }
    }
}

static JSON_LD_CONTEXT_URI: &str = "http://www.w3.org/ns/jf2";

static RESERVED_PROPERTY_NAMES: [&str; 8] = [
    "type",
    "children",
    "references",
    "content-type",
    "html",
    "text",
    "lang",
    "@context",
];

impl<'a> TryFrom<(&'a str, PropertyValue)> for Property {
    type Error = crate::Error;

    fn try_from((name, value): (&'a str, PropertyValue)) -> Result<Self, Self::Error> {
        if RESERVED_PROPERTY_NAMES.contains(&name) {
            if ["type", "content-type", "name", "html", "text", "lang"].contains(&name)
                && !matches!(value, PropertyValue::Plain(_))
            {
                Err(crate::Error::InvalidRequiredProperty {
                    name: name.into(),
                    kind: "string".into(),
                })
            } else {
                value.try_into()
            }
        } else {
            value.try_into()
        }
    }
}

impl TryFrom<Vec<PropertyValue>> for Property {
    type Error = crate::Error;
    fn try_from(values: Vec<PropertyValue>) -> Result<Self, Self::Error> {
        let mut converted_values: Vec<Property> = Vec::default();
        for value in values {
            converted_values.push(value.try_into()?);
        }

        if converted_values.len() == 1 {
            Ok(converted_values[0].to_owned())
        } else {
            Ok(Self::Multiple(PropertyList(converted_values)))
        }
    }
}

impl TryFrom<Property> for Vec<PropertyValue> {
    type Error = crate::Error;

    fn try_from(value: Property) -> Result<Self, Self::Error> {
        match value {
            Property::Multiple(property_list) => {
                let mut property_values = Vec::new();
                for property in property_list.iter() {
                    property_values.push(property.clone().try_into()?);
                }
                Ok(property_values)
            }
            single_property => Ok(vec![single_property.try_into()?]),
        }
    }
}

/// A JF2 object representing a microformat item.
///
/// The inner BTreeMap stores property names as keys and their
/// corresponding Property values.
#[derive(Default, Debug, PartialEq, serde::Deserialize, serde::Serialize, Clone)]
pub struct Object(pub BTreeMap<String, Property>);

impl Object {
    /// Returns all child objects of this object.
    ///
    /// Children are stored under the "children" property and represent
    /// nested microformat items.
    pub fn children(&self) -> Vec<Object> {
        self.0
            .get("children")
            .and_then(|list_val| list_val.as_list())
            .map(|props| {
                props
                    .iter()
                    .flat_map(Property::as_object)
                    .cloned()
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default()
    }

    /// Replaces the children of this object with the provided vector.
    ///
    /// Returns the previously stored children, if any.
    pub fn set_children(&mut self, children: Vec<Object>) -> Vec<Object> {
        let replaced_values = self.0.insert(
            "children".into(),
            PropertyList::from_iter(children.into_iter().map(Property::from)).into(),
        );

        // FIXME: Implement children swap of object.
        if let Some(_values) = replaced_values {
            Vec::default()
        } else {
            Vec::default()
        }
    }

    /// Returns the URL property of this object, if present.
    pub fn url(&self) -> Option<Url> {
        self.0.get("url").and_then(|p| p.as_url().cloned())
    }

    /// Extracts reference objects from subobjects and collects them.
    ///
    /// This method walks through all properties, finding subobjects with
    /// URL properties and converting them to references stored under
    /// the "references" key.
    pub(crate) fn extract_references(&mut self) {
        let mut references = if let Some(Property::References(refs)) = self.0.remove("references") {
            refs
        } else {
            Default::default()
        };

        for (property_name, property_value) in self.0.iter_mut() {
            if RESERVED_PROPERTY_NAMES.contains(&property_name.as_str()) {
                continue;
            }

            if property_name != "children" {
                *property_value = property_value.clone().flatten()
            } else {
                *property_value = PropertyList(property_value.clone().into_list()).into();
            }

            if let Property::Subobject(child_obj) = property_value {
                if let Some(url) = child_obj.url() {
                    let new_value = Property::Url(url.clone());
                    references.insert(url.to_string(), child_obj.to_owned());
                    *property_value = new_value;
                }
            }

            // FIXME: Walk over properties.
        }

        if let Some(Property::Multiple(children)) = self.0.get_mut("children") {
            for child in children.iter_mut() {
                if let Property::Subobject(child_obj) = child {
                    child_obj.extract_references();
                    if let Some(Property::References(refs)) = child_obj.remove("references") {
                        references.extend(refs)
                    }
                }
            }

            if !references.is_empty() {
                self.insert("references".to_string(), Property::References(references));
            }
        }
    }

    /// Inserts the JSON-LD context URI if not already present.
    fn insert_context_uri(&mut self) {
        if !self.contains_key("@context") {
            self.insert(
                "@context".to_string(),
                Property::String(JSON_LD_CONTEXT_URI.to_string()),
            );
        }
    }

    /// Returns the only children of this object if it's a solo item.
    pub fn flatten(self) -> Self {
        if let Some(only_child) = self
            .children()
            .first()
            .cloned()
            .filter(|_| self.children().len() == 1)
        {
            only_child
        } else {
            self
        }
    }
}

impl std::ops::Deref for Object {
    type Target = BTreeMap<String, Property>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for Object {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl Object {
    /// Returns the microformat type of this object.
    ///
    /// Reads the "type" property and converts it to a `Class`.
    /// Defaults to `Entry` if no type is specified.
    pub fn r#type(&self) -> Class {
        self.get("type")
            .and_then(|type_property_value| {
                if let Property::String(class_str) = type_property_value {
                    Class::from_str(class_str).ok()
                } else {
                    None
                }
            })
            .unwrap_or(Class::Known(KnownClass::Entry))
    }
}

impl TryFrom<Item> for Object {
    type Error = crate::Error;

    fn try_from(value: Item) -> Result<Self, Self::Error> {
        let mut new_object = Self::default();

        let resolved_type = value
            .r#type
            .iter()
            .find(|c| c.is_recognized())
            .cloned()
            .unwrap_or(Class::Known(KnownClass::Entry));
        new_object.insert(
            "type".to_string(),
            Property::String(resolved_type.to_string().replacen("h-", "", 1)),
        );

        if !value.children.is_empty() {
            let mut jf2_objects = Vec::default();

            for item in value.children.iter() {
                jf2_objects.push(item.clone().try_into().map(Property::Subobject)?);
            }

            new_object.insert(
                "children".to_string(),
                Property::Multiple(PropertyList(jf2_objects)),
            );
        }

        let mut remaining_properties = value.properties.clone();
        // NOTE: This currently removes all of the following content; this could be a bug?
        let content = remaining_properties
            .remove("content")
            .and_then(|content_values| {
                content_values
                    .into_iter()
                    .flat_map(|prop_value| {
                        if prop_value.is_empty() {
                            None
                        } else if let PropertyValue::Fragment(f) = prop_value {
                            Some(f)
                        } else {
                            None
                        }
                    })
                    .next()
            });

        if let Some(html_value) = content.as_ref().map(|fr| fr.html.clone()) {
            new_object.insert("html".to_string(), Property::String(html_value));
        }

        if let Some(text_value) = content.as_ref().map(|fr| fr.value.clone()) {
            new_object.insert("text".to_string(), Property::String(text_value));
        }

        let restructed_properties = remaining_properties.into_iter().try_fold(
            Default::default(),
            |mut properties_acc,
             (property_name, property_values)|
             -> Result<BTreeMap<String, Property>, crate::Error> {
                let values = property_values.into_iter().try_fold(
                    Default::default(),
                    |mut list_acc, mf2_value| -> Result<PropertyList, crate::Error> {
                        list_acc.push(mf2_value.try_into()?);
                        Ok(list_acc)
                    },
                )?;

                let is_children = &property_name == "children";
                let property_value = Property::Multiple(values);
                properties_acc.insert(
                    property_name,
                    if !is_children {
                        property_value.flatten()
                    } else {
                        property_value
                    },
                );

                Ok(properties_acc)
            },
        )?;

        new_object.extend(restructed_properties);

        Ok(new_object)
    }
}

impl TryFrom<Object> for Item {
    type Error = crate::Error;

    fn try_from(value: Object) -> Result<Self, Self::Error> {
        let mut item = Item::new(vec![value.r#type()]);

        // Process children first
        item.children.extend(value.children().into_iter().try_fold(
            Vec::default(),
            |mut acc, object| {
                acc.push(object.try_into()?);
                Result::<_, Self::Error>::Ok(acc)
            },
        )?);

        // Get references for resolution
        let references = value
            .get("references")
            .and_then(|prop| prop.as_object_list())
            .cloned()
            .unwrap_or_default();

        // Process all properties except reserved ones
        for (property_name, property_value) in value.0.iter() {
            if RESERVED_PROPERTY_NAMES.contains(&property_name.as_str()) {
                continue; // Skip reserved properties
            }

            // Convert JF2 property to PropertyValue(s)
            let property_values: Vec<PropertyValue> = property_value.clone().try_into()?;

            // Add the properties to the item
            if !property_values.is_empty() {
                item.properties
                    .insert(property_name.clone(), property_values);
            }
        }

        // Handle content (HTML/text) if present
        if let Some(html_property) = value.get("html") {
            if let Some(html_str) = html_property.as_string() {
                let text_property = value
                    .get("text")
                    .and_then(|t| t.as_string())
                    .cloned()
                    .unwrap_or_default();

                let fragment = Fragment {
                    html: html_str.clone(),
                    value: text_property,
                    #[cfg(feature = "per_element_lang")]
                    lang: None,
                    links: Vec::new(),
                };

                // Add content as 'content' property
                item.properties.insert(
                    "content".to_string(),
                    vec![PropertyValue::Fragment(fragment)],
                );
            }
        }

        // Handle URL property if present
        if let Some(url_property) = value.get("url") {
            if let Some(url_str) = url_property.as_url() {
                // Check if this URL exists in references and resolve if needed
                if let Some(referenced_obj) = references.get(&url_str.to_string()) {
                    if let Ok(referenced_item) =
                        <Object as TryInto<Item>>::try_into(referenced_obj.clone())
                    {
                        // Merge properties from referenced item
                        for (prop_name, prop_values) in referenced_item.properties {
                            item.properties.entry(prop_name).or_insert(prop_values);
                        }
                    }
                }
            }
        }

        Ok(item)
    }
}

impl From<Object> for Property {
    fn from(mut object: Object) -> Self {
        object.remove("@context");
        Self::Subobject(object)
    }
}

/// A helper trait to convert values into JF2.
pub trait IntoJf2 {
    /// Converts a value into a JF2 object.
    fn into_jf2(self) -> Result<Object, crate::Error>;
}

impl IntoJf2 for Item {
    fn into_jf2(self) -> Result<Object, crate::Error> {
        self.try_into()
    }
}

impl IntoJf2 for Document {
    fn into_jf2(self) -> Result<Object, crate::Error> {
        let mut doc_obj = if self.items.len() == 1
            && self.items[0]
                .r#type
                .contains(&Class::Known(KnownClass::Feed))
        {
            self.items[0].clone().try_into()?
        } else {
            let mut top_level_items = Vec::default();

            for item in self.items {
                top_level_items.push(item.into_jf2()?.into());
            }

            let mut doc_obj = Object::default();
            doc_obj.insert(
                "children".to_string(),
                Property::Multiple(PropertyList(top_level_items)),
            );
            doc_obj
        };

        doc_obj.insert_context_uri();
        doc_obj.extract_references();

        Ok(doc_obj)
    }
}

/// JF2 profile implementations for various microformat types.
pub mod profiles;

mod test;

#[cfg(test)]
mod conversion_tests {
    use super::*;
    use url::Url;

    #[test]
    fn test_property_to_property_value_string() {
        let jf2_prop = Property::String("hello".to_string());
        let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
        assert_eq!(
            prop_value,
            PropertyValue::Plain(crate::types::TextValue::new("hello".to_string()))
        );
    }

    #[test]
    fn test_property_to_property_value_url() {
        let url = Url::parse("https://example.com").unwrap();
        let jf2_prop = Property::Url(url.clone());
        let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
        assert_eq!(
            prop_value,
            PropertyValue::Url(crate::types::UrlValue::new(url))
        );
    }

    #[test]
    fn test_property_to_property_value_temporal() {
        let temporal = temporal::Value::Timestamp(crate::types::temporal::Stamp::now());
        let jf2_prop = Property::Temporal(temporal.clone());
        let prop_value: PropertyValue = jf2_prop.try_into().unwrap();
        assert_eq!(prop_value, PropertyValue::Temporal(temporal));
    }

    #[test]
    fn test_property_to_property_value_html() {
        let jf2_prop = Property::Html {
            html: "<p>hello</p>".to_string(),
            text: "hello".to_string(),
        };
        let prop_value: PropertyValue = jf2_prop.try_into().unwrap();

        if let PropertyValue::Fragment(fragment) = prop_value {
            assert_eq!(fragment.html, "<p>hello</p>");
            assert_eq!(fragment.value, "hello");
        } else {
            panic!("Expected Fragment");
        }
    }

    #[test]
    fn test_property_to_property_value_image() {
        let url = Url::parse("https://example.com/img.jpg").unwrap();
        let jf2_prop = Property::Image {
            alt: Some("An image".to_string()),
            url: url.clone(),
        };
        let prop_value: PropertyValue = jf2_prop.try_into().unwrap();

        if let PropertyValue::Image(image) = prop_value {
            assert_eq!(image.alt, Some("An image".to_string()));
            assert_eq!(image.value, url);
        } else {
            panic!("Expected Image");
        }
    }

    #[test]
    fn test_property_multiple_to_vec_property_value() {
        let props = vec![
            Property::String("first".to_string()),
            Property::String("second".to_string()),
        ];
        let jf2_prop = Property::Multiple(PropertyList(props));
        let prop_values: Vec<PropertyValue> = jf2_prop.try_into().unwrap();

        assert_eq!(prop_values.len(), 2);
        assert_eq!(
            prop_values[0],
            PropertyValue::Plain(crate::types::TextValue::new("first".to_string()))
        );
        assert_eq!(
            prop_values[1],
            PropertyValue::Plain(crate::types::TextValue::new("second".to_string()))
        );
    }

    #[test]
    fn test_single_property_to_vec_property_value() {
        let jf2_prop = Property::String("single".to_string());
        let prop_values: Vec<PropertyValue> = jf2_prop.try_into().unwrap();

        assert_eq!(prop_values.len(), 1);
        assert_eq!(
            prop_values[0],
            PropertyValue::Plain(crate::types::TextValue::new("single".to_string()))
        );
    }

    #[test]
    fn test_object_to_item_basic() {
        let mut obj = Object::default();
        obj.insert("type".to_string(), Property::String("entry".to_string()));
        obj.insert(
            "name".to_string(),
            Property::String("Test Entry".to_string()),
        );

        let item: Item = obj.try_into().unwrap();
        assert_eq!(item.r#type.len(), 1);
        assert_eq!(item.r#type[0], Class::Known(KnownClass::Entry));

        let names = &item.properties["name"];
        assert_eq!(names.len(), 1);
        assert_eq!(
            names[0],
            PropertyValue::Plain(crate::types::TextValue::new("Test Entry".to_string()))
        );
    }

    #[test]
    fn test_object_to_item_with_content() {
        let mut obj = Object::default();
        obj.insert("type".to_string(), Property::String("entry".to_string()));
        obj.insert(
            "html".to_string(),
            Property::String("<p>Content</p>".to_string()),
        );
        obj.insert("text".to_string(), Property::String("Content".to_string()));

        let item: Item = obj.try_into().unwrap();

        let content = &item.properties["content"];
        assert_eq!(content.len(), 1);

        if let PropertyValue::Fragment(fragment) = &content[0] {
            assert_eq!(fragment.html, "<p>Content</p>");
            assert_eq!(fragment.value, "Content");
        } else {
            panic!("Expected Fragment");
        }
    }

    #[test]
    fn test_object_to_item_with_children() {
        let mut child_obj = Object::default();
        child_obj.insert("type".to_string(), Property::String("cite".to_string()));
        child_obj.insert("name".to_string(), Property::String("Child".to_string()));

        let mut parent_obj = Object::default();
        parent_obj.insert("type".to_string(), Property::String("entry".to_string()));
        parent_obj.insert("name".to_string(), Property::String("Parent".to_string()));
        parent_obj.insert(
            "children".to_string(),
            Property::Multiple(PropertyList(vec![Property::Subobject(child_obj)])),
        );

        let item: Item = parent_obj.try_into().unwrap();

        assert_eq!(item.children.len(), 1);
        assert_eq!(item.children[0].r#type.len(), 1);
        assert_eq!(item.children[0].r#type[0], Class::Known(KnownClass::Cite));
    }

    #[test]
    fn test_object_to_item_with_references() {
        let mut ref_obj = Object::default();
        ref_obj.insert("type".to_string(), Property::String("cite".to_string()));
        ref_obj.insert(
            "name".to_string(),
            Property::String("Referenced".to_string()),
        );

        let mut obj = Object::default();
        obj.insert("type".to_string(), Property::String("entry".to_string()));
        obj.insert(
            "url".to_string(),
            Property::Url(Url::parse("https://example.com").unwrap()),
        );

        let mut references = std::collections::BTreeMap::new();
        references.insert("https://example.com".to_string(), ref_obj);
        obj.insert("references".to_string(), Property::References(references));

        let item: Item = obj.try_into().unwrap();

        // The referenced item should be processed
        assert_eq!(item.r#type.len(), 1);
        assert_eq!(item.r#type[0], Class::Known(KnownClass::Entry));
    }

    #[test]
    fn test_property_value_to_property_roundtrip() {
        let original = PropertyValue::Plain(crate::types::TextValue::new("test".to_string()));
        let jf2_prop: Property = original.clone().try_into().unwrap();
        let converted: PropertyValue = jf2_prop.try_into().unwrap();
        assert_eq!(original, converted);
    }

    #[test]
    fn test_conversion_errors() {
        // Test that Multiple properties can't be converted to single PropertyValue
        let jf2_prop = Property::Multiple(PropertyList(vec![
            Property::String("one".to_string()),
            Property::String("two".to_string()),
        ]));
        let result: Result<PropertyValue, _> = jf2_prop.try_into();
        assert!(result.is_err());

        // Test that References can't be converted
        let jf2_ref_prop = Property::References(std::collections::BTreeMap::new());
        let result: Result<PropertyValue, _> = jf2_ref_prop.try_into();
        assert!(result.is_err());
    }
}
