use crate::identity::AgentId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCard {
pub display_name: String,
pub agent_id: String,
pub machine_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(default)]
pub addresses: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<CardGroup>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stores: Vec<CardStore>,
pub created_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardGroup {
pub name: String,
pub invite_link: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardStore {
pub name: String,
pub topic: String,
}
impl AgentCard {
#[must_use]
pub fn new(display_name: String, agent_id: &AgentId, machine_id: &str) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
display_name,
agent_id: hex::encode(agent_id.as_bytes()),
machine_id: machine_id.to_string(),
user_id: None,
addresses: Vec::new(),
groups: Vec::new(),
stores: Vec::new(),
created_at: now,
}
}
#[must_use]
pub fn to_link(&self) -> String {
let json = serde_json::to_string(self).unwrap_or_default();
use base64::Engine;
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes());
format!("x0x://agent/{b64}")
}
pub fn from_link(link: &str) -> std::result::Result<Self, String> {
let b64 = link.strip_prefix("x0x://agent/").unwrap_or(link).trim();
use base64::Engine;
let json_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(b64)
.map_err(|e| format!("invalid base64: {e}"))?;
let json_str = String::from_utf8(json_bytes).map_err(|e| format!("invalid UTF-8: {e}"))?;
serde_json::from_str(&json_str).map_err(|e| format!("invalid card JSON: {e}"))
}
#[must_use]
pub fn short_display(&self) -> String {
let id_short = if self.agent_id.len() >= 8 {
&self.agent_id[..8]
} else {
&self.agent_id
};
format!("{} ({}…)", self.display_name, id_short)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agent(n: u8) -> AgentId {
AgentId([n; 32])
}
#[test]
fn test_new_card() {
let card = AgentCard::new("David".to_string(), &agent(1), &hex::encode([2u8; 32]));
assert_eq!(card.display_name, "David");
assert_eq!(card.agent_id.len(), 64);
assert_eq!(card.machine_id.len(), 64);
assert!(card.user_id.is_none());
assert!(card.addresses.is_empty());
assert!(card.groups.is_empty());
assert!(card.created_at > 0);
}
#[test]
fn test_link_roundtrip() {
let mut card = AgentCard::new("Alice".to_string(), &agent(1), &hex::encode([2u8; 32]));
card.user_id = Some(hex::encode([3u8; 32]));
card.addresses = vec!["1.2.3.4:5483".to_string()];
card.groups.push(CardGroup {
name: "Team".to_string(),
invite_link: "x0x://invite/abc123".to_string(),
});
card.stores.push(CardStore {
name: "Shared".to_string(),
topic: "shared-kv".to_string(),
});
let link = card.to_link();
assert!(link.starts_with("x0x://agent/"));
let restored = AgentCard::from_link(&link).expect("parse");
assert_eq!(card.display_name, restored.display_name);
assert_eq!(card.agent_id, restored.agent_id);
assert_eq!(card.machine_id, restored.machine_id);
assert_eq!(card.user_id, restored.user_id);
assert_eq!(card.addresses, restored.addresses);
assert_eq!(card.groups.len(), 1);
assert_eq!(card.stores.len(), 1);
}
#[test]
fn test_from_link_raw_base64() {
let card = AgentCard::new("Bob".to_string(), &agent(5), &hex::encode([6u8; 32]));
let link = card.to_link();
let raw = link.strip_prefix("x0x://agent/").expect("prefix");
let restored = AgentCard::from_link(raw).expect("parse raw");
assert_eq!(card.agent_id, restored.agent_id);
}
#[test]
fn test_from_link_invalid() {
assert!(AgentCard::from_link("garbage!!!").is_err());
}
#[test]
fn test_short_display() {
let card = AgentCard::new("David".to_string(), &agent(1), &hex::encode([2u8; 32]));
let display = card.short_display();
assert!(display.starts_with("David ("));
assert!(display.contains('…'));
}
#[test]
fn test_minimal_card_no_optional_fields() {
let card = AgentCard::new("Minimal".to_string(), &agent(1), &hex::encode([2u8; 32]));
let json = serde_json::to_string(&card).expect("serialize");
assert!(!json.contains("user_id"));
assert!(!json.contains("groups"));
assert!(!json.contains("stores"));
}
}