use std::collections::{BTreeMap, HashMap};

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::algorithms::ptd;
use crate::mf2::types;

/// Microsub protocol implementation
///
/// This module provides a complete implementation of the Microsub specification,
/// allowing servers to manage feed subscriptions, normalize content to JF2 format,
/// and provide timeline-based feed consumption for clients.
/// Errors that can occur during Microsub operations
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum MicrosubError {
    #[error("Channel not found: {0}")]
    #[diagnostic(code(microsub::channel_not_found))]
    ChannelNotFound(String),

    #[error("Feed not found: {0}")]
    #[diagnostic(code(microsub::feed_not_found))]
    FeedNotFound(String),

    #[error("Entry not found: {0}")]
    #[diagnostic(code(microsub::entry_not_found))]
    EntryNotFound(String),

    #[error("Invalid URL: {0}")]
    #[diagnostic(code(microsub::invalid_url))]
    InvalidUrl(String),

    #[error("HTTP error: {0}")]
    #[diagnostic(code(microsub::http_error))]
    Http(String),

    #[error("Network error: {0}")]
    #[diagnostic(code(microsub::network_error))]
    Network(#[from] Box<dyn std::error::Error + Send + Sync>),
}

/// Result type for Microsub operations
pub type Result<T> = std::result::Result<T, MicrosubError>;

/// A Microsub channel
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Channel {
    /// Unique identifier for the channel
    pub uid: String,
    /// Display name for the channel
    pub name: String,
}

/// A Microsub feed
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Feed {
    /// Type, always "feed"
    #[serde(rename = "type")]
    pub feed_type: String,
    /// URL of the feed
    pub url: String,
    /// Display name of the feed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Photo/avatar for the feed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo: Option<String>,
    /// Description of the feed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Author information
    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<Card>,
}

/// A JF2 card representing a person or place
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Card {
    /// Type, "card"
    #[serde(rename = "type")]
    pub card_type: String,
    /// Name of the person/place
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// URL/profile URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// Photo/avatar
    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo: Option<String>,
}

/// A JF2 entry representing a post
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Entry {
    /// Type, "entry"
    #[serde(rename = "type")]
    pub entry_type: String,
    /// Published timestamp
    #[serde(skip_serializing_if = "Option::is_none")]
    pub published: Option<String>,
    /// Permalink URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    /// Unique ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uid: Option<String>,
    /// Title/name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Content
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<Content>,
    /// Summary
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    /// Author
    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<Card>,
    /// Categories/tags
    #[serde(skip_serializing_if = "Option::is_none")]
    pub category: Option<Vec<String>>,
    /// Photos
    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo: Option<Vec<String>>,
    /// Videos
    #[serde(skip_serializing_if = "Option::is_none")]
    pub video: Option<Vec<String>>,
    /// Audio
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audio: Option<Vec<String>>,
    /// Syndication URLs
    #[serde(skip_serializing_if = "Option::is_none")]
    pub syndication: Option<Vec<String>>,
    /// Checkin location
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checkin: Option<Card>,
    /// Like-of URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub like_of: Option<Vec<String>>,
    /// Repost-of URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repost_of: Option<Vec<String>>,
    /// Bookmark-of URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bookmark_of: Option<Vec<String>>,
    /// In-reply-to URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub in_reply_to: Option<Vec<String>>,
    /// Post type classification (article/note/photo/video/audio/etc.)
    #[serde(rename = "_kind")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    /// Internal ID (server-generated)
    #[serde(rename = "_id")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _id: Option<String>,
    /// Read status
    #[serde(rename = "_is_read")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub _is_read: Option<bool>,
}

/// Content of an entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Content {
    /// Plain text content
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
    /// HTML content
    #[serde(skip_serializing_if = "Option::is_none")]
    pub html: Option<String>,
}

/// Timeline response
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TimelineResponse {
    /// List of entries
    pub items: Vec<Entry>,
    /// Pagination information
    pub paging: Paging,
}

/// Search response
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SearchResponse {
    /// List of matching feeds
    pub results: Vec<Feed>,
}

/// Preview response (same structure as timeline)
pub type PreviewResponse = TimelineResponse;

/// Channels response
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChannelsResponse {
    /// List of channels
    pub channels: Vec<Channel>,
}

/// Followed feeds response
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FollowResponse {
    /// List of followed feeds
    pub items: Vec<Feed>,
}

/// Muted/blocked users response
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MutedResponse {
    /// List of muted/blocked cards
    pub items: Vec<Card>,
}

/// Pagination information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Paging {
    /// Cursor for previous page
    #[serde(skip_serializing_if = "Option::is_none")]
    pub before: Option<String>,
    /// Cursor for next page
    #[serde(skip_serializing_if = "Option::is_none")]
    pub after: Option<String>,
}

/// Trait for Microsub server implementations
///
/// Consumers can implement this trait to provide their own storage
/// and feed management logic (e.g., database-backed, in-memory, etc.)
#[async_trait]
pub trait MicrosubServer {
    /// Get all channels for the user
    async fn channels(&self) -> Result<ChannelsResponse>;

    /// Create a new channel
    async fn create_channel(&mut self, name: &str) -> Result<Channel>;

    /// Update a channel
    async fn update_channel(&mut self, uid: &str, name: &str) -> Result<Channel>;

    /// Delete a channel
    async fn delete_channel(&mut self, uid: &str) -> Result<()>;

    /// Set channel order
    async fn order_channels(&mut self, channel_uids: &[String]) -> Result<()>;

    /// Get timeline for a channel
    async fn timeline(
        &self,
        channel: &str,
        before: Option<&str>,
        after: Option<&str>,
        limit: Option<usize>,
        source: Option<&str>,
    ) -> Result<TimelineResponse>;

    /// Mark entries as read
    async fn mark_read(
        &mut self,
        channel: &str,
        entry_ids: &[String],
        last_read_entry: Option<&str>,
    ) -> Result<()>;

    /// Remove entry from channel
    async fn remove_entry(&mut self, channel: &str, entry_id: &str) -> Result<()>;

    /// Search for feeds
    async fn search(&self, query: &str) -> Result<SearchResponse>;

    /// Preview a feed URL
    async fn preview(&self, url: &str) -> Result<PreviewResponse>;

    /// Get followed feeds for a channel
    async fn followed(&self, channel: &str) -> Result<FollowResponse>;

    /// Follow a feed in a channel
    async fn follow(&mut self, channel: &str, url: &str) -> Result<Feed>;

    /// Unfollow a feed from a channel
    async fn unfollow(&mut self, channel: &str, url: &str) -> Result<()>;

    /// Get muted users for a channel
    async fn muted(&self, channel: &str) -> Result<MutedResponse>;

    /// Mute a user in a channel
    async fn mute(&mut self, channel: &str, url: &str) -> Result<()>;

    /// Unmute a user in a channel
    async fn unmute(&mut self, channel: &str, url: &str) -> Result<()>;

    /// Get blocked users for a channel
    async fn blocked(&self, channel: &str) -> Result<MutedResponse>;

    /// Block a user in a channel
    async fn block(&mut self, channel: &str, url: &str) -> Result<()>;

    /// Unblock a user in a channel
    async fn unblock(&mut self, channel: &str, url: &str) -> Result<()>;
}

/// In-memory Microsub server implementation for testing and simple applications
///
/// This implementation stores all data in memory using HashMaps.
/// It's suitable for testing, development, and simple applications
/// that don't require persistence across restarts.
#[derive(Debug)]
pub struct InMemoryMicrosub {
    channels: HashMap<String, Channel>,
    channel_feeds: HashMap<String, Vec<Feed>>,
    channel_entries: HashMap<String, Vec<EntryWithSource>>,
    channel_muted: HashMap<String, Vec<Card>>,
    channel_blocked: HashMap<String, Vec<Card>>,
    next_channel_id: u32,
}

/// Internal entry with source tracking for filtering
#[derive(Debug, Clone)]
struct EntryWithSource {
    entry: Entry,
    source_url: String, // Feed URL this entry came from
}

impl InMemoryMicrosub {
    /// Add an entry to a channel with source tracking (for testing)
    pub fn add_entry_with_source(&mut self, channel: &str, mut entry: Entry, source_url: &str) {
        // Process entry with PTD to determine post type
        let mf2_item = types::Item {
            r#type: vec![types::Class::Known(types::KnownClass::Entry)],
            properties: {
                let mut props = BTreeMap::new();
                if let Some(name) = &entry.name {
                    props.insert("name".to_string(), vec![types::PropertyValue::Plain(name.clone().into())]);
                }
                if let Some(content) = &entry.content
                    && let Some(text) = &content.text {
                        props.insert("content".to_string(), vec![types::PropertyValue::Plain(text.clone().into())]);
                    }
                if entry.like_of.is_some() {
                    props.insert("like-of".to_string(), vec![types::PropertyValue::Plain("https://example.com/liked".to_string().into())]);
                }
                if entry.repost_of.is_some() {
                    props.insert("repost-of".to_string(), vec![types::PropertyValue::Plain("https://example.com/reposted".to_string().into())]);
                }
                if entry.in_reply_to.is_some() {
                    props.insert("in-reply-to".to_string(), vec![types::PropertyValue::Plain("https://example.com/replied".to_string().into())]);
                }
                props
            },
            ..Default::default()
        };

        entry.kind = ptd::resolve_from_object(mf2_item).map(|t| t.to_string());

        self.channel_entries.entry(channel.to_string())
            .or_default()
            .push(EntryWithSource {
                entry,
                source_url: source_url.to_string(),
            });
    }

    /// Create a new in-memory Microsub server
    pub fn new() -> Self {
        let mut server = Self {
            channels: HashMap::new(),
            channel_feeds: HashMap::new(),
            channel_entries: HashMap::new(),
            channel_muted: HashMap::new(),
            channel_blocked: HashMap::new(),
            next_channel_id: 1,
        };

        // Always have a notifications channel
        let notifications = Channel {
            uid: "notifications".to_string(),
            name: "Notifications".to_string(),
        };
        server.channels.insert(notifications.uid.clone(), notifications);

        server
    }

    fn generate_channel_uid(&mut self) -> String {
        let uid = format!("channel_{}", self.next_channel_id);
        self.next_channel_id += 1;
        uid
    }
}

impl Default for InMemoryMicrosub {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl MicrosubServer for InMemoryMicrosub {
    async fn channels(&self) -> Result<ChannelsResponse> {
        let mut channels: Vec<_> = self.channels.values().cloned().collect();
        // Ensure notifications is first
        channels.sort_by(|a, b| {
            if a.uid == "notifications" {
                std::cmp::Ordering::Less
            } else if b.uid == "notifications" {
                std::cmp::Ordering::Greater
            } else {
                a.uid.cmp(&b.uid)
            }
        });
        Ok(ChannelsResponse { channels })
    }

    async fn create_channel(&mut self, name: &str) -> Result<Channel> {
        let uid = self.generate_channel_uid();
        let channel = Channel {
            uid: uid.clone(),
            name: name.to_string(),
        };
        self.channels.insert(uid, channel.clone());
        Ok(channel)
    }

    async fn update_channel(&mut self, uid: &str, name: &str) -> Result<Channel> {
        if let Some(channel) = self.channels.get_mut(uid) {
            channel.name = name.to_string();
            Ok(channel.clone())
        } else {
            Err(MicrosubError::ChannelNotFound(uid.to_string()))
        }
    }

    async fn delete_channel(&mut self, uid: &str) -> Result<()> {
        if uid == "notifications" {
            return Err(MicrosubError::ChannelNotFound(uid.to_string()));
        }
        self.channels.remove(uid);
        self.channel_feeds.remove(uid);
        self.channel_entries.remove(uid);
        self.channel_muted.remove(uid);
        self.channel_blocked.remove(uid);
        Ok(())
    }

    async fn order_channels(&mut self, _channel_uids: &[String]) -> Result<()> {
        // In-memory implementation doesn't persist order
        Ok(())
    }

    async fn timeline(
        &self,
        channel: &str,
        _before: Option<&str>,
        _after: Option<&str>,
        _limit: Option<usize>,
        source: Option<&str>,
    ) -> Result<TimelineResponse> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        let entries_with_source = self.channel_entries.get(channel)
            .cloned()
            .unwrap_or_default();

        // Filter by source if provided
        let filtered_entries: Vec<Entry> = if let Some(source_url) = source {
            entries_with_source.into_iter()
                .filter(|e| e.source_url == source_url)
                .map(|e| e.entry)
                .collect()
        } else {
            entries_with_source.into_iter()
                .map(|e| e.entry)
                .collect()
        };

        Ok(TimelineResponse {
            items: filtered_entries,
            paging: Paging {
                before: Some("cursor_before".to_string()),
                after: Some("cursor_after".to_string()),
            },
        })
    }

    async fn mark_read(
        &mut self,
        channel: &str,
        entry_ids: &[String],
        _last_read_entry: Option<&str>,
    ) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        if let Some(entries) = self.channel_entries.get_mut(channel) {
            for entry_with_source in entries {
                if let Some(id) = &entry_with_source.entry._id && entry_ids.contains(id) {
                    entry_with_source.entry._is_read = Some(true);
                }
            }
        }
        Ok(())
    }

    async fn remove_entry(&mut self, channel: &str, entry_id: &str) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        if let Some(entries) = self.channel_entries.get_mut(channel) {
            entries.retain(|e| e.entry._id.as_ref() != Some(&entry_id.to_string()));
        }
        Ok(())
    }

    async fn search(&self, _query: &str) -> Result<SearchResponse> {
        // Return sample search results
        let results = vec![
            Feed {
                feed_type: "feed".to_string(),
                url: "https://example.com/feed".to_string(),
                name: Some("Example Feed".to_string()),
                photo: Some("https://example.com/avatar.jpg".to_string()),
                description: Some("A sample feed".to_string()),
                author: Some(Card {
                    card_type: "card".to_string(),
                    name: Some("Example Author".to_string()),
                    url: Some("https://example.com".to_string()),
                    photo: Some("https://example.com/avatar.jpg".to_string()),
                }),
            }
        ];
        Ok(SearchResponse { results })
    }

    async fn preview(&self, _url: &str) -> Result<PreviewResponse> {
        // Return sample preview
        let mut entry = Entry {
            entry_type: "entry".to_string(),
            published: Some("2024-01-01T10:00:00Z".to_string()),
            url: Some("https://example.com/post1".to_string()),
            name: Some("Preview Post".to_string()),
            content: Some(Content {
                text: Some("This is a preview post.".to_string()),
                html: Some("<p>This is a preview post.</p>".to_string()),
            }),
            author: Some(Card {
                card_type: "card".to_string(),
                name: Some("Example Author".to_string()),
                url: Some("https://example.com".to_string()),
                photo: Some("https://example.com/avatar.jpg".to_string()),
            }),
            _id: Some("preview_1".to_string()),
            _is_read: Some(false),
            uid: None,
            summary: None,
            category: None,
            photo: None,
            video: None,
            audio: None,
            syndication: None,
            checkin: None,
            like_of: None,
            repost_of: None,
            bookmark_of: None,
            in_reply_to: None,
            kind: None, // Will be set by PTD processing
        };

        // Integrate with PTD algorithm for real post type detection
        // Create MF2 representation from the entry data for PTD processing
        let mf2_item = types::Item {
            r#type: vec![types::Class::Known(types::KnownClass::Entry)],
            properties: {
                let mut props = BTreeMap::new();
                if let Some(name) = &entry.name {
                    props.insert("name".to_string(), vec![types::PropertyValue::Plain(name.clone().into())]);
                }
                if let Some(content) = &entry.content
                    && let Some(text) = &content.text {
                        props.insert("content".to_string(), vec![types::PropertyValue::Plain(text.clone().into())]);
                    }
                if entry.like_of.is_some() {
                    props.insert("like-of".to_string(), vec![types::PropertyValue::Plain("https://example.com/liked".to_string().into())]);
                }
                if entry.repost_of.is_some() {
                    props.insert("repost-of".to_string(), vec![types::PropertyValue::Plain("https://example.com/reposted".to_string().into())]);
                }
                if entry.in_reply_to.is_some() {
                    props.insert("in-reply-to".to_string(), vec![types::PropertyValue::Plain("https://example.com/replied".to_string().into())]);
                }
                props
            },
            ..Default::default()
        };

        // Use PTD algorithm to determine post type
        entry.kind = ptd::resolve_from_object(mf2_item).map(|t| t.to_string());

        let items = vec![entry];
        Ok(PreviewResponse {
            items,
            paging: Paging { before: None, after: None },
        })
    }

    async fn followed(&self, channel: &str) -> Result<FollowResponse> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        let items = self.channel_feeds.get(channel)
            .cloned()
            .unwrap_or_default();
        Ok(FollowResponse { items })
    }

    async fn follow(&mut self, channel: &str, url: &str) -> Result<Feed> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        // Generate a better name from the URL
        let name = if let Some(domain) = url.split("://").nth(1) {
            let domain = domain.split('/').next().unwrap_or(domain);
            let domain = domain.split('.').next().unwrap_or(domain);
            Some(format!("{} Feed", domain.to_uppercase()))
        } else {
            Some("New Feed".to_string())
        };

        // For demonstration, add a placeholder photo for known domains
        let photo = if url.contains("github.com") {
            Some("https://github.com/favicon.ico".to_string())
        } else if url.contains("twitter.com") || url.contains("x.com") {
            Some("https://abs.twimg.com/icons/apple-touch-icon-192x192.png".to_string())
        } else {
            None
        };

        let feed = Feed {
            feed_type: "feed".to_string(),
            url: url.to_string(),
            name,
            photo,
            description: None,
            author: None,
        };

        self.channel_feeds.entry(channel.to_string())
            .or_default()
            .push(feed.clone());

        Ok(feed)
    }

    async fn unfollow(&mut self, channel: &str, url: &str) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        if let Some(feeds) = self.channel_feeds.get_mut(channel) {
            feeds.retain(|f| f.url != url);
        }
        Ok(())
    }

    async fn muted(&self, channel: &str) -> Result<MutedResponse> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        let items = self.channel_muted.get(channel)
            .cloned()
            .unwrap_or_default();
        Ok(MutedResponse { items })
    }

    async fn mute(&mut self, channel: &str, url: &str) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        let card = Card {
            card_type: "card".to_string(),
            name: Some("Muted User".to_string()),
            url: Some(url.to_string()),
            photo: None,
        };

        self.channel_muted.entry(channel.to_string())
            .or_default()
            .push(card);

        Ok(())
    }

    async fn unmute(&mut self, channel: &str, url: &str) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        if let Some(muted) = self.channel_muted.get_mut(channel) {
            muted.retain(|c| c.url.as_ref() != Some(&url.to_string()));
        }
        Ok(())
    }

    async fn blocked(&self, channel: &str) -> Result<MutedResponse> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        let items = self.channel_blocked.get(channel)
            .cloned()
            .unwrap_or_default();
        Ok(MutedResponse { items })
    }

    async fn block(&mut self, channel: &str, url: &str) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        let card = Card {
            card_type: "card".to_string(),
            name: Some("Blocked User".to_string()),
            url: Some(url.to_string()),
            photo: None,
        };

        self.channel_blocked.entry(channel.to_string())
            .or_default()
            .push(card);

        Ok(())
    }

    async fn unblock(&mut self, channel: &str, url: &str) -> Result<()> {
        if !self.channels.contains_key(channel) {
            return Err(MicrosubError::ChannelNotFound(channel.to_string()));
        }

        if let Some(blocked) = self.channel_blocked.get_mut(channel) {
            blocked.retain(|c| c.url.as_ref() != Some(&url.to_string()));
        }
        Ok(())
    }
}

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

    #[tokio::test]
    async fn test_create_channel() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test Channel").await.unwrap();

        assert_eq!(channel.name, "Test Channel");
        assert!(channel.uid.starts_with("channel_"));
    }

    #[tokio::test]
    async fn test_channels_includes_notifications() {
        let server = InMemoryMicrosub::new();
        let response = server.channels().await.unwrap();

        assert!(response.channels.iter().any(|c| c.uid == "notifications"));
    }

    #[tokio::test]
    async fn test_follow_feed() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        let feed = server.follow(&channel.uid, "https://example.com/feed").await.unwrap();
        assert_eq!(feed.url, "https://example.com/feed");

        let followed = server.followed(&channel.uid).await.unwrap();
        assert_eq!(followed.items.len(), 1);
        assert_eq!(followed.items[0].url, "https://example.com/feed");
    }

    #[tokio::test]
    async fn test_timeline_returns_empty_for_new_channel() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        let timeline = server.timeline(&channel.uid, None, None, None, None).await.unwrap();
        assert_eq!(timeline.items.len(), 0);
        assert!(timeline.paging.before.is_some());
        assert!(timeline.paging.after.is_some());
    }

    #[tokio::test]
    async fn test_search_returns_results() {
        let server = InMemoryMicrosub::new();
        let response = server.search("example").await.unwrap();

        assert!(!response.results.is_empty());
        assert_eq!(response.results[0].feed_type, "feed");
    }

    #[tokio::test]
    async fn test_preview_returns_entries() {
        let server = InMemoryMicrosub::new();
        let response = server.preview("https://example.com").await.unwrap();

        assert!(!response.items.is_empty());
        assert_eq!(response.items[0].entry_type, "entry");
    }

    #[tokio::test]
    async fn test_preview_returns_entries_with_kind() {
        let server = InMemoryMicrosub::new();
        let response = server.preview("https://example.com").await.unwrap();

        assert!(!response.items.is_empty());
        let entry = &response.items[0];
        // PTD correctly identifies this as "article" since it has both name and content
        assert_eq!(entry.kind, Some("article".to_string()));
    }

    #[tokio::test]
    async fn test_ptd_detects_different_post_types() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        // Test note (content only)
        let note_entry = Entry {
            entry_type: "entry".to_string(),
            content: Some(Content {
                text: Some("This is just a note".to_string()),
                html: None,
            }),
            uid: None,
            published: None,
            url: None,
            name: None,
            summary: None,
            author: None,
            category: None,
            photo: None,
            video: None,
            audio: None,
            syndication: None,
            checkin: None,
            like_of: None,
            repost_of: None,
            bookmark_of: None,
            in_reply_to: None,
            _id: Some("note_entry".to_string()),
            _is_read: Some(false),
            kind: None,
        };

        // Test like
        let like_entry = Entry {
            entry_type: "entry".to_string(),
            like_of: Some(vec!["https://example.com/liked-post".to_string()]),
            uid: None,
            published: None,
            url: None,
            name: None,
            summary: None,
            author: None,
            category: None,
            photo: None,
            video: None,
            audio: None,
            syndication: None,
            checkin: None,
            repost_of: None,
            bookmark_of: None,
            in_reply_to: None,
            _id: Some("like_entry".to_string()),
            _is_read: Some(false),
            kind: None,
            content: None,
        };

        // Test reply
        let reply_entry = Entry {
            entry_type: "entry".to_string(),
            in_reply_to: Some(vec!["https://example.com/replied-post".to_string()]),
            content: Some(Content {
                text: Some("This is a reply".to_string()),
                html: None,
            }),
            uid: None,
            published: None,
            url: None,
            name: None,
            summary: None,
            author: None,
            category: None,
            photo: None,
            video: None,
            audio: None,
            syndication: None,
            checkin: None,
            like_of: None,
            repost_of: None,
            bookmark_of: None,
            _id: Some("reply_entry".to_string()),
            _is_read: Some(false),
            kind: None,
        };

        // Add entries and check their post types
        server.add_entry_with_source(&channel.uid, note_entry, "https://feed1.com/feed.xml");
        server.add_entry_with_source(&channel.uid, like_entry, "https://feed2.com/feed.xml");
        server.add_entry_with_source(&channel.uid, reply_entry, "https://feed3.com/feed.xml");

        let timeline = server.timeline(&channel.uid, None, None, None, None).await.unwrap();
        assert_eq!(timeline.items.len(), 3);

        // Find entries by ID and check their kinds
        let note = timeline.items.iter().find(|e| e._id == Some("note_entry".to_string())).unwrap();
        assert_eq!(note.kind, Some("note".to_string()));

        let like = timeline.items.iter().find(|e| e._id == Some("like_entry".to_string())).unwrap();
        assert_eq!(like.kind, Some("like".to_string()));

        let reply = timeline.items.iter().find(|e| e._id == Some("reply_entry".to_string())).unwrap();
        assert_eq!(reply.kind, Some("reply".to_string()));
    }

    #[tokio::test]
    async fn test_timeline_filters_by_source() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        // Add entries from different sources
        let entry1 = Entry {
            entry_type: "entry".to_string(),
            published: Some("2024-01-01T10:00:00Z".to_string()),
            url: Some("https://feed1.com/post1".to_string()),
            name: Some("Post from Feed 1".to_string()),
            content: Some(Content {
                text: Some("Content from feed 1".to_string()),
                html: None,
            }),
            author: Some(Card {
                card_type: "card".to_string(),
                name: Some("Author 1".to_string()),
                url: Some("https://feed1.com".to_string()),
                photo: None,
            }),
            _id: Some("entry1".to_string()),
            _is_read: Some(false),
            uid: None,
            summary: None,
            category: None,
            photo: None,
            video: None,
            audio: None,
            syndication: None,
            checkin: None,
            like_of: None,
            repost_of: None,
            bookmark_of: None,
            in_reply_to: None,
            kind: Some("note".to_string()),
        };

        let entry2 = Entry {
            entry_type: "entry".to_string(),
            published: Some("2024-01-01T11:00:00Z".to_string()),
            url: Some("https://feed2.com/post1".to_string()),
            name: Some("Post from Feed 2".to_string()),
            content: Some(Content {
                text: Some("Content from feed 2".to_string()),
                html: None,
            }),
            author: Some(Card {
                card_type: "card".to_string(),
                name: Some("Author 2".to_string()),
                url: Some("https://feed2.com".to_string()),
                photo: None,
            }),
            _id: Some("entry2".to_string()),
            _is_read: Some(false),
            uid: None,
            summary: None,
            category: None,
            photo: None,
            video: None,
            audio: None,
            syndication: None,
            checkin: None,
            like_of: None,
            repost_of: None,
            bookmark_of: None,
            in_reply_to: None,
            kind: Some("article".to_string()),
        };

        server.add_entry_with_source(&channel.uid, entry1, "https://feed1.com/feed.xml");
        server.add_entry_with_source(&channel.uid, entry2, "https://feed2.com/feed.xml");

        // Test: Get all entries (no source filter)
        let timeline = server.timeline(&channel.uid, None, None, None, None).await.unwrap();
        assert_eq!(timeline.items.len(), 2);

        // Test: Filter by feed1
        let timeline_feed1 = server.timeline(&channel.uid, None, None, None, Some("https://feed1.com/feed.xml")).await.unwrap();
        assert_eq!(timeline_feed1.items.len(), 1);
        assert_eq!(timeline_feed1.items[0]._id, Some("entry1".to_string()));

        // Test: Filter by feed2
        let timeline_feed2 = server.timeline(&channel.uid, None, None, None, Some("https://feed2.com/feed.xml")).await.unwrap();
        assert_eq!(timeline_feed2.items.len(), 1);
        assert_eq!(timeline_feed2.items[0]._id, Some("entry2".to_string()));

        // Test: Filter by non-existent feed
        let timeline_empty = server.timeline(&channel.uid, None, None, None, Some("https://nonexistent.com/feed.xml")).await.unwrap();
        assert_eq!(timeline_empty.items.len(), 0);
    }

    #[tokio::test]
    async fn test_feed_metadata_enhancement() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        // Test feed with GitHub URL
        let github_feed = server.follow(&channel.uid, "https://github.com/user/blog.atom").await.unwrap();
        assert_eq!(github_feed.name, Some("GITHUB Feed".to_string()));
        assert_eq!(github_feed.photo, Some("https://github.com/favicon.ico".to_string()));

        // Test feed with Twitter URL
        let twitter_feed = server.follow(&channel.uid, "https://twitter.com/user/rss").await.unwrap();
        assert_eq!(twitter_feed.name, Some("TWITTER Feed".to_string()));
        assert_eq!(twitter_feed.photo, Some("https://abs.twimg.com/icons/apple-touch-icon-192x192.png".to_string()));

        // Test feed with generic URL
        let generic_feed = server.follow(&channel.uid, "https://example.com/feed.xml").await.unwrap();
        assert_eq!(generic_feed.name, Some("EXAMPLE Feed".to_string()));
        assert_eq!(generic_feed.photo, None);

        // Verify feeds are returned with metadata
        let followed = server.followed(&channel.uid).await.unwrap();
        assert_eq!(followed.items.len(), 3);

        let github = followed.items.iter().find(|f| f.url.contains("github.com")).unwrap();
        assert_eq!(github.name, Some("GITHUB Feed".to_string()));
        assert_eq!(github.photo, Some("https://github.com/favicon.ico".to_string()));
    }

    #[tokio::test]
    async fn test_mute_user() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        server.mute(&channel.uid, "https://bad.example.com").await.unwrap();

        let muted = server.muted(&channel.uid).await.unwrap();
        assert_eq!(muted.items.len(), 1);
        assert_eq!(muted.items[0].url, Some("https://bad.example.com".to_string()));
    }

    #[tokio::test]
    async fn test_block_user() {
        let mut server = InMemoryMicrosub::new();
        let channel = server.create_channel("Test").await.unwrap();

        server.block(&channel.uid, "https://spam.example.com").await.unwrap();

        let blocked = server.blocked(&channel.uid).await.unwrap();
        assert_eq!(blocked.items.len(), 1);
        assert_eq!(blocked.items[0].url, Some("https://spam.example.com".to_string()));
    }

    #[tokio::test]
    async fn test_channel_not_found_error() {
        let server = InMemoryMicrosub::new();
        let result = server.followed("nonexistent").await;

        assert!(matches!(result, Err(MicrosubError::ChannelNotFound(_))));
    }
}