//! Hand-rolled ActivityPub vocabulary types.
//!
//! No `activitystreams` crates (unmaintained); hand-rolled fits the codebase.
//! Fields are snake_case in Rust, renamed to camelCase for JSON.

use serde::{Deserialize, Serialize};

/// The ActivityStreams and Security contexts.
pub const CONTEXT: [&str; 2] = [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
];

/// The ForgeFed context including ActivityStreams, Security, and ForgeFed.
pub const FORGEFED_CONTEXT: [&str; 3] = [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    "https://forgefed.org/ns",
];

/// An ActivityPub actor (Person for users, Repository for repositories).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Actor {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub preferred_username: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    pub inbox: String,
    pub outbox: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub followers: Option<String>,
    pub public_key: PublicKeyDoc,
    /// ForgeFed: owner's Person actor URL (for Repository actors).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attributed_to: Option<String>,
    /// ForgeFed: HTTP clone URL (for Repository actors).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub clone_uri: Option<String>,
    /// ForgeFed: array of push URLs - HTTP and SSH (for Repository actors).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub push_uri: Option<Vec<String>>,
    /// ForgeFed: creation timestamp in RFC3339 format (for Repository actors).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub published: Option<String>,
    /// ForgeFed: ticket tracker actor URL (self-referential for repos that
    /// track their own tickets).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tickets_tracked_by: Option<String>,
    /// ForgeFed: patch tracker actor URL (self-referential for repos that track
    /// their own patches).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub send_patches_to: Option<String>,
    /// ForgeFed: repositories whose tickets this actor tracks (bidirectional
    /// back-reference).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tracks_tickets_for: Option<Vec<String>>,
    /// ForgeFed: repositories whose patches this actor tracks (bidirectional
    /// back-reference).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tracks_patches_for: Option<Vec<String>>,
}

/// A public key document, embedded in an actor.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyDoc {
    pub id: String,
    pub owner: String,
    pub public_key_pem: String,
}

/// An ActivityPub attachment (Document object).
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Attachment {
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

/// An ActivityPub Note.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub attributed_to: String,
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub media_type: Option<String>,
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub in_reply_to: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<NoteSource>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attachment: Option<Vec<Attachment>>,
    pub published: String,
    pub to: Vec<String>,
    pub cc: Vec<String>,
}

/// Source content for a Note (original markdown before rendering to HTML).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoteSource {
    pub media_type: String,
    pub content: String,
}

/// A Create activity wrapping a Note.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Create {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: Note,
    pub published: String,
    pub to: Vec<String>,
    pub cc: Vec<String>,
}

/// An Accept activity (used for Follow acceptance and ForgeFed responses).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Accept {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: serde_json::Value,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub published: Option<String>,
}

/// Inbound activities the inbox accepts. Unknown types are tolerated.
#[derive(Deserialize)]
#[serde(tag = "type")]
pub enum InboxActivity {
    Follow(FollowActivity),
    Undo(UndoActivity),
    Like(ReactionActivity),
    Announce(AnnounceActivity),
    EmojiReact(ReactionActivity),
    Accept(AcceptRejectActivity),
    Reject(AcceptRejectActivity),
    Create(CreateActivity),
    Update(UpdateActivity),
    Delete(DeleteActivity),
    Offer(OfferActivity),
    Resolve(ResolveActivity),
    Edit(EditActivity),
    Add(AddActivity),
    #[serde(other)]
    Unknown,
}

/// A Follow activity.
#[derive(Deserialize)]
pub struct FollowActivity {
    pub id: String,
    pub actor: String,
    pub object: ObjectRef,
}

/// An Undo activity.
#[derive(Deserialize)]
pub struct UndoActivity {
    pub id: String,
    pub actor: String,
    pub object: Box<UndoObject>,
}

/// A reaction (Like, Announce, EmojiReact).
#[derive(Deserialize)]
pub struct ReactionActivity {
    pub id: String,
    pub actor: String,
    pub object: ObjectRef,
    #[serde(default)]
    pub content: Option<String>,
}

/// An Announce activity (boost) that can contain an embedded Note.
#[derive(Deserialize)]
pub struct AnnounceActivity {
    pub id: String,
    pub actor: String,
    pub object: AnnounceObject,
}

/// Announce.object can be a URL or an embedded Note with full content.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum AnnounceObject {
    Url(String),
    Embedded(AnnounceEmbedded),
}

impl AnnounceObject {
    pub fn id(&self) -> &str {
        match self {
            AnnounceObject::Url(url) => url,
            AnnounceObject::Embedded(embedded) => &embedded.id,
        }
    }
}

/// An embedded object in an Announce activity (e.g., a Note).
#[derive(Deserialize)]
pub struct AnnounceEmbedded {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub published: Option<String>,
    #[serde(default)]
    pub attributed_to: Option<String>,
}

/// An Accept or Reject activity (used for Follow responses).
#[derive(Deserialize)]
pub struct AcceptRejectActivity {
    pub id: String,
    pub actor: String,
    pub object: ObjectRef,
    #[serde(default)]
    pub result: Option<String>,
}

/// An object field is either a URL string or an embedded document.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum ObjectRef {
    Url(String),
    Embedded(EmbeddedObject),
}

impl ObjectRef {
    pub fn id(&self) -> &str {
        match self {
            ObjectRef::Url(url) => url,
            ObjectRef::Embedded(embedded) => &embedded.id,
        }
    }
}

/// An embedded object with an id and type.
#[derive(Deserialize, Debug)]
pub struct EmbeddedObject {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub summary: Option<String>,
    #[serde(default)]
    pub content: Option<String>,
}

/// Undo.object is the activity being undone.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum UndoObject {
    Embedded(UndoEmbedded),
    Url(String),
}

impl UndoObject {
    pub fn id(&self) -> &str {
        match self {
            UndoObject::Embedded(embedded) => &embedded.id,
            UndoObject::Url(url) => url,
        }
    }

    pub fn kind(&self) -> &str {
        match self {
            UndoObject::Embedded(embedded) => &embedded.kind,
            UndoObject::Url(_) => "Unknown",
        }
    }
}

/// An embedded object being undone.
#[derive(Deserialize)]
pub struct UndoEmbedded {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub object: Option<ObjectRef>,
}

/// A Create activity (e.g., creating a Note).
#[derive(Deserialize)]
pub struct CreateActivity {
    pub id: String,
    pub actor: String,
    pub object: CreateObject,
}

/// Create.object can be an embedded object (like a Note) or a URL.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CreateObject {
    Url(String),
    Embedded(CreateEmbedded),
}

impl CreateObject {
    pub fn id(&self) -> &str {
        match self {
            CreateObject::Url(url) => url,
            CreateObject::Embedded(embedded) => &embedded.id,
        }
    }
}

/// An embedded object in a Create activity (e.g., a Note).
#[derive(Deserialize)]
pub struct CreateEmbedded {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub published: Option<String>,
    #[serde(default)]
    pub attributed_to: Option<String>,
    #[serde(default, rename = "inReplyTo")]
    pub in_reply_to: Option<String>,
    #[serde(default)]
    pub source: Option<NoteSourceIn>,
    #[serde(default)]
    pub attachment: Option<Vec<crate::activitypub::types::Attachment>>,
}

/// An Update activity (e.g., updating a Note).
#[derive(Deserialize)]
pub struct UpdateActivity {
    pub id: String,
    pub actor: String,
    pub object: UpdateObject,
}

/// Update.object can be an embedded object (like a Note) or a URL.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum UpdateObject {
    Url(String),
    Embedded(UpdateEmbedded),
}

impl UpdateObject {
    pub fn id(&self) -> &str {
        match self {
            UpdateObject::Url(url) => url,
            UpdateObject::Embedded(embedded) => &embedded.id,
        }
    }
}

/// An embedded object in an Update activity (e.g., a Note).
#[derive(Deserialize)]
pub struct UpdateEmbedded {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub published: Option<String>,
    #[serde(default)]
    pub attachment: Option<Vec<crate::activitypub::types::Attachment>>,
}

/// A Delete activity (e.g., deleting a Note).
#[derive(Deserialize)]
pub struct DeleteActivity {
    pub id: String,
    pub actor: String,
    pub object: DeleteObject,
}

/// Delete.object can be an embedded object or a URL.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum DeleteObject {
    Url(String),
    Embedded(DeleteEmbedded),
}

impl DeleteObject {
    pub fn id(&self) -> &str {
        match self {
            DeleteObject::Url(url) => url,
            DeleteObject::Embedded(embedded) => &embedded.id,
        }
    }
}

/// An embedded object in a Delete activity.
#[derive(Deserialize)]
pub struct DeleteEmbedded {
    pub id: String,
    #[serde(rename = "type")]
    pub kind: String,
}

/// A ForgeFed Offer activity (for creating tickets and merge requests).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Offer {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: serde_json::Value,
    pub target: String,
    pub to: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    pub published: String,
}

/// A ForgeFed Resolve activity (for closing tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Resolve {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: String,
    pub to: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    pub published: String,
}

/// A ForgeFed Undo activity (for reopening tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Undo {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: Resolve,
    pub to: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    pub published: String,
}

/// A ForgeFed Edit activity (for editing tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Edit {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: serde_json::Value,
    pub to: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    pub published: String,
}

/// A ForgeFed Delete activity (for deleting tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Delete {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: String,
    pub to: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    pub published: String,
}

/// A ForgeFed Add activity (for patch set updates).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Add {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub object: serde_json::Value,
    pub target: String,
    pub to: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cc: Option<Vec<String>>,
    pub published: String,
}

// --- ForgeFed inbound activity types (Deserialize) ---

/// Inbound ForgeFed Offer activity (for receiving tickets and merge requests).
#[derive(Deserialize, Debug)]
pub struct OfferActivity {
    pub id: String,
    pub actor: String,
    pub object: OfferObject,
    #[serde(default)]
    pub target: Option<ObjectRef>,
}

/// Offer.object can be a URL (fetch via signed_get) or an embedded
/// Ticket/Patch.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum OfferObject {
    Url(String),
    Embedded(OfferTicketEmbed),
}

impl OfferObject {
    pub fn id(&self) -> Option<&str> {
        match self {
            OfferObject::Url(url) => Some(url),
            OfferObject::Embedded(embedded) => embedded.id.as_deref(),
        }
    }
}

/// An embedded Ticket or Patch object in an Offer activity.
#[derive(Deserialize, Debug)]
pub struct OfferTicketEmbed {
    #[serde(default)]
    pub id: Option<String>,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub attributed_to: Option<String>,
    #[serde(default)]
    pub summary: Option<String>,
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default, alias = "attachment")]
    pub attachment: Vec<MrAttachment>,
    #[serde(default, rename = "context")]
    pub tracker: Option<String>,
    #[serde(default)]
    pub replies: Option<String>,
    #[serde(default)]
    pub is_resolved: Option<bool>,
    #[serde(default)]
    pub media_type: Option<String>,
    #[serde(default)]
    pub published: Option<String>,
}

/// An attachment in a Ticket/Patch (e.g., a patch set or branch reference).
#[derive(Deserialize, Debug)]
pub struct MrAttachment {
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub media_type: Option<String>,
    #[serde(default)]
    pub url: Option<String>,
    #[serde(default)]
    pub branch: Option<BranchOrUrl>,
    #[serde(default)]
    pub content: Option<PatchCollectionIn>,
    #[serde(default)]
    pub author: Option<String>,
    #[serde(default)]
    pub commit_id: Option<String>,
}

/// A patch collection (ordered list of patches).
#[derive(Deserialize, Debug, Default)]
pub struct PatchCollectionIn {
    #[serde(default, alias = "orderedItems", alias = "items")]
    pub items: Vec<PatchIn>,
    #[serde(default)]
    pub total_items: Option<i64>,
}

/// A single patch in a patch collection.
#[derive(Deserialize, Debug)]
pub struct PatchIn {
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub id: Option<String>,
    #[serde(default)]
    pub author: Option<String>,
    #[serde(default)]
    pub commit_id: Option<String>,
    #[serde(default)]
    pub summary: Option<String>,
}

/// A branch reference or URL.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum BranchOrUrl {
    Branch(BranchRef),
    Url(String),
}

/// A branch reference with context, name, and git ref.
#[derive(Deserialize, Debug)]
pub struct BranchRef {
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub context: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default, rename = "ref")]
    pub git_ref: Option<String>,
}

/// Inbound ForgeFed Resolve activity (for closing tickets/patches).
#[derive(Deserialize, Debug)]
pub struct ResolveActivity {
    pub id: String,
    pub actor: String,
    pub object: ObjectRef,
}

/// Inbound ForgeFed Edit activity (for editing tickets/comments).
#[derive(Deserialize, Debug)]
pub struct EditActivity {
    pub id: String,
    pub actor: String,
    pub object: ObjectRef,
}

/// Inbound ForgeFed Add activity (for patch set updates).
#[derive(Deserialize, Debug)]
pub struct AddActivity {
    pub id: String,
    pub actor: String,
    pub object: ObjectRef,
    #[serde(default)]
    pub target: Option<ObjectRef>,
}

/// Source information for an inbound Note (markdown content).
#[derive(Deserialize, Debug)]
pub struct NoteSourceIn {
    #[serde(rename = "mediaType")]
    pub media_type: String,
    pub content: String,
}

/// A ForgeFed Ticket object (issue/tracker item).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Ticket {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub attributed_to: String,
    pub summary: String,
    pub content: String,
    pub media_type: String,
    pub source: TicketSource,
    #[serde(rename = "context")]
    pub tracker: String,
    pub is_resolved: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved_by: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attachment: Option<Vec<Attachment>>,
    pub replies: String,
    pub published: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub updated: Option<String>,
}

/// Source information for a Ticket (markdown content).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketSource {
    pub media_type: String,
    pub content: String,
}

/// A ForgeFed Patch (MergeRequest) - a Ticket with patch attachments.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Patch {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub attributed_to: String,
    pub summary: String,
    pub content: String,
    pub media_type: String,
    pub source: TicketSource,
    #[serde(rename = "context")]
    pub tracker: String,
    pub is_resolved: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resolved_by: Option<String>,
    pub replies: String,
    pub published: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub updated: Option<String>,
    pub attachment: PatchAttachment,
}

/// Patch attachment for a MergeRequest.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchAttachment {
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub origin: String,
    pub target: String,
    pub object: PatchCollection,
}

/// Collection of patches in a MergeRequest.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchCollection {
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub total_items: u64,
    pub ordered_items: Vec<PatchObject>,
}

/// Individual patch object in a collection.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchObject {
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub media_type: String,
    pub attributed_to: String,
    pub content: String,
}

/// A ForgeFed Commit object (served at /~{owner}/{repo}/commits/{hash}).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Commit {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attributed_to: Option<String>,
    pub hash: String,
    pub created: String,
    pub summary: String,
}

/// A ForgeFed Push activity (emitted when commits are pushed to a repository).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Push {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub actor: String,
    pub attributed_to: String,
    pub target: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hash_before: Option<String>,
    pub hash_after: String,
    pub object: PushCommitCollection,
    pub published: String,
    pub to: Vec<String>,
}

/// An OrderedCollection of commits embedded in a Push activity.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PushCommitCollection {
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub total_items: u64,
    pub ordered_items: Vec<Commit>,
}

/// An OrderedCollection (outbox, followers).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderedCollection {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub total_items: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ordered_items: Option<Vec<serde_json::Value>>,
}

/// An OrderedCollectionPage (outbox paging).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderedCollectionPage {
    #[serde(rename = "@context")]
    pub context: Vec<&'static str>,
    pub id: String,
    #[serde(rename = "type")]
    pub kind: &'static str,
    pub total_items: u64,
    pub part_of: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next: Option<String>,
    pub ordered_items: Vec<serde_json::Value>,
}

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

    #[test]
    fn deserialize_follow() {
        let json = r#"{
            "type": "Follow",
            "id": "https://remote.example/activities/123",
            "actor": "https://remote.example/users/bob",
            "object": "https://local.example/~alice"
        }"#;

        let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
        match activity {
            InboxActivity::Follow(f) => {
                assert_eq!(f.id, "https://remote.example/activities/123");
                assert_eq!(f.actor, "https://remote.example/users/bob");
                assert_eq!(f.object.id(), "https://local.example/~alice");
            }
            _ => panic!("expected Follow"),
        }
    }

    #[test]
    fn deserialize_emoji_react() {
        let json = r#"{
            "type": "EmojiReact",
            "id": "https://remote.example/activities/456",
            "actor": "https://remote.example/users/bob",
            "object": "https://local.example/~alice/notes/abc",
            "content": "🔥"
        }"#;

        let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
        match activity {
            InboxActivity::EmojiReact(r) => {
                assert_eq!(r.content, Some("🔥".to_string()));
            }
            _ => panic!("expected EmojiReact"),
        }
    }

    #[test]
    fn deserialize_undo_with_embedded() {
        let json = r#"{
            "type": "Undo",
            "id": "https://remote.example/activities/789",
            "actor": "https://remote.example/users/bob",
            "object": {
                "type": "Follow",
                "id": "https://remote.example/activities/123"
            }
        }"#;

        let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
        match activity {
            InboxActivity::Undo(u) => {
                assert_eq!(u.object.id(), "https://remote.example/activities/123");
                assert_eq!(u.object.kind(), "Follow");
            }
            _ => panic!("expected Undo"),
        }
    }

    #[test]
    fn unknown_type_becomes_unknown() {
        let json = r#"{
            "type": "Move",
            "id": "https://remote.example/activities/999",
            "actor": "https://remote.example/users/bob"
        }"#;

        let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
        assert!(matches!(activity, InboxActivity::Unknown));
    }
}
