/// Logic around discovering the author of posts.
///
/// This module provides an implementation of the [IndieWeb authorship algorithm][authorship-algorithm]
/// which deterministically discovers who the author of a post is based on microformats markup.
///
/// The algorithm follows these main steps:
/// 1. Check for direct author properties in the h-entry
/// 2. Check parent h-feed for author property if entry has no author
/// 3. Handle different author property types (h-card, URL, or plain text)
/// 4. Follow rel-author links as backcompat discovery method
/// 5. Fetch and parse author pages when needed
/// 6. Validate representative h-cards using url/uid matching
///
/// [authorship-algorithm]: https://indieweb.org/authorship-spec
use crate::{algorithms::link_rel, http, mf2};
use ::http::Request;
use microformats::types::{Class, KnownClass, PropertyValue};
use microformats::types::{Document, Item};
use url::Url;

/// Represents a discovered author with their information.
#[derive(Debug, Clone, PartialEq)]
pub struct Author {
    /// The author's name.
    pub name: String,
    /// The author's URL, if available.
    pub url: Option<Url>,
    /// The author's photo URL, if available.
    pub photo: Option<Url>,
    /// The full h-card data, if available.
    pub h_card: Option<Item>,
}

/// Errors that can occur during authorship discovery.
#[derive(Debug, thiserror::Error)]
pub enum AuthorshipError {
    #[error("No deterministic author could be found")]
    NoAuthorFound,

    #[error("Network error: {0}")]
    NetworkError(#[from] crate::Error),

    #[error("Invalid URL: {0}")]
    InvalidUrl(#[from] url::ParseError),

    #[error("No h-entry provided")]
    NoEntry,

    #[error("Failed to parse microformats2")]
    ParseError,
}

/// Represents different types of author properties found in microformats.
#[derive(Debug, Clone, PartialEq)]
enum AuthorProperty {
    /// Embedded h-card containing author information
    HCard(Item),
    /// URL pointing to an author page
    Url(Url),
    /// Plain text author name
    Name(String),
}

/// Determines if the h-entry's page is a permalink page.
///
/// A page is considered a permalink if the h-entry has a u-url or url property
/// that matches the page URL.
fn is_permalink_page(entry: &Item, page_url: &Url) -> bool {
    for property_name in ["u-url", "url"] {
        if let Some(values) = entry.properties.get(property_name)
            && let Some(value) = values.first()
        {
            match value {
                PropertyValue::Url(url) => {
                    if url.as_str() == page_url.as_str() {
                        return true;
                    }
                }
                PropertyValue::Plain(text) => {
                    // Handle URL as plain text string
                    if let Ok(url) = Url::parse(text)
                        && url.as_str() == page_url.as_str()
                    {
                        return true;
                    }
                }
                _ => continue,
            }
        }
    }
    false
}

/// Resolves the author of an h-entry using the IndieWeb authorship algorithm.
///
/// # Arguments
/// * `client` - HTTP client for fetching remote resources
/// * `entry` - The h-entry to find the author for
/// * `document` - The document containing the entry and potentially parent h-feeds
/// * `page_url` - The URL of the page being processed
///
/// # Returns
/// * `Ok(Author)` if an author is successfully discovered
/// * `Err(AuthorshipError)` if no author can be deterministically found
#[tracing::instrument(skip(client, entry, document))]
pub async fn resolve_author(
    client: &impl http::Client,
    entry: &Item,
    document: &Document,
    page_url: &Url,
) -> Result<Author, AuthorshipError> {
    // Step 1: Validate input - ensure we have an h-entry
    if !entry
        .r#type
        .iter()
        .any(|t| t == &Class::Known(KnownClass::Entry))
    {
        return Err(AuthorshipError::NoEntry);
    }

    // Step 3: Check if h-entry has an author property
    if let Some(author_property) = extract_author_from_entry(entry) {
        return process_author_property(client, author_property, page_url).await;
    }

    // Step 4: Check parent h-feed for author property
    if let Some(feed_author) = extract_author_from_parent_feed(entry, document) {
        tracing::trace!("Found author from parent h-feed");
        return process_author_property(client, feed_author, page_url).await;
    }

    // Step 6: Check for rel-author link as backcompat (permalink pages only)
    if is_permalink_page(entry, page_url) {
        let rel_map = link_rel::for_url(client, page_url, &["author"], "GET").await?;

        if let Some(author_urls) = rel_map.get("author")
            && let Some(author_page_url) = author_urls.first()
        {
            tracing::trace!("Found rel-author link: {}", author_page_url);
            return resolve_author_from_page(client, author_page_url, page_url).await;
        }
    } else {
        tracing::trace!("Skipping rel-author check - not a permalink page");
    }

    // Step 8: No deterministic author found
    Err(AuthorshipError::NoAuthorFound)
}

/// Extracts author property from an h-entry.
fn extract_author_from_entry(entry: &Item) -> Option<AuthorProperty> {
    // Check for author property in common variants
    for property_name in ["author", "p-author", "u-author"] {
        if let Some(author_values) = entry.properties.get(property_name)
            && let Some(author) = author_values.first()
        {
            return match author {
                PropertyValue::Item(item) if item.is_h_card() => {
                    tracing::trace!("Found embedded h-card author");
                    Some(AuthorProperty::HCard(item.clone()))
                }
                PropertyValue::Url(url) => {
                    tracing::trace!("Found URL author: {}", url);
                    Some(AuthorProperty::Url((**url).clone()))
                }
                PropertyValue::Plain(text) if !text.is_empty() => {
                    tracing::trace!("Found text author: {}", text);
                    Some(AuthorProperty::Name(text.to_string()))
                }
                _ => None,
            };
        }
    }
    None
}

/// Extracts author from a parent h-feed that contains the entry.
///
/// This implements Step 4 of the IndieWeb authorship algorithm:
/// "otherwise if the h-entry has a parent h-feed with author property, use that"
///
/// # Arguments
/// * `entry` - The h-entry to find the parent feed for
/// * `document` - The document containing all items to search for h-feeds
///
/// # Returns
/// * `Some(AuthorProperty)` if the entry is found in an h-feed's children and that feed has an author
/// * `None` if no parent h-feed with author is found
fn extract_author_from_parent_feed(entry: &Item, document: &Document) -> Option<AuthorProperty> {
    tracing::trace!("Checking for parent h-feed with author property");

    // Find all h-feed items in the document
    let feeds = document
        .items
        .iter()
        .filter(|item| item.r#type.contains(&Class::Known(KnownClass::Feed)))
        .collect::<Vec<_>>();

    tracing::trace!("Found {} h-feed(s) in document", feeds.len());

    // Check each h-feed to see if our entry is in its children
    for feed in feeds {
        // Check if the entry is in this feed's children
        let entry_in_feed = feed.children.iter().any(|child| {
            std::ptr::eq(child, entry) || {
                // Also check by comparing properties if ptr eq fails
                // (for cases where entries might be cloned)
                child.properties == entry.properties
            }
        });

        if entry_in_feed {
            tracing::trace!("Entry found in h-feed children");

            // Extract author from the h-feed using the same logic as extract_author_from_entry
            for property_name in ["author", "p-author", "u-author"] {
                if let Some(author_values) = feed.properties.get(property_name)
                    && let Some(author) = author_values.first()
                {
                    return match author {
                        PropertyValue::Item(item) if item.is_h_card() => {
                            tracing::trace!("Found h-feed author as h-card");
                            Some(AuthorProperty::HCard(item.clone()))
                        }
                        PropertyValue::Url(url) => {
                            tracing::trace!("Found h-feed author as URL: {}", url);
                            Some(AuthorProperty::Url((**url).clone()))
                        }
                        PropertyValue::Plain(text) if !text.is_empty() => {
                            tracing::trace!("Found h-feed author as text: {}", text);
                            Some(AuthorProperty::Name(text.to_string()))
                        }
                        _ => None,
                    };
                }
            }

            // Feed has no author property
            tracing::trace!("h-feed has no author property");
            return None;
        }
    }

    tracing::trace!("Entry not found in any h-feed's children");
    None
}

/// Processes different types of author properties to extract author information.
async fn process_author_property(
    client: &impl http::Client,
    author_property: AuthorProperty,
    page_url: &Url,
) -> Result<Author, AuthorshipError> {
    match author_property {
        AuthorProperty::HCard(h_card) => {
            // Step 5.1: Use embedded h-card directly
            let name = extract_name_from_h_card(&h_card)?;
            let url = extract_url_from_h_card(&h_card);
            let photo = extract_photo_from_h_card(&h_card);

            Ok(Author {
                name,
                url,
                photo,
                h_card: Some(h_card),
            })
        }
        AuthorProperty::Url(author_page_url) => {
            // Step 5.2: Fetch author page and extract h-card
            resolve_author_from_page(client, &author_page_url, page_url).await
        }
        AuthorProperty::Name(name) => {
            // Step 5.3: Use the name directly
            Ok(Author {
                name,
                url: None,
                photo: None,
                h_card: None,
            })
        }
    }
}

/// Resolves author information from an author page URL.
async fn resolve_author_from_page(
    client: &impl http::Client,
    author_page_url: &Url,
    original_page_url: &Url,
) -> Result<Author, AuthorshipError> {
    tracing::trace!("Fetching author page: {}", author_page_url);

    // Extract representative h-card from author page
    let author = extract_representative_h_card(client, author_page_url).await?;

    // If we couldn't find a representative h-card, fall back to checking the original page
    if author.is_none() {
        tracing::trace!("No representative h-card found on author page, checking original page");
        return find_author_on_original_page(client, author_page_url, original_page_url).await;
    }

    Ok(author.unwrap())
}

/// Extracts a representative h-card from an author page.
async fn extract_representative_h_card(
    client: &impl http::Client,
    author_page_url: &Url,
) -> Result<Option<Author>, AuthorshipError> {
    // Fetch the author page
    let req = Request::builder()
        .uri(author_page_url.as_str())
        .header("Accept", "text/html, application/mf2+json")
        .body(Default::default())
        .map_err(crate::Error::Http)?;

    let resp = client.send_request(req).await?;
    let document = mf2::http::to_mf2_document(
        resp.map(|body| body.as_bytes().to_vec()),
        author_page_url.as_str(),
    )
    .map_err(|_| AuthorshipError::ParseError)?;

    // Step 7.1: Look for h-cards with url == uid == page's URL
    for item in &document.items {
        if item.is_h_card() {
            let url = extract_url_from_h_card(item);
            let uid = extract_uid_from_h_card(item);

            if let (Some(url), Some(uid)) = (url, uid)
                && url == *author_page_url
                && uid == *author_page_url
            {
                let name = extract_name_from_h_card(item)?;
                let photo = extract_photo_from_h_card(item);

                return Ok(Some(Author {
                    name,
                    url: Some(url),
                    photo,
                    h_card: Some(item.clone()),
                }));
            }
        }
    }

    // Step 7.2: Check for rel-me links and find matching h-cards
    let rel_map = link_rel::for_url(client, author_page_url, &["me"], "GET").await?;
    if let Some(me_urls) = rel_map.get("me") {
        for item in &document.items {
            if item.is_h_card()
                && let Some(url) = extract_url_from_h_card(item)
                && me_urls.contains(&url)
            {
                let name = extract_name_from_h_card(item)?;
                let photo = extract_photo_from_h_card(item);

                return Ok(Some(Author {
                    name,
                    url: Some(url),
                    photo,
                    h_card: Some(item.clone()),
                }));
            }
        }
    }

    Ok(None)
}

/// Attempts to find author information on the original page after fetching author page fails.
async fn find_author_on_original_page(
    client: &impl http::Client,
    author_page_url: &Url,
    original_page_url: &Url,
) -> Result<Author, AuthorshipError> {
    // Fetch the original page
    let req = Request::builder()
        .uri(original_page_url.as_str())
        .header("Accept", "text/html, application/mf2+json")
        .body(Default::default())
        .map_err(crate::Error::Http)?;

    let resp = client.send_request(req).await?;
    let document = mf2::http::to_mf2_document(
        resp.map(|body| body.as_bytes().to_vec()),
        original_page_url.as_str(),
    )
    .map_err(|_| AuthorshipError::ParseError)?;

    // Step 7.4: Find h-cards on the original page that match the author page URL
    for item in &document.items {
        if item.is_h_card()
            && let Some(url) = extract_url_from_h_card(item)
            && url == *author_page_url
        {
            let name = extract_name_from_h_card(item)?;
            let photo = extract_photo_from_h_card(item);

            return Ok(Author {
                name,
                url: Some(url),
                photo,
                h_card: Some(item.clone()),
            });
        }
    }

    Err(AuthorshipError::NoAuthorFound)
}

/// Helper functions for extracting information from h-cards
///
/// Check if an Item represents an h-card
trait HCardExt {
    fn is_h_card(&self) -> bool;
}

impl HCardExt for Item {
    fn is_h_card(&self) -> bool {
        self.r#type
            .iter()
            .any(|t| t == &Class::Known(KnownClass::Card))
    }
}

/// Extracts the name from an h-card, trying various properties.
fn extract_name_from_h_card(h_card: &Item) -> Result<String, AuthorshipError> {
    // Try these properties in order of preference
    for property in ["name", "p-name", "given-name", "family-name"] {
        if let Some(values) = h_card.properties.get(property)
            && let Some(value) = values.first()
        {
            match value {
                PropertyValue::Plain(text) if !text.is_empty() => {
                    return Ok(text.to_string());
                }
                _ => continue,
            }
        }
    }
    Err(AuthorshipError::NoAuthorFound)
}

/// Extracts the URL from an h-card.
fn extract_url_from_h_card(h_card: &Item) -> Option<Url> {
    for property in ["url", "u-url"] {
        if let Some(values) = h_card.properties.get(property)
            && let Some(value) = values.first()
            && let PropertyValue::Url(url) = value
        {
            return Some((**url).clone());
        }
    }
    None
}

/// Extracts the UID from an h-card.
fn extract_uid_from_h_card(h_card: &Item) -> Option<Url> {
    if let Some(values) = h_card.properties.get("uid")
        && let Some(value) = values.first()
        && let PropertyValue::Url(url) = value
    {
        return Some((**url).clone());
    }
    None
}

/// Extracts the photo URL from an h-card.
fn extract_photo_from_h_card(h_card: &Item) -> Option<Url> {
    for property in ["photo", "u-photo"] {
        if let Some(values) = h_card.properties.get(property)
            && let Some(value) = values.first()
            && let PropertyValue::Url(url) = value
        {
            return Some((**url).clone());
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use microformats::types::{Class, Document};
    use std::collections::BTreeMap;

    fn create_test_h_card(name: &str, url: &str, photo: Option<&str>) -> Item {
        let mut properties = BTreeMap::new();
        properties.insert(
            "name".to_string(),
            vec![PropertyValue::Plain(name.to_string().into())],
        );
        properties.insert(
            "url".to_string(),
            vec![PropertyValue::Url(url.parse::<url::Url>().unwrap().into())],
        );

        if let Some(photo_url) = photo {
            properties.insert(
                "photo".to_string(),
                vec![PropertyValue::Url(
                    photo_url.parse::<url::Url>().unwrap().into(),
                )],
            );
        }

        Item {
            r#type: vec![Class::Known(KnownClass::Card)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        }
    }

    fn create_test_h_entry(author_property: Option<(&str, PropertyValue)>) -> Item {
        let mut properties = BTreeMap::new();

        if let Some((prop_name, prop_value)) = author_property {
            properties.insert(prop_name.to_string(), vec![prop_value]);
        }

        Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        }
    }

    fn create_test_h_feed(author_property: Option<(&str, PropertyValue)>) -> Item {
        let mut properties = BTreeMap::new();

        if let Some((prop_name, prop_value)) = author_property {
            properties.insert(prop_name.to_string(), vec![prop_value]);
        }

        Item {
            r#type: vec![Class::Known(KnownClass::Feed)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        }
    }

    // ========================================================================
    // Step 4 Tests: Parent h-feed Author Discovery
    // ========================================================================

    #[test]
    fn test_step4_hfeed_with_author_hcard() {
        // Test that Step 4 finds author from parent h-feed with h-card author
        let feed_author_hcard = create_test_h_card(
            "Feed Author",
            "https://feed-author.com/",
            Some("https://feed-author.com/photo.jpg"),
        );
        let mut h_feed = create_test_h_feed(Some((
            "author",
            PropertyValue::Item(feed_author_hcard.clone()),
        )));

        let h_entry = create_test_h_entry(None); // Entry has no author

        // Add entry as child of feed
        h_feed.children = vec![h_entry.clone()].into();

        let document = Document {
            rels: Default::default(),
            items: vec![h_feed.clone()],
            url: None,
            lang: None,
        };

        let author_property = extract_author_from_parent_feed(&h_entry, &document);
        assert!(
            author_property.is_some(),
            "Should find author from parent h-feed"
        );

        match author_property.unwrap() {
            AuthorProperty::HCard(_) => {} // Expected
            _ => panic!("Expected AuthorProperty::HCard from h-feed author"),
        }
    }

    #[test]
    fn test_step4_hfeed_with_author_url() {
        // Test that Step 4 finds author from parent h-feed with URL author
        let mut h_feed = create_test_h_feed(Some((
            "author",
            PropertyValue::Url(
                "https://feed-author.com/"
                    .parse::<url::Url>()
                    .unwrap()
                    .into(),
            ),
        )));

        let h_entry = create_test_h_entry(None); // Entry has no author
        h_feed.children = vec![h_entry.clone()].into();

        let document = Document {
            rels: Default::default(),
            items: vec![h_feed.clone()],
            url: None,
            lang: None,
        };

        let author_property = extract_author_from_parent_feed(&h_entry, &document);
        assert!(
            author_property.is_some(),
            "Should find author from parent h-feed"
        );

        match author_property.unwrap() {
            AuthorProperty::Url(url) => {
                assert_eq!(url.as_str(), "https://feed-author.com/");
            }
            _ => panic!("Expected AuthorProperty::Url from h-feed author"),
        }
    }

    #[test]
    fn test_step4_hfeed_no_author() {
        // Test that Step 4 returns None when h-feed has no author
        let _h_feed = create_test_h_feed(None); // Feed has no author

        let h_entry = create_test_h_entry(None); // Entry has no author
        let h_feed_with_child = Item {
            r#type: vec![Class::Known(KnownClass::Feed)],
            properties: BTreeMap::new(),
            value: None,
            children: vec![h_entry.clone()].into(),
            id: None,
            lang: None,
        };

        let document = Document {
            rels: Default::default(),
            items: vec![h_feed_with_child],
            url: None,
            lang: None,
        };

        let author_property = extract_author_from_parent_feed(&h_entry, &document);
        assert!(
            author_property.is_none(),
            "Should return None when h-feed has no author"
        );
    }

    #[test]
    fn test_step4_entry_not_in_feed() {
        // Test that Step 4 returns None when entry is not in any h-feed's children
        let h_feed = create_test_h_feed(Some((
            "author",
            PropertyValue::Url(
                "https://feed-author.com/"
                    .parse::<url::Url>()
                    .unwrap()
                    .into(),
            ),
        )));

        let h_entry = create_test_h_entry(None); // Entry has no author
        // Entry is NOT added as child of feed

        let document = Document {
            rels: Default::default(),
            items: vec![h_feed],
            url: None,
            lang: None,
        };

        let author_property = extract_author_from_parent_feed(&h_entry, &document);
        assert!(
            author_property.is_none(),
            "Should return None when entry is not in h-feed's children"
        );
    }

    #[test]
    fn test_extract_author_from_h_card() {
        let h_card = create_test_h_card(
            "John Doe",
            "https://example.com/john",
            Some("https://example.com/john.jpg"),
        );
        let entry = create_test_h_entry(Some(("author", PropertyValue::Item(h_card))));

        let author_property = extract_author_from_entry(&entry);
        assert!(author_property.is_some());

        match author_property.unwrap() {
            AuthorProperty::HCard(_) => {} // Expected
            _ => panic!("Expected AuthorProperty::HCard"),
        }
    }

    #[test]
    fn test_extract_author_from_url() {
        let entry = create_test_h_entry(Some((
            "author",
            PropertyValue::Url(
                "https://example.com/author"
                    .parse::<url::Url>()
                    .unwrap()
                    .into(),
            ),
        )));

        let author_property = extract_author_from_entry(&entry);
        assert!(author_property.is_some());

        match author_property.unwrap() {
            AuthorProperty::Url(url) => {
                assert_eq!(url.as_str(), "https://example.com/author");
            }
            _ => panic!("Expected AuthorProperty::Url"),
        }
    }

    #[test]
    fn test_extract_author_from_name() {
        let entry = create_test_h_entry(Some((
            "author",
            PropertyValue::Plain("Jane Smith".to_string().into()),
        )));

        let author_property = extract_author_from_entry(&entry);
        assert!(author_property.is_some());

        match author_property.unwrap() {
            AuthorProperty::Name(name) => {
                assert_eq!(name, "Jane Smith");
            }
            _ => panic!("Expected AuthorProperty::Name"),
        }
    }

    #[test]
    fn test_extract_name_from_h_card() {
        let h_card = create_test_h_card("Test Author", "https://example.com", None);
        let name = extract_name_from_h_card(&h_card).unwrap();
        assert_eq!(name, "Test Author");
    }

    #[test]
    fn test_extract_url_from_h_card() {
        let h_card = create_test_h_card("Test Author", "https://example.com/author", None);
        let url = extract_url_from_h_card(&h_card).unwrap();
        assert_eq!(url.as_str(), "https://example.com/author");
    }

    #[test]
    fn test_extract_photo_from_h_card() {
        let h_card = create_test_h_card(
            "Test Author",
            "https://example.com/author",
            Some("https://example.com/photo.jpg"),
        );
        let photo = extract_photo_from_h_card(&h_card).unwrap();
        assert_eq!(photo.as_str(), "https://example.com/photo.jpg");
    }

    #[test]
    fn test_no_author_property() {
        let entry = create_test_h_entry(None);
        let author_property = extract_author_from_entry(&entry);
        assert!(author_property.is_none());
    }

    #[test]
    fn test_empty_h_entry() {
        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Event)], // Not an entry
            properties: BTreeMap::new(),
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        // This should not be able to extract author since it's not an h-entry
        let author_property = extract_author_from_entry(&entry);
        assert!(author_property.is_none());
    }

    // ========================================================================
    // Permalink Page Detection Tests (Step 6)
    // ========================================================================

    #[test]
    fn test_is_permalink_page_with_matching_u_url() {
        // Entry has u-url matching page URL → true
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "u-url".to_string(),
            vec![PropertyValue::Url(page_url.clone().into())],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            is_permalink_page(&entry, &page_url),
            "Should return true when u-url matches page URL"
        );
    }

    #[test]
    fn test_is_permalink_page_with_non_matching_u_url() {
        // Entry has u-url not matching page URL → false
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let different_url = Url::parse("https://example.com/post/456").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "u-url".to_string(),
            vec![PropertyValue::Url(different_url.into())],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            !is_permalink_page(&entry, &page_url),
            "Should return false when u-url does not match page URL"
        );
    }

    #[test]
    fn test_is_permalink_page_with_matching_url() {
        // Entry has url matching page URL → true
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "url".to_string(),
            vec![PropertyValue::Url(page_url.clone().into())],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            is_permalink_page(&entry, &page_url),
            "Should return true when url matches page URL"
        );
    }

    #[test]
    fn test_is_permalink_page_without_url() {
        // Entry has no u-url or url → false
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "content".to_string(),
            vec![PropertyValue::Plain("Some content".to_string().into())],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            !is_permalink_page(&entry, &page_url),
            "Should return false when entry has no u-url or url"
        );
    }

    #[test]
    fn test_is_permalink_page_with_plain_url() {
        // Entry has url as plain text string matching page URL → true
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "url".to_string(),
            vec![PropertyValue::Plain(
                "https://example.com/post/123".to_string().into(),
            )],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            is_permalink_page(&entry, &page_url),
            "Should return true when url as plain text matches page URL"
        );
    }

    #[test]
    fn test_is_permalink_page_with_plain_url_non_matching() {
        // Entry has url as plain text string not matching page URL → false
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "url".to_string(),
            vec![PropertyValue::Plain(
                "https://example.com/post/456".to_string().into(),
            )],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            !is_permalink_page(&entry, &page_url),
            "Should return false when url as plain text does not match page URL"
        );
    }

    #[test]
    fn test_is_permalink_page_with_invalid_plain_url() {
        // Entry has invalid plain text URL → false
        let page_url = Url::parse("https://example.com/post/123").unwrap();
        let mut properties = BTreeMap::new();
        properties.insert(
            "url".to_string(),
            vec![PropertyValue::Plain("not a valid url".to_string().into())],
        );

        let entry = Item {
            r#type: vec![Class::Known(KnownClass::Entry)],
            properties,
            value: None,
            children: Vec::new().into(),
            id: None,
            lang: None,
        };

        assert!(
            !is_permalink_page(&entry, &page_url),
            "Should return false when url is invalid plain text"
        );
    }
}
