//! Matrix transport configuration — deserialization of `bot.toml` Matrix settings.
#![allow(unused_imports, dead_code)]
use serde::Deserialize;
use std::path::Path;

fn default_history_size() -> usize {
    20
}

fn default_permission_timeout_secs() -> u64 {
    120
}

fn default_aggregated_notifications_poll_interval_secs() -> u64 {
    5
}

fn default_aggregated_notifications_enabled() -> bool {
    true
}

/// Configuration for the Matrix bot, read from `.huskies/bot.toml`.
#[derive(Deserialize, Clone, Debug)]
pub struct BotConfig {
    /// Matrix homeserver URL, e.g. `https://matrix.example.com`
    /// Only required when `transport = "matrix"` (the default).
    #[serde(default)]
    pub homeserver: Option<String>,
    /// Bot user ID, e.g. `@storykit:example.com`
    /// Only required when `transport = "matrix"`.
    #[serde(default)]
    pub username: Option<String>,
    /// Bot password
    /// Only required when `transport = "matrix"`.
    #[serde(default)]
    pub password: Option<String>,
    /// Matrix room IDs to join, e.g. `["!roomid:example.com"]`.
    /// Use an array for multiple rooms; a single string is accepted via the
    /// deprecated `room_id` key for backwards compatibility.
    #[serde(default)]
    pub room_ids: Vec<String>,
    /// Deprecated: use `room_ids` (list) instead. Still accepted so existing
    /// `bot.toml` files continue to work without modification.
    #[serde(default)]
    pub room_id: Option<String>,
    /// Set to `true` to enable the bot (default: false)
    #[serde(default)]
    pub enabled: bool,
    /// Matrix user IDs allowed to interact with the bot.
    /// If empty or omitted, the bot ignores ALL messages (fail-closed).
    #[serde(default)]
    pub allowed_users: Vec<String>,
    /// Maximum number of conversation turns (user + assistant pairs) to keep
    /// per room. When the history exceeds this limit the oldest messages are
    /// dropped. Defaults to 20.
    #[serde(default = "default_history_size")]
    pub history_size: usize,
    /// Timeout in seconds for permission prompts surfaced to the Matrix room.
    /// If the user does not respond within this window the permission is denied
    /// (fail-closed). Defaults to 120 seconds.
    #[serde(default = "default_permission_timeout_secs")]
    pub permission_timeout_secs: u64,
    /// Previously used to select an Anthropic model. Now ignored — the bot
    /// uses Claude Code which manages its own model selection. Kept for
    /// backwards compatibility so existing bot.toml files still parse.
    #[allow(dead_code)]
    pub model: Option<String>,
    /// Display name the bot uses to identify itself in conversations.
    /// If unset, the bot falls back to "Assistant".
    #[serde(default)]
    pub display_name: Option<String>,
    /// Room IDs where ambient mode is active (bot responds to all messages).
    /// Updated at runtime when the user toggles ambient mode — do not edit
    /// manually while the bot is running.
    #[serde(default)]
    pub ambient_rooms: Vec<String>,
    /// Chat transport to use: `"matrix"` (default), `"whatsapp"`, `"slack"`,
    /// or `"discord"`.
    ///
    /// Selects which [`ChatTransport`] implementation the bot uses for
    /// sending and editing messages.  Currently only read during bot
    /// startup to select the transport; the field is kept for config
    /// round-tripping.
    #[serde(default = "default_transport")]
    pub transport: String,

    // ── WhatsApp Business API fields ─────────────────────────────────
    // These are only required when `transport = "whatsapp"`.
    /// WhatsApp Business phone number ID from the Meta dashboard.
    #[serde(default)]
    pub whatsapp_phone_number_id: Option<String>,
    /// Long-lived access token for the WhatsApp Business API.
    #[serde(default)]
    pub whatsapp_access_token: Option<String>,
    /// Verify token used in the webhook handshake (you choose this value
    /// and configure it in the Meta webhook settings).
    #[serde(default)]
    pub whatsapp_verify_token: Option<String>,
    /// Name of the approved Meta message template used for pipeline
    /// notifications when the 24-hour messaging window has expired.
    ///
    /// The template must be registered in the Meta Business Manager before
    /// use.  Defaults to `"pipeline_notification"`.
    #[serde(default)]
    pub whatsapp_notification_template: Option<String>,
    /// Which WhatsApp provider to use: `"meta"` (default, direct Graph API)
    /// or `"twilio"` (Twilio REST API as alternative to Meta).
    ///
    /// When `"twilio"`, the Twilio-specific fields below are required instead
    /// of the Meta `whatsapp_phone_number_id` / `whatsapp_access_token` pair.
    #[serde(default = "default_whatsapp_provider")]
    pub whatsapp_provider: String,

    // ── Twilio WhatsApp fields ─────────────────────────────────────────
    // Only required when `transport = "whatsapp"` and `whatsapp_provider = "twilio"`.
    /// Twilio Account SID (starts with `AC`).
    #[serde(default)]
    pub twilio_account_sid: Option<String>,
    /// Twilio Auth Token.
    #[serde(default)]
    pub twilio_auth_token: Option<String>,
    /// Twilio WhatsApp sender number in E.164 format, e.g. `+14155551234`.
    #[serde(default)]
    pub twilio_whatsapp_number: Option<String>,

    /// Phone numbers allowed to interact with the bot when using WhatsApp.
    /// When non-empty, only listed numbers can send commands; all others are
    /// silently ignored. When empty or absent, all numbers are allowed
    /// (backwards compatible — open by default, unlike Matrix which is
    /// fail-closed).
    #[serde(default)]
    pub whatsapp_allowed_phones: Vec<String>,

    // ── Slack Bot API fields ─────────────────────────────────────────
    // These are only required when `transport = "slack"`.
    /// Slack Bot User OAuth Token (starts with `xoxb-`).
    #[serde(default)]
    pub slack_bot_token: Option<String>,
    /// Slack Signing Secret used to verify incoming webhook requests.
    #[serde(default)]
    pub slack_signing_secret: Option<String>,
    /// Slack channel IDs the bot should listen in.
    #[serde(default)]
    pub slack_channel_ids: Vec<String>,

    // ── Discord Bot API fields ──────────────────────────────────────
    // These are only required when `transport = "discord"`.
    /// Discord bot token from the Discord Developer Portal.
    #[serde(default)]
    pub discord_bot_token: Option<String>,
    /// Discord channel IDs the bot should listen in.
    #[serde(default)]
    pub discord_channel_ids: Vec<String>,
    /// Discord user IDs allowed to interact with the bot.
    /// When empty or absent, all users in configured channels are allowed.
    #[serde(default)]
    pub discord_allowed_users: Vec<String>,

    /// How often (in seconds) the gateway polls each project server's
    /// `/api/events` endpoint to aggregate cross-project notifications.
    ///
    /// Only used when the gateway's bot is enabled.  Defaults to 5 seconds.
    #[serde(default = "default_aggregated_notifications_poll_interval_secs")]
    pub aggregated_notifications_poll_interval_secs: u64,

    /// Whether the gateway-level aggregated cross-project notification stream
    /// is enabled.  When `false`, the gateway will not poll per-project
    /// servers for events even if the bot is otherwise enabled.
    ///
    /// Set this in the **gateway's** `bot.toml` (not in per-project configs).
    /// Adding a new project to `projects.toml` never requires touching
    /// per-project bot configs — the aggregated stream picks it up
    /// automatically once this flag is `true` (the default).
    ///
    /// Defaults to `true`.
    #[serde(default = "default_aggregated_notifications_enabled")]
    pub aggregated_notifications_enabled: bool,
}

fn default_transport() -> String {
    "matrix".to_string()
}

fn default_whatsapp_provider() -> String {
    "meta".to_string()
}

mod loading;

pub use loading::save_ambient_rooms;

mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn load_returns_none_when_file_missing() {
        let tmp = tempfile::tempdir().unwrap();
        let result = BotConfig::load(tmp.path());
        assert!(result.is_none());
    }

    #[test]
    fn load_returns_none_when_disabled() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = false
"#,
        )
        .unwrap();
        let result = BotConfig::load(tmp.path());
        assert!(result.is_none());
    }

    #[test]
    fn load_returns_config_when_enabled_with_room_ids() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com", "!def:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let result = BotConfig::load(tmp.path());
        assert!(result.is_some());
        let config = result.unwrap();
        assert_eq!(
            config.homeserver.as_deref(),
            Some("https://matrix.example.com")
        );
        assert_eq!(config.username.as_deref(), Some("@bot:example.com"));
        assert_eq!(
            config.effective_room_ids(),
            &["!abc:example.com", "!def:example.com"]
        );
        assert!(config.model.is_none());
    }

    #[test]
    fn load_merges_deprecated_room_id_into_room_ids() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        // Old-style single room_id key — should still work.
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_id = "!abc:example.com"
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.effective_room_ids(), &["!abc:example.com"]);
    }

    #[test]
    fn load_returns_none_when_no_room_ids() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
"#,
        )
        .unwrap();
        let result = BotConfig::load(tmp.path());
        assert!(result.is_none());
    }

    #[test]
    fn load_returns_none_when_toml_invalid() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(sk.join("bot.toml"), "not valid toml {{{").unwrap();
        let result = BotConfig::load(tmp.path());
        assert!(result.is_none());
    }

    #[test]
    fn load_respects_optional_model() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
model = "claude-sonnet-4-6"
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.model.as_deref(), Some("claude-sonnet-4-6"));
    }

    #[test]
    fn load_uses_default_history_size() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.history_size, 20);
    }

    #[test]
    fn load_respects_custom_history_size() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
history_size = 50
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.history_size, 50);
    }

    #[test]
    fn load_reads_display_name() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
display_name = "Timmy"
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.display_name.as_deref(), Some("Timmy"));
    }

    #[test]
    fn load_display_name_defaults_to_none_when_absent() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert!(config.display_name.is_none());
    }

    #[test]
    fn load_uses_default_permission_timeout() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.permission_timeout_secs, 120);
    }

    #[test]
    fn load_respects_custom_permission_timeout() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
permission_timeout_secs = 60
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.permission_timeout_secs, 60);
    }

    #[test]
    fn load_ignores_legacy_require_verified_devices_key() {
        // Old bot.toml files that still have `require_verified_devices = true`
        // must parse successfully — the field is simply ignored now that
        // verification is always enforced unconditionally.
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
require_verified_devices = true
"#,
        )
        .unwrap();
        // Should still load successfully despite the unknown field.
        let config = BotConfig::load(tmp.path());
        assert!(
            config.is_some(),
            "bot.toml with legacy require_verified_devices key must still load"
        );
    }

    #[test]
    fn aggregated_notifications_enabled_defaults_to_true() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert!(config.aggregated_notifications_enabled);
    }

    #[test]
    fn aggregated_notifications_enabled_can_be_set_to_false() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
aggregated_notifications_enabled = false
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert!(!config.aggregated_notifications_enabled);
    }

    #[test]
    fn load_reads_ambient_rooms() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
ambient_rooms = ["!abc:example.com"]
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.ambient_rooms, vec!["!abc:example.com"]);
    }

    #[test]
    fn load_ambient_rooms_defaults_to_empty_when_absent() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert!(config.ambient_rooms.is_empty());
    }

    #[test]
    fn save_ambient_rooms_persists_to_bot_toml() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();

        save_ambient_rooms(tmp.path(), &["!abc:example.com".to_string()]);

        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.ambient_rooms, vec!["!abc:example.com"]);
    }

    #[test]
    fn save_ambient_rooms_clears_when_empty() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
ambient_rooms = ["!abc:example.com"]
"#,
        )
        .unwrap();

        save_ambient_rooms(tmp.path(), &[]);

        let config = BotConfig::load(tmp.path()).unwrap();
        assert!(config.ambient_rooms.is_empty());
    }

    #[test]
    fn load_transport_defaults_to_matrix() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.transport, "matrix");
    }

    #[test]
    fn load_transport_reads_custom_value() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_access_token = "EAAtoken"
whatsapp_verify_token = "my-verify"
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.transport, "whatsapp");
        assert_eq!(config.whatsapp_phone_number_id.as_deref(), Some("123456"));
        assert_eq!(config.whatsapp_access_token.as_deref(), Some("EAAtoken"));
        assert_eq!(config.whatsapp_verify_token.as_deref(), Some("my-verify"));
    }

    #[test]
    fn load_whatsapp_returns_none_when_missing_phone_number_id() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_access_token = "EAAtoken"
whatsapp_verify_token = "my-verify"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_whatsapp_returns_none_when_missing_access_token() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_verify_token = "my-verify"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_whatsapp_returns_none_when_missing_verify_token() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_access_token = "EAAtoken"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    // ── Twilio config tests ─────────────────────────────────────────────

    #[test]
    fn load_twilio_whatsapp_reads_config() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_provider = "twilio"
twilio_account_sid = "ACtest"
twilio_auth_token = "authtest"
twilio_whatsapp_number = "+14155551234"
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.transport, "whatsapp");
        assert_eq!(config.whatsapp_provider, "twilio");
        assert_eq!(config.twilio_account_sid.as_deref(), Some("ACtest"));
        assert_eq!(config.twilio_auth_token.as_deref(), Some("authtest"));
        assert_eq!(
            config.twilio_whatsapp_number.as_deref(),
            Some("+14155551234")
        );
    }

    #[test]
    fn load_whatsapp_provider_defaults_to_meta() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_access_token = "EAAtoken"
whatsapp_verify_token = "my-verify"
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.whatsapp_provider, "meta");
    }

    #[test]
    fn load_twilio_returns_none_when_missing_account_sid() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_provider = "twilio"
twilio_auth_token = "authtest"
twilio_whatsapp_number = "+14155551234"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_twilio_returns_none_when_missing_auth_token() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_provider = "twilio"
twilio_account_sid = "ACtest"
twilio_whatsapp_number = "+14155551234"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_twilio_returns_none_when_missing_whatsapp_number() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "whatsapp"
whatsapp_provider = "twilio"
twilio_account_sid = "ACtest"
twilio_auth_token = "authtest"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    // ── Slack config tests ─────────────────────────────────────────────

    #[test]
    fn load_slack_transport_reads_config() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_bot_token = "xoxb-123"
slack_signing_secret = "secret123"
slack_channel_ids = ["C01ABCDEF"]
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.transport, "slack");
        assert_eq!(config.slack_bot_token.as_deref(), Some("xoxb-123"));
        assert_eq!(config.slack_signing_secret.as_deref(), Some("secret123"));
        assert_eq!(config.slack_channel_ids, vec!["C01ABCDEF"]);
    }

    #[test]
    fn load_slack_returns_none_when_missing_bot_token() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_signing_secret = "secret123"
slack_channel_ids = ["C01ABCDEF"]
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_slack_returns_none_when_missing_signing_secret() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_bot_token = "xoxb-123"
slack_channel_ids = ["C01ABCDEF"]
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_slack_returns_none_when_missing_channel_ids() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_bot_token = "xoxb-123"
slack_signing_secret = "secret123"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn whatsapp_allowed_phones_defaults_to_empty_when_absent() {
        let config: BotConfig = toml::from_str(
            r#"
enabled = true
transport = "whatsapp"
whatsapp_provider = "meta"
whatsapp_phone_number_id = "123"
whatsapp_access_token = "tok"
whatsapp_verify_token = "ver"
"#,
        )
        .unwrap();
        assert!(config.whatsapp_allowed_phones.is_empty());
    }

    #[test]
    fn whatsapp_allowed_phones_deserializes_list() {
        let config: BotConfig = toml::from_str(
            r#"
enabled = true
transport = "whatsapp"
whatsapp_provider = "meta"
whatsapp_phone_number_id = "123"
whatsapp_access_token = "tok"
whatsapp_verify_token = "ver"
whatsapp_allowed_phones = ["+15551234567", "+15559876543"]
"#,
        )
        .unwrap();
        assert_eq!(
            config.whatsapp_allowed_phones,
            vec!["+15551234567", "+15559876543"]
        );
    }

    // ── Discord config tests ──────────────────────────────────────────

    #[test]
    fn load_discord_transport_reads_config() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
enabled = true
transport = "discord"
discord_bot_token = "Bot.Token.Here"
discord_channel_ids = ["123456789012345678"]
"#,
        )
        .unwrap();
        let config = BotConfig::load(tmp.path()).unwrap();
        assert_eq!(config.transport, "discord");
        assert_eq!(config.discord_bot_token.as_deref(), Some("Bot.Token.Here"));
        assert_eq!(config.discord_channel_ids, vec!["123456789012345678"]);
    }

    #[test]
    fn load_discord_returns_none_when_missing_bot_token() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
enabled = true
transport = "discord"
discord_channel_ids = ["123456789012345678"]
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn load_discord_returns_none_when_missing_channel_ids() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        fs::create_dir_all(&sk).unwrap();
        fs::write(
            sk.join("bot.toml"),
            r#"
enabled = true
transport = "discord"
discord_bot_token = "Bot.Token.Here"
"#,
        )
        .unwrap();
        assert!(BotConfig::load(tmp.path()).is_none());
    }

    #[test]
    fn discord_allowed_users_defaults_to_empty_when_absent() {
        let config: BotConfig = toml::from_str(
            r#"
enabled = true
transport = "discord"
discord_bot_token = "Bot.Token.Here"
discord_channel_ids = ["123456789"]
"#,
        )
        .unwrap();
        assert!(config.discord_allowed_users.is_empty());
    }

    #[test]
    fn discord_allowed_users_deserializes_list() {
        let config: BotConfig = toml::from_str(
            r#"
enabled = true
transport = "discord"
discord_bot_token = "Bot.Token.Here"
discord_channel_ids = ["123456789"]
discord_allowed_users = ["111222333", "444555666"]
"#,
        )
        .unwrap();
        assert_eq!(config.discord_allowed_users, vec!["111222333", "444555666"]);
    }
}
