use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::event_source::EventSourceMeta;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BindingContext {
pub agent_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub binding_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcp_channel_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_source: Option<EventSourceMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
}
impl BindingContext {
pub fn agent_only(agent_id: impl Into<String>) -> Self {
Self {
agent_id: agent_id.into(),
session_id: None,
channel: None,
account_id: None,
binding_id: None,
mcp_channel_source: None,
event_source: None,
tenant_id: None,
}
}
pub fn with_mcp_channel_source(mut self, source: impl Into<String>) -> Self {
self.mcp_channel_source = Some(source.into());
self
}
pub fn binding_id(&self) -> Option<&str> {
self.binding_id.as_deref()
}
}
pub fn binding_id_render(channel: &str, account_id: Option<&str>) -> String {
let account = account_id.unwrap_or("default");
format!("{channel}:{account}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_only_clears_optional_fields() {
let ctx = BindingContext::agent_only("ana");
assert_eq!(ctx.agent_id, "ana");
assert!(ctx.session_id.is_none());
assert!(ctx.channel.is_none());
assert!(ctx.account_id.is_none());
assert!(ctx.binding_id.is_none());
assert!(ctx.mcp_channel_source.is_none());
assert!(ctx.tenant_id.is_none());
}
#[test]
fn binding_context_round_trips_with_tenant_id() {
let mut ctx = BindingContext::agent_only("ana");
ctx.tenant_id = Some("acme-corp".into());
let v = serde_json::to_value(&ctx).unwrap();
assert_eq!(v["tenant_id"], serde_json::json!("acme-corp"));
let back: BindingContext = serde_json::from_value(v).unwrap();
assert_eq!(back.tenant_id, Some("acme-corp".into()));
}
#[test]
fn binding_context_deserializes_legacy_payload_without_tenant_id() {
let legacy = serde_json::json!({
"agent_id": "ana",
"channel": "whatsapp",
"account_id": "wa.0",
"binding_id": "whatsapp:wa.0"
});
let parsed: BindingContext = serde_json::from_value(legacy).unwrap();
assert_eq!(parsed.agent_id, "ana");
assert!(parsed.tenant_id.is_none());
}
#[test]
fn with_mcp_channel_source_sets_field() {
let ctx = BindingContext::agent_only("ana").with_mcp_channel_source("slack");
assert_eq!(ctx.mcp_channel_source.as_deref(), Some("slack"));
assert_eq!(ctx.agent_id, "ana");
}
#[test]
fn binding_id_accessor_returns_str_or_none() {
let mut ctx = BindingContext::agent_only("ana");
assert!(ctx.binding_id().is_none());
ctx.binding_id = Some("whatsapp:personal".into());
assert_eq!(ctx.binding_id(), Some("whatsapp:personal"));
}
#[test]
fn render_with_account_id() {
assert_eq!(
binding_id_render("whatsapp", Some("personal")),
"whatsapp:personal"
);
assert_eq!(
binding_id_render("telegram", Some("kate_tg")),
"telegram:kate_tg"
);
}
#[test]
fn render_without_account_id_uses_default_sentinel() {
assert_eq!(binding_id_render("whatsapp", None), "whatsapp:default");
}
#[test]
fn serialise_skips_none_fields() {
let ctx = BindingContext::agent_only("ana");
let json = serde_json::to_value(&ctx).unwrap();
let obj = json.as_object().unwrap();
assert!(obj.contains_key("agent_id"));
assert!(!obj.contains_key("session_id"));
assert!(!obj.contains_key("channel"));
assert!(!obj.contains_key("account_id"));
assert!(!obj.contains_key("binding_id"));
assert!(!obj.contains_key("mcp_channel_source"));
}
#[test]
fn round_trip_through_serde() {
let original = BindingContext::agent_only("carlos").with_mcp_channel_source("slack");
let json = serde_json::to_string(&original).unwrap();
let back: BindingContext = serde_json::from_str(&json).unwrap();
assert_eq!(original, back);
}
#[test]
fn clone_eq_holds_for_full_payload() {
let mut a = BindingContext::agent_only("ana");
a.session_id = Some(Uuid::nil());
a.channel = Some("whatsapp".into());
a.account_id = Some("personal".into());
a.binding_id = Some("whatsapp:personal".into());
a.mcp_channel_source = Some("slack".into());
let b = a.clone();
assert_eq!(a, b);
}
}