//! Focused raw integration coverage for the public tools and channels surfaces.
//!
//! These tests stay local-only: temp workspaces, in-memory adapters, and
//! payload parsing instead of real network calls.

use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use serde_json::{json, Map, Value};
use tempfile::tempdir;

use openhuman_core::openhuman::channels::controllers::{
    all_channel_definitions, all_channels_controller_schemas, all_channels_registered_controllers,
    find_channel_definition, ChannelAuthMode, ChannelCapability,
};
use openhuman_core::openhuman::channels::traits::{Channel, ChannelMessage, SendMessage};
use openhuman_core::openhuman::channels::yuanbao::config::YuanbaoConfig;
use openhuman_core::openhuman::channels::yuanbao::errors::{
    AUTH_FAILED_CODES, AUTH_RETRYABLE_CODES, NO_RECONNECT_CLOSE_CODES,
};
use openhuman_core::openhuman::channels::yuanbao::media::{
    build_file_msg_body, build_image_msg_body, guess_mime_type, image_format_code, is_image,
    parse_image_size,
};
use openhuman_core::openhuman::channels::yuanbao::proto::{
    decode_auth_bind_rsp, decode_conn_msg, decode_inbound_json, decode_inbound_push,
    decode_push_msg, encode_auth_bind, encode_conn_msg, encode_msg_body_element, encode_ping,
    encode_push_ack,
};
use openhuman_core::openhuman::channels::yuanbao::proto_constants::{cmd, cmd_type, module};
use openhuman_core::openhuman::channels::yuanbao::splitter::split_markdown;
use openhuman_core::openhuman::channels::yuanbao::types::{
    ConnFrame as YuanbaoConnFrame, MsgBodyElement as YuanbaoMsgBodyElement,
    MsgContent as YuanbaoMsgContent,
};
use openhuman_core::openhuman::channels::yuanbao::wire::{
    decode_varint, encode_field_bytes, encode_field_string, encode_field_varint, encode_varint,
    get_bytes, get_repeated_bytes, get_string, get_varint, next_seq_no, parse_fields, FieldValue,
};
use openhuman_core::openhuman::channels::{CliChannel, WhatsAppChannel};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::memory::{
    Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts,
};
use openhuman_core::openhuman::security::{AuditLogger, SecurityPolicy};
use openhuman_core::openhuman::tools::generated::{
    admit_generated_tool_definitions, generated_tools_from_definitions, GeneratedToolAdapter,
    GeneratedToolAdmissionConfig, GeneratedToolDefinition, GeneratedToolRisk,
};
use openhuman_core::openhuman::tools::local_cli::tools_wrappers_list_json;
use openhuman_core::openhuman::tools::{
    all_tools, all_tools_controller_schemas, all_tools_registered_controllers,
    decode_data_url_bytes, default_tools, extract_data_url, extract_saved_path,
    write_bytes_to_path, DefaultToolPolicy, PermissionLevel, PolicyDecision, ToolCategory,
    ToolPolicy, ToolResult, ToolScope,
};

#[path = "tools_approval_channels_raw_coverage_e2e.rs"]
mod prior_tools_approval_channels_raw_coverage_e2e;

#[derive(Default)]
struct StubMemory;

#[async_trait]
impl Memory for StubMemory {
    fn name(&self) -> &str {
        "tools-channels-stub"
    }

    async fn store(
        &self,
        _namespace: &str,
        _key: &str,
        _content: &str,
        _category: MemoryCategory,
        _session_id: Option<&str>,
    ) -> Result<()> {
        Ok(())
    }

    async fn recall(
        &self,
        _query: &str,
        _limit: usize,
        _opts: RecallOpts<'_>,
    ) -> Result<Vec<MemoryEntry>> {
        Ok(Vec::new())
    }

    async fn get(&self, _namespace: &str, _key: &str) -> Result<Option<MemoryEntry>> {
        Ok(None)
    }

    async fn list(
        &self,
        _namespace: Option<&str>,
        _category: Option<&MemoryCategory>,
        _session_id: Option<&str>,
    ) -> Result<Vec<MemoryEntry>> {
        Ok(Vec::new())
    }

    async fn forget(&self, _namespace: &str, _key: &str) -> Result<bool> {
        Ok(false)
    }

    async fn namespace_summaries(&self) -> Result<Vec<NamespaceSummary>> {
        Ok(Vec::new())
    }

    async fn count(&self) -> Result<usize> {
        Ok(0)
    }

    async fn health_check(&self) -> bool {
        true
    }
}

struct RecordingGeneratedAdapter;

#[async_trait]
impl GeneratedToolAdapter for RecordingGeneratedAdapter {
    fn id(&self) -> &str {
        "recording-adapter"
    }

    async fn execute(
        &self,
        definition: &GeneratedToolDefinition,
        args: Value,
    ) -> Result<ToolResult> {
        Ok(ToolResult::success(
            json!({
                "tool": definition.name,
                "adapter": definition.adapter_id,
                "args": args,
            })
            .to_string(),
        ))
    }
}

fn basic_generated_definition(name: &str) -> GeneratedToolDefinition {
    let mut definition = GeneratedToolDefinition::new(
        name,
        format!("Execute {name}"),
        json!({
            "type": "object",
            "properties": {
                "message": { "type": "string" }
            },
            "required": ["message"]
        }),
        "recording-adapter",
    );
    definition.provider_id = Some(" Trusted.Provider ".to_string());
    definition.capability_id = Some(format!("{name}.capability"));
    definition.source_digest = Some(format!("sha256:{name}"));
    definition.risk = Some(GeneratedToolRisk::Read);
    definition
}

fn temp_config() -> (tempfile::TempDir, Config) {
    let tmp = tempdir().expect("tempdir");
    let mut config = Config::default();
    config.workspace_dir = tmp.path().join("workspace");
    config.config_path = tmp.path().join("config.toml");
    config.node.enabled = false;
    config.browser.enabled = true;
    config.gitbooks.enabled = true;
    config.http_request.allowed_domains = vec![
        "*".to_string(),
        "example.com".to_string(),
        "docs.openhuman.ai".to_string(),
    ];
    config.search.engine = "managed".to_string();
    std::fs::create_dir_all(&config.workspace_dir).expect("workspace");
    (tmp, config)
}

#[test]
fn generated_tool_admission_covers_provenance_and_rejection_paths() {
    let mut trusted = BTreeSet::new();
    trusted.insert("trusted.provider".to_string());
    trusted.insert(" invalid provider ".to_string());

    let mut disabled_providers = BTreeSet::new();
    disabled_providers.insert("blocked.provider".to_string());

    let mut disabled_capabilities = BTreeSet::new();
    disabled_capabilities.insert("blocked.capability".to_string());

    let mut existing_tool_names = BTreeSet::new();
    existing_tool_names.insert("already_registered".to_string());

    let config = GeneratedToolAdmissionConfig {
        enforce_provenance: true,
        trusted_providers: trusted,
        disabled_providers,
        disabled_capabilities,
        existing_tool_names,
    };

    let mut duplicate = basic_generated_definition("already_registered");
    duplicate.capability_id = Some("duplicate.capability".to_string());

    let mut disabled_provider = basic_generated_definition("blocked_provider_tool");
    disabled_provider.provider_id = Some("Blocked.Provider".to_string());

    let mut untrusted = basic_generated_definition("untrusted_tool");
    untrusted.provider_id = Some("unknown.provider".to_string());

    let mut disabled_capability = basic_generated_definition("disabled_capability_tool");
    disabled_capability.capability_id = Some("blocked.capability".to_string());

    let mut missing_risk = basic_generated_definition("missing_risk_tool");
    missing_risk.risk = None;

    let mut missing_digest = basic_generated_definition("missing_digest_tool");
    missing_digest.source_digest = None;

    let mut unsafe_name = basic_generated_definition("-unsafe");
    unsafe_name.provider_id = Some("trusted.provider".to_string());

    let mut invalid_schema = basic_generated_definition("invalid_schema");
    invalid_schema.parameters_schema = json!({"type": "object", "additionalProperties": true});

    let report = admit_generated_tool_definitions(
        vec![
            basic_generated_definition("accepted_tool"),
            duplicate,
            disabled_provider,
            untrusted,
            disabled_capability,
            missing_risk,
            missing_digest,
            unsafe_name,
            invalid_schema,
        ],
        &config,
    );

    assert!(report
        .admitted
        .iter()
        .any(|definition| definition.name == "accepted_tool"
            && definition.provider_id.as_deref() == Some("trusted.provider")));
    let reasons = report
        .rejected
        .iter()
        .map(|rejection| rejection.reason.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    assert!(reasons.contains("duplicate generated tool"));
    assert!(reasons.contains("provider `blocked.provider` is disabled"));
    assert!(reasons.contains("provider `unknown.provider` is not trusted"));
    assert!(reasons.contains("capability `blocked.capability` is disabled"));
    assert!(reasons.contains("missing risk metadata"));
    assert!(reasons.contains("missing source_digest"));
    assert!(reasons.contains("name contains unsupported characters"));
    assert!(report
        .admitted
        .iter()
        .any(|definition| definition.name == "invalid_schema"));
}

#[tokio::test]
async fn generated_tool_wrapper_executes_and_exposes_metadata() {
    let adapter = Arc::new(RecordingGeneratedAdapter);
    let mut write_tool = basic_generated_definition("write_status");
    write_tool.permission_level = PermissionLevel::Write;
    write_tool.category = ToolCategory::Workflow;
    write_tool.scope = ToolScope::AgentOnly;
    write_tool.risk = Some(GeneratedToolRisk::ExternalWrite);

    let tools = generated_tools_from_definitions(vec![write_tool], adapter).expect("wrap tool");
    let tool = tools.first().expect("generated tool");

    assert_eq!(tool.name(), "write_status");
    assert_eq!(tool.permission_level(), PermissionLevel::Write);
    assert_eq!(tool.category(), ToolCategory::Workflow);
    assert_eq!(tool.scope(), ToolScope::AgentOnly);
    assert!(tool.external_effect());
    assert_eq!(
        tool.permission_level_with_args(&json!({"message": "hi"})),
        PermissionLevel::Write
    );
    assert!(!tool.supports_markdown());
    assert!(!tool.is_concurrency_safe(&Value::Null));

    let result = tool
        .execute(json!({"message": "hello"}))
        .await
        .expect("execute generated tool");
    let output = result.output();
    assert!(output.contains("write_status"));
    assert!(output.contains("recording-adapter"));

    let bad_adapter = Arc::new(RecordingGeneratedAdapter);
    let bad_definition = GeneratedToolDefinition::new(
        "needs_other_adapter",
        "Adapter mismatch",
        json!({"type": "object"}),
        "other-adapter",
    );
    let err = match generated_tools_from_definitions(vec![bad_definition], bad_adapter) {
        Ok(_) => panic!("adapter mismatch should fail"),
        Err(err) => err.to_string(),
    };
    assert!(err.contains("requires adapter `other-adapter`"));
}

#[test]
fn tool_registries_schemas_and_local_helpers_cover_safe_branches() {
    let (_tmp, config) = temp_config();
    let config = Arc::new(config);
    let security = Arc::new(SecurityPolicy::from_config(
        &config.autonomy,
        &config.workspace_dir,
        &config.workspace_dir,
    ));
    let audit = AuditLogger::disabled();
    let memory: Arc<dyn Memory> = Arc::new(StubMemory);

    let baseline = default_tools(Arc::clone(&security));
    assert_eq!(baseline.len(), 3);
    assert!(baseline.iter().any(|tool| tool.name() == "shell"));
    assert!(baseline.iter().any(|tool| tool.name() == "file_read"));
    assert!(baseline.iter().any(|tool| tool.name() == "file_write"));

    let tools = all_tools(
        Arc::clone(&config),
        &security,
        audit,
        memory,
        &config.browser,
        &config.http_request,
        &config.workspace_dir,
        &HashMap::new(),
        &config,
    );
    let names = tools
        .iter()
        .map(|tool| tool.name())
        .collect::<BTreeSet<_>>();
    for expected in [
        "browser",
        "browser_open",
        "http_request",
        "web_fetch",
        "curl",
        "gitbooks_search",
        "gitbooks_get_page",
        "mcp_setup_search",
        "mcp_setup_install_and_connect",
    ] {
        assert!(names.contains(expected), "missing tool {expected}");
    }

    for tool in &tools {
        assert!(!tool.name().trim().is_empty());
        assert!(!tool.description().trim().is_empty());
        let schema = tool.parameters_schema();
        assert_eq!(
            schema.get("type").and_then(Value::as_str),
            Some("object"),
            "{} schema should be an object",
            tool.name()
        );
        let _ = tool.permission_level_with_args(&json!({"action": "list"}));
    }

    let schema_names = all_tools_controller_schemas()
        .into_iter()
        .map(|schema| schema.function)
        .collect::<BTreeSet<_>>();
    assert!(schema_names.contains("web_search"));
    assert!(schema_names.contains("composio_execute"));
    let registered_names = all_tools_registered_controllers()
        .into_iter()
        .map(|registered| registered.schema.function)
        .collect::<BTreeSet<_>>();
    assert_eq!(schema_names, registered_names);

    let wrappers = tools_wrappers_list_json();
    assert_eq!(wrappers["result"]["wrappers"][0]["name"], "screenshot");
    assert_eq!(wrappers["logs"][0], "tools wrappers listed");

    let raw = "Screenshot saved to: /tmp/shot.png\ndata:image/png;base64,aGVsbG8=";
    assert_eq!(
        extract_saved_path(raw).unwrap().display().to_string(),
        "/tmp/shot.png"
    );
    assert_eq!(
        extract_data_url(raw).as_deref(),
        Some("data:image/png;base64,aGVsbG8=")
    );
    assert_eq!(
        decode_data_url_bytes("data:image/png;base64,aGVsbG8=").unwrap(),
        b"hello"
    );
    assert!(decode_data_url_bytes("data:text/plain;base64,aGVsbG8=").is_err());

    let out = config.workspace_dir.join("nested").join("bytes.bin");
    write_bytes_to_path(&out, b"abc").expect("write bytes");
    assert_eq!(std::fs::read(out).expect("read bytes"), b"abc");

    let policy = DefaultToolPolicy;
    assert_eq!(
        policy.evaluate("anything", &json!({"arg": true})),
        PolicyDecision::Allow
    );
}

#[test]
fn channel_definitions_validate_all_auth_modes_and_controller_metadata() {
    let definitions = all_channel_definitions();
    let ids = definitions
        .iter()
        .map(|definition| definition.id)
        .collect::<BTreeSet<_>>();
    assert_eq!(ids.len(), definitions.len(), "channel ids should be unique");
    for expected in [
        "telegram", "discord", "web", "imessage", "lark", "dingtalk", "yuanbao",
    ] {
        assert!(
            ids.contains(expected),
            "missing channel definition {expected}"
        );
        assert!(find_channel_definition(expected).is_some());
    }
    assert!(find_channel_definition("missing").is_none());

    let telegram = find_channel_definition("telegram").expect("telegram");
    assert!(telegram
        .capabilities
        .contains(&ChannelCapability::DraftUpdates));
    assert_eq!(
        telegram
            .auth_mode_spec(ChannelAuthMode::ManagedDm)
            .and_then(|mode| mode.auth_action),
        Some("telegram_managed_dm")
    );
    assert!(telegram
        .validate_credentials(ChannelAuthMode::ManagedDm, &Map::new())
        .is_ok());
    let err = telegram
        .validate_credentials(ChannelAuthMode::BotToken, &Map::new())
        .expect_err("bot token should be required");
    assert!(err.contains("missing required fields"));
    assert!(err.contains("bot_token"));
    let mut creds = Map::new();
    creds.insert("bot_token".to_string(), json!("123456:token"));
    assert!(telegram
        .validate_credentials(ChannelAuthMode::BotToken, &creds)
        .is_ok());
    assert!(telegram
        .validate_credentials(ChannelAuthMode::OAuth, &creds)
        .expect_err("unsupported auth mode")
        .contains("does not support auth mode"));

    for (raw, parsed) in [
        ("api_key", ChannelAuthMode::ApiKey),
        ("bot_token", ChannelAuthMode::BotToken),
        ("oauth", ChannelAuthMode::OAuth),
        ("managed_dm", ChannelAuthMode::ManagedDm),
    ] {
        assert_eq!(raw.parse::<ChannelAuthMode>().unwrap(), parsed);
        assert_eq!(parsed.to_string(), raw);
    }
    assert!("bad-mode".parse::<ChannelAuthMode>().is_err());

    let schema_names = all_channels_controller_schemas()
        .into_iter()
        .map(|schema| schema.function)
        .collect::<BTreeSet<_>>();
    for expected in [
        "list",
        "describe",
        "connect",
        "disconnect",
        "status",
        "test",
        "send_message",
        "send_reaction",
        "create_thread",
        "update_thread",
        "list_threads",
    ] {
        assert!(schema_names.contains(expected), "missing schema {expected}");
    }
    let registered_names = all_channels_registered_controllers()
        .into_iter()
        .map(|registered| registered.schema.function)
        .collect::<BTreeSet<_>>();
    assert_eq!(schema_names, registered_names);
}

#[test]
fn whatsapp_webhook_parser_covers_allowed_and_skipped_payloads() {
    let channel = WhatsAppChannel::new(
        "token".to_string(),
        "phone-id".to_string(),
        "verify-me".to_string(),
        vec!["+15551234567".to_string(), "+15557654321".to_string()],
    );
    assert_eq!(channel.name(), "whatsapp");
    assert_eq!(channel.verify_token(), "verify-me");

    assert!(channel
        .parse_webhook_payload(&json!({"entry": "bad"}))
        .is_empty());
    assert!(channel
        .parse_webhook_payload(&json!({"entry": [{"changes": "bad"}]}))
        .is_empty());

    let payload = json!({
        "entry": [
            {
                "changes": [
                    {
                        "value": {
                            "messages": [
                                {
                                    "from": "15551234567",
                                    "timestamp": "1710000000",
                                    "text": { "body": "hello from allowed" }
                                },
                                {
                                    "from": "+15557654321",
                                    "timestamp": "not-a-number",
                                    "text": { "body": "timestamp fallback" }
                                },
                                {
                                    "from": "15550000000",
                                    "timestamp": "1710000001",
                                    "text": { "body": "blocked" }
                                },
                                {
                                    "from": "15551234567",
                                    "timestamp": "1710000002",
                                    "image": { "id": "media" }
                                },
                                {
                                    "from": "15551234567",
                                    "timestamp": "1710000003",
                                    "text": { "body": "" }
                                },
                                {
                                    "timestamp": "1710000004",
                                    "text": { "body": "missing sender" }
                                }
                            ]
                        }
                    },
                    { "value": { "messages": "bad" } }
                ]
            },
            { "changes": [] }
        ]
    });

    let messages = channel.parse_webhook_payload(&payload);
    assert_eq!(messages.len(), 2);
    assert_eq!(messages[0].sender, "+15551234567");
    assert_eq!(messages[0].reply_target, "+15551234567");
    assert_eq!(messages[0].content, "hello from allowed");
    assert_eq!(messages[0].channel, "whatsapp");
    assert_eq!(messages[0].timestamp, 1_710_000_000);
    assert_eq!(messages[1].sender, "+15557654321");
    assert!(messages[1].timestamp > 0);

    let wildcard = WhatsAppChannel::new(
        "token".to_string(),
        "phone-id".to_string(),
        "verify".to_string(),
        vec!["*".to_string()],
    );
    let wildcard_messages = wildcard.parse_webhook_payload(&json!({
        "entry": [{"changes": [{"value": {"messages": [{
            "from": "14155550100",
            "timestamp": "1710000100",
            "text": {"body": "wildcard"}
        }]}}]}]
    }));
    assert_eq!(wildcard_messages.len(), 1);
    assert_eq!(wildcard_messages[0].sender, "+14155550100");
}

#[test]
fn yuanbao_config_wire_and_splitter_helpers_cover_public_deterministic_paths() {
    assert!(NO_RECONNECT_CLOSE_CODES.contains(&4012));
    assert!(AUTH_FAILED_CODES.contains(&40001));
    assert!(AUTH_RETRYABLE_CODES.contains(&40010));

    let mut cfg = YuanbaoConfig::default();
    assert_eq!(cfg.env, "prod");
    assert_eq!(cfg.bot_version, "0.1.0");
    assert_eq!(cfg.dm_access, "open");
    assert_eq!(cfg.group_access, "allowlist");
    assert!(cfg.group_at_required);
    assert_eq!(cfg.max_message_length, 4500);
    assert_eq!(cfg.max_media_mb, 50);
    assert!(cfg
        .validate()
        .expect_err("default config invalid")
        .to_string()
        .contains("app_key"));
    cfg.env = "pre".into();
    cfg.apply_env_defaults();
    assert_eq!(cfg.api_domain, "https://bot-pre.yuanbao.tencent.com");
    assert_eq!(
        cfg.ws_domain,
        "wss://bot-wss-pre.yuanbao.tencent.com/wss/connection"
    );
    cfg.app_key = "app-key".into();
    assert!(cfg
        .validate()
        .expect_err("missing token invalid")
        .to_string()
        .contains("token"));
    cfg.token = "pre-provisioned-token".into();
    assert!(cfg.validate().is_ok());

    let mut explicit = YuanbaoConfig {
        app_key: "app-key".into(),
        token: "token".into(),
        api_domain: "https://custom-api.example.test".into(),
        ws_domain: "wss://custom-ws.example.test".into(),
        ..YuanbaoConfig::default()
    };
    explicit.apply_env_defaults();
    assert_eq!(explicit.api_domain, "https://custom-api.example.test");
    assert_eq!(explicit.ws_domain, "wss://custom-ws.example.test");
    assert!(explicit.validate().is_ok());

    let seq_a = next_seq_no();
    let seq_b = next_seq_no();
    assert_eq!(seq_b, seq_a + 1);

    let mut varint = Vec::new();
    encode_varint(300, &mut varint);
    assert_eq!(varint, vec![0xac, 0x02]);
    assert_eq!(decode_varint(&varint, 0).expect("decode varint"), (300, 2));
    assert!(decode_varint(&[0x80], 0)
        .expect_err("truncated varint")
        .to_string()
        .contains("truncated varint"));
    assert!(decode_varint(&[0xff; 10], 0)
        .expect_err("overflow varint")
        .to_string()
        .contains("overflow"));

    let mut fields_buf = Vec::new();
    encode_field_varint(1, 42, &mut fields_buf);
    encode_field_string(2, "hello", &mut fields_buf);
    encode_field_bytes(2, b"again", &mut fields_buf);
    fields_buf.push((3 << 3) | 5);
    fields_buf.extend_from_slice(&0x1234_5678_u32.to_le_bytes());
    fields_buf.push((4 << 3) | 1);
    fields_buf.extend_from_slice(&0x0102_0304_0506_0708_u64.to_le_bytes());

    let fields = parse_fields(&fields_buf).expect("parse mixed fields");
    assert_eq!(get_varint(&fields, 1), 42);
    assert_eq!(get_string(&fields, 2), "hello");
    assert_eq!(get_bytes(&fields, 2), b"hello".to_vec());
    assert_eq!(
        get_repeated_bytes(&fields, 2),
        vec![b"hello".to_vec(), b"again".to_vec()]
    );
    assert_eq!(get_varint(&fields, 99), 0);
    assert_eq!(get_string(&fields, 99), "");
    assert!(get_bytes(&fields, 99).is_empty());
    assert!(fields
        .iter()
        .any(|(_, value)| matches!(value, FieldValue::Fixed32(0x1234_5678))));
    assert!(fields
        .iter()
        .any(|(_, value)| matches!(value, FieldValue::Fixed64(0x0102_0304_0506_0708))));
    assert!(parse_fields(&[((9 << 3) | 3) as u8])
        .expect_err("unsupported wire type")
        .to_string()
        .contains("unsupported wire type"));
    assert!(parse_fields(&[((9 << 3) | 2) as u8, 5, b'a'])
        .expect_err("truncated len field")
        .to_string()
        .contains("truncated len field"));

    assert_eq!(split_markdown("short", 100), vec!["short"]);
    let fenced = "intro\n```rust\nfn alpha() {}\nfn beta() {}\n```\noutro\n";
    let chunks = split_markdown(fenced, 32);
    assert!(chunks.len() > 1);
    assert!(chunks.iter().any(|chunk| chunk.contains("```rust")));
    assert!(chunks.iter().all(|chunk| !chunk.trim().is_empty()));
    let hard_split = split_markdown("é".repeat(8).as_str(), 3);
    assert!(hard_split.len() > 1);
    assert!(hard_split.iter().all(|chunk| chunk.len() <= 4));
}

#[test]
fn yuanbao_media_and_proto_helpers_cover_public_roundtrips() {
    assert_eq!(guess_mime_type("PHOTO.JPG"), "image/jpeg");
    assert_eq!(
        guess_mime_type("slides.pptx"),
        "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    );
    assert_eq!(
        guess_mime_type("archive.unknown"),
        "application/octet-stream"
    );
    assert!(is_image("avatar.webp", ""));
    assert!(is_image("no-extension", "image/png"));
    assert!(!is_image("notes.txt", ""));
    assert_eq!(image_format_code("image/jpeg"), 1);
    assert_eq!(image_format_code("image/gif"), 2);
    assert_eq!(image_format_code("image/png"), 3);
    assert_eq!(image_format_code("image/bmp"), 4);
    assert_eq!(image_format_code("image/heic"), 255);

    let png = [
        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
        0x52, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0xF0,
    ];
    let png_dims = parse_image_size(&png).expect("png dims");
    assert_eq!(png_dims.width, 320);
    assert_eq!(png_dims.height, 240);
    let gif_dims = parse_image_size(b"GIF89a\x40\x01\xF0\x00rest").expect("gif dims");
    assert_eq!(gif_dims.width, 320);
    assert_eq!(gif_dims.height, 240);
    let mut webp_vp8x = b"RIFF\x00\x00\x00\x00WEBPVP8X".to_vec();
    webp_vp8x.extend_from_slice(&[0u8; 8]);
    webp_vp8x.extend_from_slice(&[0x3F, 0x01, 0x00, 0xEF, 0x00, 0x00]);
    let webp_dims = parse_image_size(&webp_vp8x).expect("webp dims");
    assert_eq!(webp_dims.width, 320);
    assert_eq!(webp_dims.height, 240);
    assert!(parse_image_size(b"not-an-image").is_none());

    let image_body = build_image_msg_body(
        "https://cdn.example.test/cat.png",
        None,
        Some("cat.png"),
        1024,
        800,
        600,
        "image/png",
    );
    assert_eq!(image_body[0].msg_type, "TIMImageElem");
    assert_eq!(image_body[0].msg_content.uuid.as_deref(), Some("cat.png"));
    assert_eq!(image_body[0].msg_content.image_format, Some(3));
    assert_eq!(
        image_body[0].msg_content.image_info_array[0].url,
        "https://cdn.example.test/cat.png"
    );
    let file_body = build_file_msg_body(
        "https://cdn.example.test/report.pdf",
        "report.pdf",
        Some("file-uuid"),
        2048,
    );
    assert_eq!(file_body[0].msg_type, "TIMFileElem");
    assert_eq!(
        file_body[0].msg_content.file_name.as_deref(),
        Some("report.pdf")
    );
    assert_eq!(file_body[0].msg_content.file_size, Some(2048));

    let frame_buf = encode_conn_msg(
        cmd_type::REQUEST,
        cmd::PING,
        7,
        "msg-7",
        module::CONN_ACCESS,
        b"payload",
    );
    let frame = decode_conn_msg(&frame_buf).expect("decode conn msg");
    assert_eq!(frame.cmd_type, cmd_type::REQUEST);
    assert_eq!(frame.cmd, cmd::PING);
    assert_eq!(frame.seq_no, 7);
    assert_eq!(frame.msg_id, "msg-7");
    assert_eq!(frame.data, b"payload");
    let ping = decode_conn_msg(&encode_ping("ping-1")).expect("decode ping");
    assert_eq!(ping.cmd, cmd::PING);
    let ack = decode_conn_msg(&encode_push_ack(&YuanbaoConnFrame {
        cmd_type: cmd_type::PUSH,
        cmd: "push".into(),
        seq_no: 9,
        msg_id: "push-1".into(),
        need_ack: true,
        status: 0,
        module: module::BIZ_PKG.into(),
        data: Vec::new(),
    }))
    .expect("decode ack");
    assert_eq!(ack.cmd_type, cmd_type::PUSH_ACK);
    assert_eq!(ack.msg_id, "push-1");
    let auth = decode_conn_msg(&encode_auth_bind(
        "biz", "uid", "openclaw", "token", "auth-1", "1.0.0", "linux", "2.0.0", "pre",
    ))
    .expect("decode auth bind");
    assert_eq!(auth.cmd, cmd::AUTH_BIND);
    assert_eq!(auth.module, module::CONN_ACCESS);
    assert!(!auth.data.is_empty());

    let mut auth_rsp = Vec::new();
    encode_field_varint(1, 0, &mut auth_rsp);
    encode_field_string(2, "ok", &mut auth_rsp);
    encode_field_string(3, "connect-1", &mut auth_rsp);
    let auth_rsp = decode_auth_bind_rsp(&auth_rsp).expect("decode auth rsp");
    assert_eq!(auth_rsp.message, "ok");
    assert_eq!(auth_rsp.connect_id, "connect-1");

    let mut push_msg = Vec::new();
    encode_field_string(1, "inbound_message", &mut push_msg);
    encode_field_string(2, module::BIZ_PKG, &mut push_msg);
    encode_field_string(3, "push-msg-1", &mut push_msg);
    encode_field_bytes(4, b"biz-payload", &mut push_msg);
    let decoded_push = decode_push_msg(&push_msg).expect("decode push msg");
    assert_eq!(decoded_push.cmd, "inbound_message");
    assert_eq!(decoded_push.data, b"biz-payload");

    let text_el = YuanbaoMsgBodyElement {
        msg_type: "TIMTextElem".into(),
        msg_content: YuanbaoMsgContent {
            text: Some("hello from proto".into()),
            ..Default::default()
        },
    };
    let mut inbound = Vec::new();
    encode_field_string(1, "C2C.Callback", &mut inbound);
    encode_field_string(2, "sender", &mut inbound);
    encode_field_string(3, "bot", &mut inbound);
    encode_field_string(4, "Alice", &mut inbound);
    encode_field_varint(8, 11, &mut inbound);
    encode_field_varint(10, 1_780_000_000, &mut inbound);
    encode_field_string(12, "msg-11", &mut inbound);
    encode_field_bytes(13, &encode_msg_body_element(&text_el), &mut inbound);
    let mut recall = Vec::new();
    encode_field_varint(1, 10, &mut recall);
    encode_field_string(2, "old-msg", &mut recall);
    encode_field_bytes(17, &recall, &mut inbound);
    let mut log_ext = Vec::new();
    encode_field_string(1, "trace-11", &mut log_ext);
    encode_field_bytes(20, &log_ext, &mut inbound);
    let decoded_inbound = decode_inbound_push(&inbound).expect("decode inbound push");
    assert_eq!(decoded_inbound.callback_command, "C2C.Callback");
    assert_eq!(decoded_inbound.extract_text(), "hello from proto");
    assert_eq!(decoded_inbound.recall_msg_seq_list[0].msg_id, "old-msg");
    assert_eq!(decoded_inbound.trace_id, "trace-11");

    let decoded_json = decode_inbound_json(
        br#"{
            "callback_command": "Group.Callback",
            "from_account": "sender-json",
            "group_code": "group-json",
            "msg_seq": 12,
            "msg_body": [{
                "msg_type": "TIMImageElem",
                "msg_content": {
                    "uuid": "img-json",
                    "image_format": 3,
                    "image_info_array": [{
                        "image_type": 1,
                        "size": 50,
                        "width": 10,
                        "height": 20,
                        "url": "https://cdn.example.test/json.png"
                    }]
                }
            }],
            "recall_msg_seq_list": [{ "msg_seq": 11, "msg_id": "old-json" }],
            "log_ext": { "trace_id": "trace-json" }
        }"#,
    )
    .expect("decode inbound json");
    assert!(decoded_json.is_group());
    assert_eq!(
        decoded_json.extract_image_urls(),
        vec!["https://cdn.example.test/json.png".to_string()]
    );
    assert_eq!(decoded_json.recall_msg_seq_list[0].msg_seq, 11);
    assert_eq!(decoded_json.trace_id, "trace-json");
    assert!(decode_inbound_json(b"[]")
        .expect_err("json root must be object")
        .to_string()
        .contains("json root is not an object"));
}

#[tokio::test]
async fn channel_trait_defaults_and_cli_channel_cover_message_paths() {
    struct TestChannel;

    #[async_trait]
    impl Channel for TestChannel {
        fn name(&self) -> &str {
            "test"
        }

        async fn send(&self, _message: &SendMessage) -> Result<()> {
            Ok(())
        }

        async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> Result<()> {
            tx.send(ChannelMessage {
                id: "m1".to_string(),
                sender: "sender".to_string(),
                reply_target: "reply".to_string(),
                content: "content".to_string(),
                channel: "test".to_string(),
                timestamp: 123,
                thread_ts: Some("thread-1".to_string()),
            })
            .await?;
            Ok(())
        }
    }

    let message = SendMessage::with_subject("hello", "recipient", "subject")
        .in_thread(Some("thread-42".to_string()));
    assert_eq!(message.content, "hello");
    assert_eq!(message.recipient, "recipient");
    assert_eq!(message.subject.as_deref(), Some("subject"));
    assert_eq!(message.thread_ts.as_deref(), Some("thread-42"));

    let simple = SendMessage::new("simple", "target");
    assert!(simple.subject.is_none());
    assert!(simple.thread_ts.is_none());

    let channel = TestChannel;
    assert_eq!(channel.name(), "test");
    assert!(channel.health_check().await);
    assert!(!channel.supports_reactions());
    assert!(!channel.supports_draft_updates());
    assert!(channel.start_typing("recipient").await.is_ok());
    assert!(channel.stop_typing("recipient").await.is_ok());
    assert!(channel.send(&message).await.is_ok());
    assert!(channel.send_draft(&message).await.unwrap().is_none());
    assert!(channel
        .update_draft("recipient", "message-id", "draft")
        .await
        .is_ok());
    assert!(channel
        .finalize_draft("recipient", "message-id", "final", Some("thread-42"))
        .await
        .is_ok());

    let (tx, mut rx) = tokio::sync::mpsc::channel(1);
    channel.listen(tx).await.expect("listen");
    let received = rx.recv().await.expect("message");
    assert_eq!(received.id, "m1");
    assert_eq!(received.thread_ts.as_deref(), Some("thread-1"));

    let cli = CliChannel::default();
    assert_eq!(cli.name(), "cli");
    assert!(cli
        .send(&SendMessage::new("printed by test", "stdout"))
        .await
        .is_ok());
    assert!(cli.health_check().await);
}
