#![cfg(feature = "_async")]

use std::sync::{Arc, Mutex};
use std::time::Duration;

use tele::testing::{FakeTelegramServer, RequestExpectation};
use tele::types::advanced::{
    AdvancedAnswerPreCheckoutQueryRequest, AdvancedAnswerShippingQueryRequest,
    AdvancedAnswerWebAppQueryRequest, AdvancedCreateInvoiceLinkRequest,
    AdvancedForwardMessagesRequest, AdvancedGetAvailableGiftsRequest,
    AdvancedGetCustomEmojiStickersRequest, AdvancedSendStickerRequest,
    AdvancedSetStickerKeywordsRequest,
};
use tele::types::{
    AnswerInlineQueryRequest, BotCommand, ChatAdministratorCapability, CreateInvoiceLinkRequest,
    GetFileRequest, GetMyCommandsRequest, InlineKeyboardButton, InlineKeyboardMarkup,
    InlineQueryResult, InlineQueryResultsButton, InputMedia, InputMediaPhoto, InputMediaVideo,
    LabeledPrice, MessageId, ParseMode, SendDocumentRequest, SendMediaGroupRequest,
    SendPhotoRequest, SendStickerRequest, SetChatPhotoRequest, SetMyCommandsRequest,
    ShippingOption, StickerFormat, Update, UploadStickerFileRequest, WebAppData,
};
use tele::{
    BanMemberOptions, BootstrapPlan, BootstrapRetryPolicy, BootstrapStepPhase, BootstrapStepStatus,
    Client, ClientMetric, Error, ErrorClass, MenuButtonConfig, RestrictMemberOptions, RetryConfig,
    UploadFile, UploadPart,
};

#[cfg(feature = "bot")]
use tele::types::BotCommandScope;

type DynError = Box<dyn std::error::Error + Send + Sync>;
type TestServer = FakeTelegramServer;

fn spawn_server(
    expected_path: &'static str,
    response_status: u16,
    response_body: &'static str,
) -> Result<(String, TestServer), DynError> {
    let server = FakeTelegramServer::single(
        RequestExpectation::post(expected_path).respond_json(response_status, response_body),
    )?;
    Ok((server.base_url().to_owned(), server))
}

fn spawn_server_with_checks(
    expected_path: &'static str,
    response_status: u16,
    response_body: &'static str,
    required_substrings: &'static [&'static str],
) -> Result<(String, TestServer), DynError> {
    let mut expectation =
        RequestExpectation::post(expected_path).respond_json(response_status, response_body);
    for required in required_substrings {
        expectation = expectation.contains_case_insensitive(*required);
    }
    let server = FakeTelegramServer::single(expectation)?;
    Ok((server.base_url().to_owned(), server))
}

fn spawn_server_script(
    script: Vec<(&'static str, u16, &'static str)>,
) -> Result<(String, TestServer), DynError> {
    let expectations = script
        .into_iter()
        .map(|(expected_path, response_status, response_body)| {
            RequestExpectation::post(expected_path).respond_json(response_status, response_body)
        })
        .collect();
    let server = FakeTelegramServer::start(expectations)?;
    Ok((server.base_url().to_owned(), server))
}

fn fast_retry(max_attempts: usize, allow_non_idempotent_retries: bool) -> RetryConfig {
    let mut retry = RetryConfig::default();
    retry.max_attempts = max_attempts;
    retry.base_backoff = Duration::from_millis(1);
    retry.max_backoff = Duration::from_millis(1);
    retry.jitter_ratio = 0.0;
    retry.allow_non_idempotent_retries = allow_non_idempotent_retries;
    retry
}

fn fast_bootstrap_retry(max_attempts: usize) -> BootstrapRetryPolicy {
    BootstrapRetryPolicy {
        max_attempts,
        base_backoff: Duration::from_millis(1),
        max_backoff: Duration::from_millis(1),
        jitter_ratio: 0.0,
        continue_on_failure: false,
    }
}

fn join_server(server: TestServer) -> Result<(), DynError> {
    let _ = server.finish()?;
    Ok(())
}

#[tokio::test]
async fn client_metric_hook_records_method_latency() -> Result<(), DynError> {
    let response =
        r#"{"ok":true,"result":{"id":42,"is_bot":true,"first_name":"tele","username":"tele_bot"}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getMe", 200, response)?;
    let metrics = Arc::new(Mutex::new(Vec::<ClientMetric>::new()));

    let client = Client::builder(base_url)?
        .bot_token("123:abc")?
        .on_metric({
            let metrics = Arc::clone(&metrics);
            move |metric| {
                if let Ok(mut captured) = metrics.lock() {
                    captured.push(metric.clone());
                }
            }
        })
        .build()?;

    let _ = client.bot().get_me().await?;
    join_server(handle)?;

    let captured = metrics.lock().map_err(|_| "client metric mutex poisoned")?;
    assert_eq!(captured.len(), 1);
    assert_eq!(captured[0].method, "getMe");
    assert!(captured[0].success);
    assert!(captured[0].latency >= Duration::ZERO);
    assert_eq!(captured[0].classification, None);

    Ok(())
}

#[tokio::test]
async fn get_me_success() -> Result<(), DynError> {
    let response =
        r#"{"ok":true,"result":{"id":42,"is_bot":true,"first_name":"tele","username":"tele_bot"}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getMe", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;

    let me = client.bot().get_me().await?;
    assert_eq!(me.username.as_deref(), Some("tele_bot"));

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn raw_layer_call_no_params_success() -> Result<(), DynError> {
    let response =
        r#"{"ok":true,"result":{"id":42,"is_bot":true,"first_name":"tele","username":"tele_bot"}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getMe", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let me: tele::types::User = client.raw().call_no_params("getMe").await?;
    assert_eq!(me.username.as_deref(), Some("tele_bot"));

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn typed_layer_advanced_request_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"gifts":[]}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getAvailableGifts", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let request = AdvancedGetAvailableGiftsRequest::new();
    let value: serde_json::Value = client.typed().call(&request).await?;
    assert!(value.is_object());

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn typed_layer_validates_advanced_request_before_transport() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:1")?
        .bot_token("123:abc")?
        .build()?;
    let request = AdvancedForwardMessagesRequest::new(1_i64, 2_i64, Vec::new());

    let error = match client.typed().call(&request).await {
        Ok(_) => return Err("empty required vector must be rejected before transport".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));
    Ok(())
}

#[tokio::test]
async fn advanced_service_validates_generated_request_before_transport() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:1")?
        .bot_token("123:abc")?
        .build()?;
    let request = AdvancedForwardMessagesRequest::new(1_i64, 2_i64, Vec::new());

    let error = match client
        .advanced()
        .forward_messages::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("empty required vector must be rejected before transport".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let mut request = AdvancedSendStickerRequest::new(1_i64, "sticker-file-id");
    request.business_connection_id = Some(" \n ".to_owned());
    let error = match client
        .advanced()
        .send_sticker::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("empty generated string identifiers must be rejected".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let request = AdvancedCreateInvoiceLinkRequest::new(
        "title",
        "description",
        "payload",
        "USD",
        vec![LabeledPrice::new("", 100)],
    );
    let error = match client
        .advanced()
        .create_invoice_link::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("invalid generated payment price entries must be rejected".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let mut request = AdvancedAnswerShippingQueryRequest::new("shipping-query", true);
    request.shipping_options = Some(vec![ShippingOption::new(
        "",
        "shipping",
        vec![LabeledPrice::new("shipping", 100)],
    )]);
    let error = match client
        .advanced()
        .answer_shipping_query::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("invalid generated shipping option entries must be rejected".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let request = AdvancedAnswerShippingQueryRequest::new("shipping-query", true);
    let error = match client
        .advanced()
        .answer_shipping_query::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("generated shipping success requires shipping options".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let mut request = AdvancedAnswerShippingQueryRequest::new("shipping-query", false);
    request.shipping_options = Some(vec![ShippingOption::new(
        "standard",
        "shipping",
        vec![LabeledPrice::new("shipping", 100)],
    )]);
    request.error_message = Some("unavailable".to_owned());
    let error = match client
        .advanced()
        .answer_shipping_query::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("generated shipping failure must omit shipping options".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let request = AdvancedAnswerPreCheckoutQueryRequest::new("checkout-query", false);
    let error = match client
        .advanced()
        .answer_pre_checkout_query::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("generated checkout failure requires an error message".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let mut request = AdvancedAnswerPreCheckoutQueryRequest::new("checkout-query", true);
    request.error_message = Some("declined".to_owned());
    let error = match client
        .advanced()
        .answer_pre_checkout_query::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("generated checkout success must omit error_message".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let request = AdvancedGetCustomEmojiStickersRequest::new(vec!["".to_owned()]);
    let error = match client
        .advanced()
        .get_custom_emoji_stickers::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => return Err("empty generated string array entries must be rejected".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    let mut request = AdvancedSetStickerKeywordsRequest::new("sticker-file-id");
    request.keywords = Some(vec!["ok".to_owned(), "\n".to_owned()]);
    let error = match client
        .advanced()
        .set_sticker_keywords::<serde_json::Value>(&request)
        .await
    {
        Ok(_) => {
            return Err("invalid optional generated string array entries must be rejected".into());
        }
        Err(error) => error,
    };
    assert!(matches!(error, Error::InvalidRequest { .. }));

    Ok(())
}

#[tokio::test]
async fn raw_retry_rejects_invalid_policy_before_request() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .build()?;
    let mut retry = RetryConfig::default();
    retry.base_backoff = Duration::ZERO;

    let error = match client
        .raw()
        .call_no_params_with_retry::<tele::types::User>("getMe", retry)
        .await
    {
        Ok(_) => return Err("invalid retry policy unexpectedly succeeded".into()),
        Err(error) => error,
    };
    assert!(matches!(error, Error::Configuration { .. }));

    Ok(())
}

#[tokio::test]
async fn raw_retry_does_not_multiply_transport_retries() -> Result<(), DynError> {
    let failure = r#"{"ok":false,"error_code":502,"description":"Bad Gateway"}"#;
    let (base_url, server) = spawn_server_script(vec![
        ("/bot123:abc/getMe", 502, failure),
        ("/bot123:abc/getMe", 502, failure),
    ])?;

    let client = Client::builder(base_url)?
        .bot_token("123:abc")?
        .retry_config(fast_retry(3, true))?
        .build()?;

    let error = match client
        .raw()
        .call_no_params_with_retry::<tele::types::User>("getMe", fast_retry(2, false))
        .await
    {
        Ok(_) => return Err("retry unexpectedly succeeded after method retry budget".into()),
        Err(error) => error,
    };

    assert!(matches!(error, Error::Api { .. }));
    assert_eq!(error.status().map(|status| status.as_u16()), Some(502));
    assert_eq!(server.finish()?.len(), 2);

    Ok(())
}

#[tokio::test]
async fn setup_bootstrap_retry_does_not_multiply_transport_retries() -> Result<(), DynError> {
    let failure = r#"{"ok":false,"error_code":502,"description":"Bad Gateway"}"#;
    let (base_url, server) = spawn_server_script(vec![
        ("/bot123:abc/getMe", 502, failure),
        ("/bot123:abc/getMe", 502, failure),
    ])?;

    let client = Client::builder(base_url)?
        .bot_token("123:abc")?
        .retry_config(fast_retry(3, true))?
        .build()?;

    let outcome = client
        .control()
        .setup()
        .bootstrap_with_retry(
            &BootstrapPlan::new().fail_fast_get_me(),
            fast_bootstrap_retry(2),
        )
        .await;
    let error = outcome
        .error()
        .ok_or("bootstrap retry unexpectedly succeeded")?;

    assert!(matches!(error, Error::Api { .. }));
    assert_eq!(error.status().map(|status| status.as_u16()), Some(502));
    assert_eq!(outcome.report.me.diagnostics.attempt_count, 2);
    assert_eq!(server.finish()?.len(), 2);

    Ok(())
}

#[tokio::test]
async fn bootstrap_retry_rejects_invalid_policy_before_work() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .build()?;
    let policy = BootstrapRetryPolicy {
        max_attempts: 0,
        ..BootstrapRetryPolicy::default()
    };

    let outcome = client
        .control()
        .setup()
        .bootstrap_with_retry(&BootstrapPlan::default(), policy)
        .await;
    assert!(matches!(outcome.error(), Some(Error::Configuration { .. })));

    Ok(())
}

#[tokio::test]
async fn api_error_exposes_retry_after() -> Result<(), DynError> {
    let response = r#"{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":3}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getMe", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;

    let err = match client.bot().get_me().await {
        Ok(_) => {
            return Err("expected Telegram API error".into());
        }
        Err(err) => err,
    };

    assert!(matches!(err, Error::Api { .. }));
    assert_eq!(err.retry_after(), Some(Duration::from_secs(3)));

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_send_text_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":7,"date":1710000000,"chat":{"id":1,"type":"private"},"text":"hello"}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/sendMessage", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let sent = client.app().send_text(1_i64, "hello").await?;
    assert_eq!(sent.message_id.0, 7);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_text_builder_supports_markup_and_common_options() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":9,"date":1710000001,"chat":{"id":1,"type":"private"},"text":"hello builder"}}"#;
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/sendMessage")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"text\":\"hello builder\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"disable_notification\":true")
            .contains_case_insensitive("\"protect_content\":true")
            .contains_case_insensitive("\"message_thread_id\":99")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":55")
            .contains_case_insensitive("\"link_preview_options\":{\"is_disabled\":true")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"Open\"")
            .contains_case_insensitive("\"callback_data\":\"open:1\"")
            .respond_json(200, response),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;
    let markup =
        InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback("Open", "open:1")?]);

    let sent = client
        .app()
        .text(1_i64, "hello builder")?
        .parse_mode(ParseMode::MarkdownV2)
        .reply_to_message(MessageId(55))
        .message_thread_id(99)
        .disable_notification(true)
        .protect_content(true)
        .disable_link_preview()
        .reply_markup(markup)
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 9);

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn app_callback_answer_builder_supports_common_options() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":true}"#;
    const CHECKS: [&str; 5] = [
        "\"callback_query_id\":\"callback-42\"",
        "\"text\":\"Updated\"",
        "\"show_alert\":true",
        "\"url\":\"https://example.com/callback\"",
        "\"cache_time\":30",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/answerCallbackQuery", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let ok = client
        .app()
        .callback_answer("callback-42")
        .text("Updated")
        .show_alert(true)
        .url("https://example.com/callback")
        .cache_time(30)
        .send()
        .await?;
    assert!(ok);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_media_builders_support_common_send_options() -> Result<(), DynError> {
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/sendPhoto")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"photo\":\"photo-file-id\"")
            .contains_case_insensitive("\"caption\":\"photo caption\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"has_spoiler\":true")
            .contains_case_insensitive("\"disable_notification\":true")
            .contains_case_insensitive("\"protect_content\":true")
            .contains_case_insensitive("\"message_thread_id\":11")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":55")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"View photo\"")
            .contains_case_insensitive("\"callback_data\":\"photo:1\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":10,"date":1710000002,"chat":{"id":1,"type":"private"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendDocument")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"document\":\"document-file-id\"")
            .contains_case_insensitive("\"thumbnail\":\"document-thumb-id\"")
            .contains_case_insensitive("\"caption\":\"document caption\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"disable_content_type_detection\":true")
            .contains_case_insensitive("\"disable_notification\":true")
            .contains_case_insensitive("\"protect_content\":true")
            .contains_case_insensitive("\"message_thread_id\":12")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":56")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"View document\"")
            .contains_case_insensitive("\"callback_data\":\"document:1\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":11,"date":1710000003,"chat":{"id":1,"type":"private"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendVideo")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"video\":\"video-file-id\"")
            .contains_case_insensitive("\"duration\":30")
            .contains_case_insensitive("\"width\":1920")
            .contains_case_insensitive("\"height\":1080")
            .contains_case_insensitive("\"thumbnail\":\"video-thumb-id\"")
            .contains_case_insensitive("\"caption\":\"video caption\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"supports_streaming\":true")
            .contains_case_insensitive("\"has_spoiler\":true")
            .contains_case_insensitive("\"disable_notification\":true")
            .contains_case_insensitive("\"protect_content\":true")
            .contains_case_insensitive("\"message_thread_id\":13")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":57")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"View video\"")
            .contains_case_insensitive("\"callback_data\":\"video:1\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":12,"date":1710000004,"chat":{"id":1,"type":"private"}}}"#,
            ),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;

    let photo_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "View photo",
        "photo:1",
    )?]);
    let photo = client
        .app()
        .photo(1_i64, "photo-file-id")
        .caption("photo caption")
        .parse_mode(ParseMode::MarkdownV2)
        .has_spoiler(true)
        .reply_to_message(MessageId(55))
        .message_thread_id(11)
        .disable_notification(true)
        .protect_content(true)
        .reply_markup(photo_markup)
        .send()
        .await?;
    assert_eq!(photo.message_id.0, 10);

    let document_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "View document",
        "document:1",
    )?]);
    let document = client
        .app()
        .document(1_i64, "document-file-id")
        .thumbnail("document-thumb-id")
        .caption("document caption")
        .parse_mode(ParseMode::MarkdownV2)
        .disable_content_type_detection(true)
        .reply_to_message(MessageId(56))
        .message_thread_id(12)
        .disable_notification(true)
        .protect_content(true)
        .reply_markup(document_markup)
        .send()
        .await?;
    assert_eq!(document.message_id.0, 11);

    let video_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "View video",
        "video:1",
    )?]);
    let video = client
        .app()
        .video(1_i64, "video-file-id")
        .duration(30)
        .width(1920)
        .height(1080)
        .thumbnail("video-thumb-id")
        .caption("video caption")
        .parse_mode(ParseMode::MarkdownV2)
        .supports_streaming(true)
        .has_spoiler(true)
        .reply_to_message(MessageId(57))
        .message_thread_id(13)
        .disable_notification(true)
        .protect_content(true)
        .reply_markup(video_markup)
        .send()
        .await?;
    assert_eq!(video.message_id.0, 12);

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn app_richer_media_builders_support_common_send_options() -> Result<(), DynError> {
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/sendAudio")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"audio\":\"audio-file-id\"")
            .contains_case_insensitive("\"caption\":\"audio caption\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"duration\":120")
            .contains_case_insensitive("\"performer\":\"tele band\"")
            .contains_case_insensitive("\"title\":\"tele song\"")
            .contains_case_insensitive("\"thumbnail\":\"audio-thumb-id\"")
            .contains_case_insensitive("\"message_thread_id\":14")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":58")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"Play audio\"")
            .contains_case_insensitive("\"callback_data\":\"audio:1\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":13,"date":1710000005,"chat":{"id":1,"type":"private"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendAnimation")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"animation\":\"animation-file-id\"")
            .contains_case_insensitive("\"caption\":\"animation caption\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"duration\":7")
            .contains_case_insensitive("\"width\":480")
            .contains_case_insensitive("\"height\":320")
            .contains_case_insensitive("\"thumbnail\":\"animation-thumb-id\"")
            .contains_case_insensitive("\"has_spoiler\":true")
            .contains_case_insensitive("\"message_thread_id\":15")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":59")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"Play animation\"")
            .contains_case_insensitive("\"callback_data\":\"animation:1\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":14,"date":1710000006,"chat":{"id":1,"type":"private"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendVoice")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"voice\":\"voice-file-id\"")
            .contains_case_insensitive("\"caption\":\"voice caption\"")
            .contains_case_insensitive("\"parse_mode\":\"MarkdownV2\"")
            .contains_case_insensitive("\"duration\":25")
            .contains_case_insensitive("\"message_thread_id\":16")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":60")
            .contains_case_insensitive("\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"Play voice\"")
            .contains_case_insensitive("\"callback_data\":\"voice:1\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":15,"date":1710000007,"chat":{"id":1,"type":"private"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendSticker")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"sticker\":\"sticker-file-id\"")
            .contains_case_insensitive("\"emoji\":\":fire:\"")
            .contains_case_insensitive("\"message_thread_id\":17")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":16,"date":1710000008,"chat":{"id":1,"type":"private"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendMediaGroup")
            .contains_case_insensitive("\"chat_id\":1")
            .contains_case_insensitive("\"media\":[{\"type\":\"photo\",\"media\":\"group-photo-file-id\",\"caption\":\"group photo caption\"")
            .contains_case_insensitive("\"type\":\"video\",\"media\":\"group-video-file-id\",\"caption\":\"group video caption\"")
            .contains_case_insensitive("\"supports_streaming\":true")
            .contains_case_insensitive("\"message_thread_id\":18")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":62")
            .respond_json(
                200,
                r#"{"ok":true,"result":[{"message_id":17,"date":1710000009,"chat":{"id":1,"type":"private"}},{"message_id":18,"date":1710000010,"chat":{"id":1,"type":"private"}}]}"#,
            ),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;

    let audio_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "Play audio",
        "audio:1",
    )?]);
    let audio = client
        .app()
        .audio(1_i64, "audio-file-id")
        .caption("audio caption")
        .parse_mode(ParseMode::MarkdownV2)
        .duration(120)
        .performer("tele band")
        .title("tele song")
        .thumbnail("audio-thumb-id")
        .reply_to_message(MessageId(58))
        .message_thread_id(14)
        .reply_markup(audio_markup)
        .send()
        .await?;
    assert_eq!(audio.message_id.0, 13);

    let animation_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "Play animation",
        "animation:1",
    )?]);
    let animation = client
        .app()
        .animation(1_i64, "animation-file-id")
        .caption("animation caption")
        .parse_mode(ParseMode::MarkdownV2)
        .duration(7)
        .width(480)
        .height(320)
        .thumbnail("animation-thumb-id")
        .has_spoiler(true)
        .reply_to_message(MessageId(59))
        .message_thread_id(15)
        .reply_markup(animation_markup)
        .send()
        .await?;
    assert_eq!(animation.message_id.0, 14);

    let voice_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "Play voice",
        "voice:1",
    )?]);
    let voice = client
        .app()
        .voice(1_i64, "voice-file-id")
        .caption("voice caption")
        .parse_mode(ParseMode::MarkdownV2)
        .duration(25)
        .reply_to_message(MessageId(60))
        .message_thread_id(16)
        .reply_markup(voice_markup)
        .send()
        .await?;
    assert_eq!(voice.message_id.0, 15);

    let sticker_markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "Review sticker",
        "sticker:1",
    )?]);
    let sticker = client
        .app()
        .sticker(1_i64, "sticker-file-id")
        .emoji(":fire:")
        .reply_to_message(MessageId(61))
        .message_thread_id(17)
        .reply_markup(sticker_markup)
        .send()
        .await?;
    assert_eq!(sticker.message_id.0, 16);

    let group = client
        .app()
        .media_group(
            1_i64,
            vec![
                serde_json::from_value::<InputMedia>(serde_json::json!({
                    "type": "photo",
                    "media": "group-photo-file-id",
                    "caption": "group photo caption",
                    "parse_mode": "MarkdownV2"
                }))?,
                serde_json::from_value::<InputMedia>(serde_json::json!({
                    "type": "video",
                    "media": "group-video-file-id",
                    "caption": "group video caption",
                    "parse_mode": "MarkdownV2",
                    "width": 1920,
                    "height": 1080,
                    "duration": 30,
                    "supports_streaming": true
                }))?,
            ],
        )?
        .reply_to_message(MessageId(62))
        .message_thread_id(18)
        .send()
        .await?;
    assert_eq!(group.len(), 2);
    assert_eq!(group[0].message_id.0, 17);

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn app_sticker_builder_supports_common_send_options() -> Result<(), DynError> {
    let client = Client::builder("https://api.telegram.org")?
        .bot_token("123:abc")?
        .build()?;
    let markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "Review sticker",
        "sticker:1",
    )?]);

    let request = client
        .app()
        .sticker(1_i64, "sticker-file-id")
        .emoji(":fire:")
        .reply_to_message(MessageId(61))
        .message_thread_id(17)
        .reply_markup(markup)
        .into_request();

    assert_eq!(request.emoji.as_deref(), Some(":fire:"));
    assert_eq!(request.message_thread_id, Some(17));
    assert_eq!(
        serde_json::to_value(request.reply_parameters.as_ref())?,
        serde_json::json!({"message_id":61})
    );
    assert_eq!(
        serde_json::to_value(request.reply_markup.as_ref())?,
        serde_json::json!({
            "inline_keyboard": [[{"text":"Review sticker","callback_data":"sticker:1"}]]
        })
    );

    Ok(())
}

#[tokio::test]
async fn app_reply_text_uses_join_request_user_chat_id() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":8,"date":1710000001,"chat":{"id":7001,"type":"private"},"text":"hello"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"chat_id\":7001",
            "\"text\":\"hello\"",
            "\"disable_notification\":true",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 43,
        "chat_join_request": {
            "chat": {"id": -10010, "type": "supergroup", "title": "mods"},
            "from": {"id": 701, "is_bot": false, "first_name": "candidate"},
            "user_chat_id": 7001,
            "date": 1710000001
        }
    }))?;

    let sent = client
        .app()
        .reply(&update, "hello")?
        .disable_notification(true)
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 8);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_quotes_source_message() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":9,"date":1710000002,"chat":{"id":-10010,"type":"supergroup","title":"mods"},"text":"quoted"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"chat_id\":-10010",
            "\"text\":\"quoted\"",
            "\"reply_parameters\":{\"message_id\":55",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 44,
        "message": {
            "message_id": 55,
            "date": 1710000001,
            "chat": {"id": -10010, "type": "supergroup", "title": "mods"},
            "from": {"id": 701, "is_bot": false, "first_name": "candidate"},
            "text": "/start"
        }
    }))?;

    let sent = client.app().reply(&update, "quoted")?.send().await?;
    assert_eq!(sent.message_id.0, 9);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_targets_inaccessible_callback_message() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":9,"date":1710000002,"chat":{"id":-10010,"type":"supergroup","title":"mods"},"text":"quoted"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"chat_id\":-10010",
            "\"text\":\"quoted\"",
            "\"reply_parameters\":{\"message_id\":55",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 45,
        "callback_query": {
            "id": "cb-inaccessible",
            "from": {"id": 701, "is_bot": false, "first_name": "candidate"},
            "message": {
                "message_id": 55,
                "date": 0,
                "chat": {"id": -10010, "type": "supergroup", "title": "mods"}
            },
            "chat_instance": "ci",
            "data": "payload"
        }
    }))?;

    let sent = client.app().reply(&update, "quoted")?.send().await?;
    assert_eq!(sent.message_id.0, 9);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_supports_business_message_updates() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":10,"date":1710000003,"chat":{"id":7001,"type":"private","first_name":"customer"},"text":"business reply"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"business_connection_id\":\"business-1\"",
            "\"chat_id\":7001",
            "\"text\":\"business reply\"",
            "\"reply_parameters\":{\"message_id\":77",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 46,
        "business_message": {
            "message_id": 77,
            "business_connection_id": "business-1",
            "date": 1710000002,
            "chat": {"id": 7001, "type": "private", "first_name": "customer"},
            "from": {"id": 7001, "is_bot": false, "first_name": "customer"},
            "text": "business hello"
        }
    }))?;

    let sent = client
        .app()
        .reply(&update, "business reply")?
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 10);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_preserves_deleted_business_message_context() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":11,"date":1710000004,"chat":{"id":7001,"type":"private","first_name":"customer"},"text":"business cleanup"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"business_connection_id\":\"business-1\"",
            "\"chat_id\":7001",
            "\"text\":\"business cleanup\"",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 49,
        "deleted_business_messages": {
            "business_connection_id": "business-1",
            "chat": {"id": 7001, "type": "private", "first_name": "customer"},
            "message_ids": [77, 78]
        }
    }))?;

    let sent = client
        .app()
        .reply(&update, "business cleanup")?
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 11);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_supports_business_connection_updates() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":12,"date":1710000005,"chat":{"id":7101,"type":"private","first_name":"customer"},"text":"business connected"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"business_connection_id\":\"business-2\"",
            "\"chat_id\":7101",
            "\"text\":\"business connected\"",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 50,
        "business_connection": {
            "id": "business-2",
            "user": {"id": 7101, "is_bot": false, "first_name": "customer"},
            "user_chat_id": 7101,
            "date": 1710000005,
            "is_enabled": true
        }
    }))?;

    let sent = client
        .app()
        .reply(&update, "business connected")?
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 12);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_quotes_reaction_source_message() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":13,"date":1710000006,"chat":{"id":-10012,"type":"supergroup","title":"mods"},"text":"reaction reply"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &[
            "\"chat_id\":-10012",
            "\"text\":\"reaction reply\"",
            "\"reply_parameters\":{\"message_id\":88",
        ],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 51,
        "message_reaction": {
            "chat": {"id": -10012, "type": "supergroup", "title": "mods"},
            "message_id": 88,
            "user": {"id": 7102, "is_bot": false, "first_name": "reactor"},
            "date": 1710000006,
            "old_reaction": [],
            "new_reaction": [{"type": "emoji", "emoji": "👍"}]
        }
    }))?;

    let sent = client
        .app()
        .reply(&update, "reaction reply")?
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 13);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_supports_chat_boost_updates() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":14,"date":1710000007,"chat":{"id":-10013,"type":"supergroup","title":"mods"},"text":"boost reply"}}"#;
    let (base_url, handle) = spawn_server_with_checks(
        "/bot123:abc/sendMessage",
        200,
        response,
        &["\"chat_id\":-10013", "\"text\":\"boost reply\""],
    )?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 52,
        "chat_boost": {
            "chat": {"id": -10013, "type": "supergroup", "title": "mods"},
            "boost": {
                "boost_id": "boost-2",
                "add_date": 1710000007,
                "expiration_date": 1710086407,
                "source": {
                    "source": "premium",
                    "user": {"id": 7103, "is_bot": false, "first_name": "booster"}
                }
            }
        }
    }))?;

    let sent = client.app().reply(&update, "boost reply")?.send().await?;
    assert_eq!(sent.message_id.0, 14);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_business_media_replies_preserve_reply_context() -> Result<(), DynError> {
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/sendPhoto")
            .contains_case_insensitive("\"business_connection_id\":\"business-1\"")
            .contains_case_insensitive("\"chat_id\":7001")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":77")
            .contains_case_insensitive("\"photo\":\"photo-file-id\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":11,"date":1710000004,"chat":{"id":7001,"type":"private","first_name":"customer"},"photo":[{"file_id":"photo_1","file_unique_id":"photo_unique_1","width":10,"height":10}]}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendSticker")
            .contains_case_insensitive("\"business_connection_id\":\"business-1\"")
            .contains_case_insensitive("\"chat_id\":7001")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":77")
            .contains_case_insensitive("\"sticker\":\"sticker-file-id\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"message_id":12,"date":1710000005,"chat":{"id":7001,"type":"private","first_name":"customer"}}}"#,
            ),
        RequestExpectation::post("/bot123:abc/sendMediaGroup")
            .contains_case_insensitive("\"business_connection_id\":\"business-1\"")
            .contains_case_insensitive("\"chat_id\":7001")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":77")
            .contains_case_insensitive("\"media\"")
            .respond_json(
                200,
                r#"{"ok":true,"result":[{"message_id":13,"date":1710000006,"chat":{"id":7001,"type":"private","first_name":"customer"}},{"message_id":14,"date":1710000006,"chat":{"id":7001,"type":"private","first_name":"customer"}}]}"#,
            ),
    ];
    let server = FakeTelegramServer::start(expectations)?;
    let base_url = server.base_url().to_owned();

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 48,
        "business_message": {
            "message_id": 77,
            "business_connection_id": "business-1",
            "date": 1710000002,
            "chat": {"id": 7001, "type": "private", "first_name": "customer"},
            "from": {"id": 7001, "is_bot": false, "first_name": "customer"},
            "text": "business hello"
        }
    }))?;

    let photo = client
        .app()
        .reply_photo(&update, "photo-file-id")?
        .send()
        .await?;
    assert_eq!(photo.message_id.0, 11);

    let sticker = client
        .app()
        .reply_sticker(&update, "sticker-file-id")?
        .send()
        .await?;
    assert_eq!(sticker.message_id.0, 12);

    let album = client
        .app()
        .reply_media_group(
            &update,
            [
                InputMediaPhoto::new("media-photo-1"),
                InputMediaPhoto::new("media-photo-2"),
            ],
        )?
        .send()
        .await?;
    assert_eq!(album.len(), 2);
    assert_eq!(album[0].message_id.0, 13);

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn app_reply_text_rejects_guest_message_updates() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 47,
        "guest_message": {
            "message_id": 78,
            "guest_query_id": "guest-1",
            "date": 1710000003,
            "chat": {"id": 8001, "type": "private", "first_name": "guest"},
            "from": {"id": 8001, "is_bot": false, "first_name": "guest"},
            "text": "guest hello"
        }
    }))?;

    assert!(matches!(
        client.app().reply(&update, "guest reply"),
        Err(Error::InvalidRequest { reason }) if reason.contains("answerGuestQuery")
    ));

    Ok(())
}

#[tokio::test]
async fn transport_error_redacts_token() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .request_timeout(Duration::from_millis(100))
        .total_timeout(Some(Duration::from_millis(300)))
        .build()?;

    let err = match client.bot().get_me().await {
        Ok(_) => {
            return Err("expected transport error".into());
        }
        Err(err) => err,
    };

    let text = err.to_string();
    assert!(!text.contains("123:abc"));
    assert!(err.request_id().is_some());
    Ok(())
}

#[tokio::test]
async fn build_configuration_error_is_not_mapped_as_transport() -> Result<(), DynError> {
    let error = match Client::builder("https://api.telegram.org")?
        .bot_token("123:abc")?
        .no_proxy(["example.com", "[::1]not-a-port"])
        .build()
    {
        Ok(_) => return Err("expected build failure".into()),
        Err(error) => error,
    };

    assert!(matches!(error, Error::Configuration { .. }));
    assert_eq!(error.classification(), ErrorClass::Configuration);
    assert!(!error.is_retryable());
    Ok(())
}

#[tokio::test]
async fn set_and_get_my_commands_success() -> Result<(), DynError> {
    let set_response = r#"{"ok":true,"result":true}"#;
    let (set_base_url, set_handle) = spawn_server("/bot123:abc/setMyCommands", 200, set_response)?;

    let set_client = Client::builder(set_base_url)?
        .bot_token("123:abc")?
        .build()?;
    let set_request = SetMyCommandsRequest::new(vec![BotCommand::new("start", "start the bot")?])?;
    let set_result = set_client.bot().set_my_commands(&set_request).await?;
    assert!(set_result);
    join_server(set_handle)?;

    let get_response =
        r#"{"ok":true,"result":[{"command":"start","description":"start the bot"}]}"#;
    let (get_base_url, get_handle) = spawn_server("/bot123:abc/getMyCommands", 200, get_response)?;

    let get_client = Client::builder(get_base_url)?
        .bot_token("123:abc")?
        .build()?;
    let get_request = GetMyCommandsRequest::default();
    let commands = get_client.bot().get_my_commands(&get_request).await?;
    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].command, "start");
    join_server(get_handle)?;

    Ok(())
}

#[tokio::test]
async fn bootstrap_skips_unchanged_commands_and_menu_button() -> Result<(), DynError> {
    let script = vec![
        (
            "/bot123:abc/getMyCommands",
            200,
            r#"{"ok":true,"result":[{"command":"start","description":"start the bot"}]}"#,
        ),
        (
            "/bot123:abc/getChatMenuButton",
            200,
            r#"{"ok":true,"result":{"type":"commands"}}"#,
        ),
    ];
    let (base_url, handle) = spawn_server_script(script)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let plan = BootstrapPlan::new()
        .commands(vec![BotCommand::new("start", "start the bot")?])?
        .menu_button(MenuButtonConfig::commands());

    let outcome = client.control().setup().bootstrap(&plan).await;
    assert!(outcome.is_success());
    let Some(commands) = outcome.report.commands.as_ref() else {
        return Err("expected commands step report".into());
    };
    assert_eq!(commands.applied, Some(false));
    assert_eq!(commands.synced, Some(true));
    let Some(menu_button) = outcome.report.menu_button.as_ref() else {
        return Err("expected menu button step report".into());
    };
    assert_eq!(menu_button.applied, Some(false));
    assert_eq!(menu_button.synced, Some(true));

    join_server(handle)?;
    Ok(())
}

#[cfg(feature = "bot")]
#[derive(Clone, Debug)]
enum DemoCommand {
    Start,
}

#[cfg(feature = "bot")]
impl tele::bot::BotCommands for DemoCommand {
    fn parse(command: &str, _args: &str) -> Option<Self> {
        if command == "start" {
            Some(Self::Start)
        } else {
            None
        }
    }

    fn descriptions() -> &'static [tele::bot::CommandDescription] {
        &[tele::bot::CommandDescription {
            command: "start",
            description: "start command",
        }]
    }
}

#[cfg(feature = "bot")]
#[tokio::test]
async fn setup_set_typed_commands_with_scope_and_language() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":true}"#;
    const CHECKS: [&str; 4] = [
        "\"commands\":[{\"command\":\"start\",\"description\":\"start command\"}]",
        "\"scope\":{\"type\":\"all_private_chats\"}",
        "\"language_code\":\"zh-hans\"",
        "POST /bot123:abc/setMyCommands HTTP/1.1",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/setMyCommands", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let applied = client
        .control()
        .setup()
        .set_typed_commands_with_options::<DemoCommand>(
            Some(BotCommandScope::AllPrivateChats),
            Some("zh-hans".to_owned()),
        )
        .await?;
    assert!(applied);

    join_server(handle)?;
    Ok(())
}

#[cfg(feature = "bot")]
#[tokio::test]
async fn bootstrap_plan_typed_commands_with_scope_and_language() -> Result<(), DynError> {
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/getMyCommands")
            .contains_case_insensitive("\"scope\":{\"type\":\"all_private_chats\"}")
            .contains_case_insensitive("\"language_code\":\"zh-hans\"")
            .respond_json(200, r#"{"ok":true,"result":[]}"#),
        RequestExpectation::post("/bot123:abc/setMyCommands")
            .contains_case_insensitive(
                "\"commands\":[{\"command\":\"start\",\"description\":\"start command\"}]",
            )
            .contains_case_insensitive("\"scope\":{\"type\":\"all_private_chats\"}")
            .contains_case_insensitive("\"language_code\":\"zh-hans\"")
            .respond_json(200, r#"{"ok":true,"result":true}"#),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;
    let plan = BootstrapPlan::new().typed_commands_with_options::<DemoCommand>(
        Some(BotCommandScope::AllPrivateChats),
        Some("zh-hans".to_owned()),
    )?;

    let outcome = client.control().setup().bootstrap(&plan).await;
    assert!(outcome.is_success());
    let Some(commands) = outcome.report.commands.as_ref() else {
        return Err("expected commands step report".into());
    };
    assert_eq!(commands.applied, Some(true));
    assert_eq!(commands.synced, Some(true));

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn bootstrap_retry_can_continue_on_failure() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .request_timeout(Duration::from_millis(100))
        .total_timeout(Some(Duration::from_millis(300)))
        .build()?;

    let plan = BootstrapPlan::new().commands(vec![BotCommand::new("start", "start bot")?])?;
    let outcome = client
        .control()
        .setup()
        .bootstrap_with_retry(
            &plan,
            BootstrapRetryPolicy {
                max_attempts: 1,
                continue_on_failure: true,
                ..BootstrapRetryPolicy::default()
            },
        )
        .await;
    assert!(outcome.is_success());
    let Some(commands) = outcome.report.commands.as_ref() else {
        return Err("expected commands step report".into());
    };
    assert_eq!(commands.applied, Some(false));
    assert_eq!(commands.diagnostics.status, BootstrapStepStatus::Warned);

    Ok(())
}

#[tokio::test]
async fn setup_bootstrap_warns_on_retryable_get_me_after_retries() -> Result<(), DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .request_timeout(Duration::from_millis(40))
        .total_timeout(Some(Duration::from_millis(120)))
        .build()?;
    let plan = BootstrapPlan::new().warn_and_continue_on_retryable_get_me();
    let outcome = client
        .control()
        .setup()
        .bootstrap_with_retry(
            &plan,
            BootstrapRetryPolicy {
                max_attempts: 2,
                base_backoff: Duration::from_millis(1),
                max_backoff: Duration::from_millis(5),
                continue_on_failure: false,
                ..BootstrapRetryPolicy::default()
            },
        )
        .await;

    assert!(outcome.is_success());
    assert!(outcome.error.is_none());
    assert!(outcome.report.me.value.is_none());
    assert_eq!(
        outcome.report.me.diagnostics.status,
        BootstrapStepStatus::Warned
    );
    assert_eq!(
        outcome.report.me.diagnostics.phase,
        Some(BootstrapStepPhase::Fetch)
    );
    assert_eq!(
        outcome.report.me.diagnostics.classification,
        Some(ErrorClass::Transport)
    );
    assert!(outcome.report.me.diagnostics.retryable);
    assert_eq!(outcome.report.me.diagnostics.attempt_count, 2);
    assert!(outcome.report.me.diagnostics.request_id.is_some());

    Ok(())
}

#[tokio::test]
async fn setup_bootstrap_reports_unchanged_steps() -> Result<(), DynError> {
    let script = vec![
        (
            "/bot123:abc/getMyCommands",
            200,
            r#"{"ok":true,"result":[{"command":"start","description":"start the bot"}]}"#,
        ),
        (
            "/bot123:abc/getChatMenuButton",
            200,
            r#"{"ok":true,"result":{"type":"commands"}}"#,
        ),
    ];
    let (base_url, handle) = spawn_server_script(script)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let plan = BootstrapPlan::new()
        .commands(vec![BotCommand::new("start", "start the bot")?])?
        .menu_button(MenuButtonConfig::commands());

    let outcome = client
        .control()
        .setup()
        .bootstrap_with_retry(&plan, BootstrapRetryPolicy::default())
        .await;

    assert!(outcome.is_success());
    let Some(commands) = outcome.report.commands.as_ref() else {
        return Err("expected commands step report".into());
    };
    assert_eq!(commands.applied, Some(false));
    assert_eq!(commands.synced, Some(true));
    assert_eq!(commands.diagnostics.status, BootstrapStepStatus::Unchanged);
    assert_eq!(commands.diagnostics.phase, Some(BootstrapStepPhase::Check));
    assert_eq!(commands.diagnostics.attempt_count, 1);

    let Some(menu_button) = outcome.report.menu_button.as_ref() else {
        return Err("expected menu button step report".into());
    };
    assert_eq!(menu_button.applied, Some(false));
    assert_eq!(menu_button.synced, Some(true));
    assert_eq!(
        menu_button.diagnostics.status,
        BootstrapStepStatus::Unchanged
    );
    assert_eq!(
        menu_button.diagnostics.phase,
        Some(BootstrapStepPhase::Check)
    );
    assert_eq!(menu_button.diagnostics.attempt_count, 1);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn web_app_answer_query_from_payload() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"inline_message_id":"inline-42"}}"#;
    const CHECKS: [&str; 3] = [
        "\"web_app_query_id\":\"query-42\"",
        "\"type\":\"article\"",
        "\"title\":\"From Payload\"",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/answerWebAppQuery", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let web_app_data = WebAppData::new("{\"query_id\":\"query-42\",\"item\":\"coffee\"}", "Open");
    let result = InlineQueryResult::article("r-42", "From Payload", "ok")?;
    let sent = client
        .app()
        .web_app()
        .answer_query_from_payload::<serde_json::Value, _>(&web_app_data, result)
        .await?;
    assert_eq!(sent.inline_message_id, "inline-42");

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn setup_and_web_app_facades_handle_menu_button_and_query_answer() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":true}"#;
    let answer_response = r#"{"ok":true,"result":{"inline_message_id":"inline-99"}}"#;
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/setChatMenuButton")
            .contains_case_insensitive("\"chat_id\":42")
            .contains_case_insensitive("\"menu_button\":{\"type\":\"web_app\"")
            .contains_case_insensitive("\"url\":\"https://example.com/mini-app\"")
            .respond_json(200, response),
        RequestExpectation::post("/bot123:abc/answerWebAppQuery")
            .contains_case_insensitive("\"web_app_query_id\":\"query-99\"")
            .contains_case_insensitive("\"title\":\"Facade Answer\"")
            .respond_json(200, answer_response),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;
    let applied = client
        .control()
        .setup()
        .set_menu_button(MenuButtonConfig::for_chat_web_app(
            42,
            "Open Mini App",
            "https://example.com/mini-app",
        ))
        .await?;
    assert!(applied);

    let web_app_data = WebAppData::new("{\"query_id\":\"query-99\",\"item\":\"tea\"}", "Open");
    let result = InlineQueryResult::article("article-99", "Facade Answer", "done")?;
    let sent = client
        .app()
        .web_app()
        .answer_query_from_payload::<serde_json::Value, _>(&web_app_data, result)
        .await?;
    assert_eq!(sent.inline_message_id, "inline-99");

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn app_membership_facade_handles_bot_member_and_capabilities() -> Result<(), DynError> {
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/getMe").respond_json(
            200,
            r#"{"ok":true,"result":{"id":999,"is_bot":true,"first_name":"tele","username":"tele_bot"}}"#,
        ),
        RequestExpectation::post("/bot123:abc/getChatMember")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"user_id\":999")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"status":"administrator","user":{"id":999,"is_bot":true,"first_name":"tele"},"can_manage_chat":true,"can_delete_messages":true}}"#,
            ),
        RequestExpectation::post("/bot123:abc/getMe").respond_json(
            200,
            r#"{"ok":true,"result":{"id":999,"is_bot":true,"first_name":"tele","username":"tele_bot"}}"#,
        ),
        RequestExpectation::post("/bot123:abc/getChatMember")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"user_id\":999")
            .respond_json(
                200,
                r#"{"ok":true,"result":{"status":"administrator","user":{"id":999,"is_bot":true,"first_name":"tele"},"can_manage_chat":true,"can_delete_messages":true}}"#,
            ),
        RequestExpectation::post("/bot123:abc/getChatAdministrators")
            .contains_case_insensitive("\"chat_id\":-10010")
            .respond_json(
                200,
                r#"{"ok":true,"result":[{"status":"administrator","user":{"id":999,"is_bot":true,"first_name":"tele"},"can_manage_chat":true,"can_delete_messages":true},{"status":"administrator","user":{"id":701,"is_bot":false,"first_name":"owner"},"can_manage_chat":true,"can_restrict_members":true}]}"#,
            ),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;
    let membership = client.app().membership();

    let bot_member = membership.bot_member(-10010_i64).await?;
    assert_eq!(bot_member.user().id.0, 999);
    assert!(bot_member.has_capability(ChatAdministratorCapability::ManageChat));

    let missing = membership
        .bot_missing_capabilities(
            -10010_i64,
            &[
                ChatAdministratorCapability::ManageChat,
                ChatAdministratorCapability::RestrictMembers,
            ],
        )
        .await?;
    assert_eq!(missing, vec![ChatAdministratorCapability::RestrictMembers]);

    let administrators = membership.administrators(-10010_i64).await?;
    assert_eq!(administrators.len(), 2);

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn moderation_facade_handles_join_actions_and_member_controls() -> Result<(), DynError> {
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/approveChatJoinRequest")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"user_id\":701")
            .respond_json(200, r#"{"ok":true,"result":true}"#),
        RequestExpectation::post("/bot123:abc/declineChatJoinRequest")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"user_id\":701")
            .respond_json(200, r#"{"ok":true,"result":true}"#),
        RequestExpectation::post("/bot123:abc/banChatMember")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"user_id\":701")
            .contains_case_insensitive("\"until_date\":1710009999")
            .contains_case_insensitive("\"revoke_messages\":true")
            .respond_json(200, r#"{"ok":true,"result":true}"#),
        RequestExpectation::post("/bot123:abc/restrictChatMember")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"user_id\":701")
            .contains_case_insensitive("\"can_send_messages\":false")
            .contains_case_insensitive("\"can_manage_topics\":false")
            .contains_case_insensitive("\"use_independent_chat_permissions\":true")
            .contains_case_insensitive("\"until_date\":1710011111")
            .respond_json(200, r#"{"ok":true,"result":true}"#),
        RequestExpectation::post("/bot123:abc/deleteMessage")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"message_id\":55")
            .respond_json(200, r#"{"ok":true,"result":true}"#),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;
    let join_update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 43,
        "chat_join_request": {
            "chat": {"id": -10010, "type": "supergroup", "title": "mods"},
            "from": {"id": 701, "is_bot": false, "first_name": "candidate"},
            "user_chat_id": 7001,
            "date": 1710000001
        }
    }))?;
    let message_update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 44,
        "message": {
            "message_id": 55,
            "date": 1710000002,
            "chat": {"id": -10010, "type": "supergroup", "title": "mods"},
            "from": {"id": 701, "is_bot": false, "first_name": "candidate"},
            "text": "spam"
        }
    }))?;
    let message = message_update
        .message
        .as_deref()
        .ok_or("missing test message")?;

    assert!(
        client
            .app()
            .moderation()
            .approve_join_request_from_update(&join_update)
            .await?
    );
    assert!(
        client
            .app()
            .moderation()
            .decline_join_request_from_update(&join_update)
            .await?
    );
    assert!(
        client
            .app()
            .moderation()
            .ban_author_with(
                message,
                BanMemberOptions::new()
                    .until_date(1710009999)
                    .revoke_messages(true),
            )
            .await?
    );
    assert!(
        client
            .app()
            .moderation()
            .mute_author_with(
                message,
                RestrictMemberOptions::new()
                    .use_independent_chat_permissions(true)
                    .until_date(1710011111),
            )
            .await?
    );
    assert!(
        client
            .app()
            .moderation()
            .delete_from_update(&message_update)
            .await?
    );

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn moderation_notice_facade_reuses_text_builder() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":56,"date":1710000003,"chat":{"id":-10010,"type":"supergroup","title":"mods"},"message_thread_id":88,"text":"Message removed"}}"#;
    let expectations = vec![
        RequestExpectation::post("/bot123:abc/sendMessage")
            .contains_case_insensitive("\"chat_id\":-10010")
            .contains_case_insensitive("\"text\":\"Message removed\"")
            .contains_case_insensitive("\"reply_parameters\":{\"message_id\":55")
            .contains_case_insensitive("\"message_thread_id\":88")
            .contains_case_insensitive("\"disable_notification\":true")
            .contains_case_insensitive(
                "\"reply_markup\":{\"inline_keyboard\":[[{\"text\":\"Review\"",
            )
            .contains_case_insensitive("\"callback_data\":\"review:55\"")
            .respond_json(200, response),
    ];
    let server = FakeTelegramServer::start(expectations)?;

    let client = Client::builder(server.base_url())?
        .bot_token("123:abc")?
        .build()?;
    let update: Update = serde_json::from_value(serde_json::json!({
        "update_id": 45,
        "message": {
            "message_id": 55,
            "message_thread_id": 88,
            "date": 1710000002,
            "chat": {"id": -10010, "type": "supergroup", "title": "mods"},
            "from": {"id": 701, "is_bot": false, "first_name": "candidate"},
            "text": "spam"
        }
    }))?;
    let message = update.message.as_deref().ok_or("missing test message")?;
    let markup = InlineKeyboardMarkup::single_row(vec![InlineKeyboardButton::callback(
        "Review",
        "review:55",
    )?]);

    let sent = client
        .app()
        .moderation()
        .notice()
        .for_message(message, "Message removed")?
        .disable_notification(true)
        .reply_markup(markup)
        .send()
        .await?;
    assert_eq!(sent.message_id.0, 56);

    join_server(server)?;
    Ok(())
}

#[tokio::test]
async fn setup_set_chat_menu_button_uses_high_level_helper() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":true}"#;
    const CHECKS: [&str; 4] = [
        "\"chat_id\":42",
        "\"menu_button\":{\"type\":\"web_app\"",
        "\"text\":\"Open Mini App\"",
        "\"url\":\"https://example.com/mini-app\"",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/setChatMenuButton", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let applied = client
        .control()
        .setup()
        .set_menu_button(MenuButtonConfig::for_chat_web_app(
            42,
            "Open Mini App",
            "https://example.com/mini-app",
        ))
        .await?;
    assert!(applied);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn get_file_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"file_id":"file_1","file_unique_id":"uniq_1","file_size":128,"file_path":"photos/pic.jpg"}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getFile", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let file = client
        .files()
        .get_file(&GetFileRequest::new("file_1"))
        .await?;
    assert_eq!(file.file_id, "file_1");
    assert_eq!(file.file_path.as_deref(), Some("photos/pic.jpg"));

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn send_photo_upload_multipart_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":100,"date":1710000000,"chat":{"id":1,"type":"private"},"photo":[{"file_id":"file_1","file_unique_id":"uniq_1","width":10,"height":10}]}}"#;
    const CHECKS: [&str; 4] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"photo\"; filename=\"image.jpg\"",
        "binary-photo-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/sendPhoto", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let file = UploadFile::from_bytes("image.jpg", b"binary-photo-data".to_vec())?;
    let request = SendPhotoRequest::for_upload(1_i64);
    let message = client.messages().send_photo_upload(&request, &file).await?;
    assert_eq!(message.message_id.0, 100);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn set_chat_photo_upload_multipart_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":true}"#;
    const CHECKS: [&str; 4] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"photo\"; filename=\"chat-photo.jpg\"",
        "binary-chat-photo-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/setChatPhoto", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let request = SetChatPhotoRequest::new(1_i64);
    let file = UploadFile::from_bytes("chat-photo.jpg", b"binary-chat-photo-data".to_vec())?;
    let ok = client
        .chats()
        .set_chat_photo_upload(&request, &file)
        .await?;
    assert!(ok);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_photo_builder_send_upload_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":102,"date":1710000005,"chat":{"id":1,"type":"private"},"photo":[{"file_id":"file_2","file_unique_id":"uniq_2","width":10,"height":10}]}}"#;
    const CHECKS: [&str; 5] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"caption\"",
        "name=\"photo\"; filename=\"builder-image.jpg\"",
        "binary-builder-photo-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/sendPhoto", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let file = UploadFile::from_bytes("builder-image.jpg", b"binary-builder-photo-data".to_vec())?;
    let message = client
        .app()
        .photo_upload(1_i64)
        .caption("builder upload")
        .send_upload(&file)
        .await?;
    assert_eq!(message.message_id.0, 102);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn app_audio_builder_send_upload_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":103,"date":1710000011,"chat":{"id":1,"type":"private"}}}"#;
    const CHECKS: [&str; 5] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"caption\"",
        "name=\"audio\"; filename=\"builder-audio.mp3\"",
        "binary-builder-audio-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/sendAudio", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let file = UploadFile::from_bytes("builder-audio.mp3", b"binary-builder-audio-data".to_vec())?;
    let message = client
        .app()
        .audio_upload(1_i64)
        .caption("builder audio upload")
        .send_upload(&file)
        .await?;
    assert_eq!(message.message_id.0, 103);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn send_document_upload_with_thumbnail_multipart_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":104,"date":1710000012,"chat":{"id":1,"type":"private"},"document":{"file_id":"doc_1","file_unique_id":"doc_unique_1"}}}"#;
    const CHECKS: [&str; 8] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"thumbnail\"",
        "attach://thumb0",
        "name=\"document\"; filename=\"report.pdf\"",
        "binary-document-data",
        "name=\"thumb0\"; filename=\"report-thumb.jpg\"",
        "binary-thumbnail-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/sendDocument", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let document = UploadFile::from_bytes("report.pdf", b"binary-document-data".to_vec())?;
    let thumbnail = UploadPart::from_bytes(
        "thumb0",
        "report-thumb.jpg",
        b"binary-thumbnail-data".to_vec(),
    )?;
    let mut request = SendDocumentRequest::for_upload(1_i64);
    request.thumbnail = Some(thumbnail.attach_uri());
    let message = client
        .messages()
        .send_document_upload_parts(&request, &document, &[thumbnail])
        .await?;
    assert_eq!(message.message_id.0, 104);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn send_media_group_upload_multipart_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":[{"message_id":104,"date":1710000012,"chat":{"id":1,"type":"private"},"photo":[{"file_id":"photo_1","file_unique_id":"photo_unique_1","width":10,"height":10}]},{"message_id":105,"date":1710000013,"chat":{"id":1,"type":"private"},"video":{"file_id":"video_1","file_unique_id":"video_unique_1","width":640,"height":480,"duration":5}}]}"#;
    const CHECKS: [&str; 8] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"media\"",
        "attach://photo0",
        "attach://video0",
        "name=\"photo0\"; filename=\"album-photo.jpg\"",
        "name=\"video0\"; filename=\"album-video.mp4\"",
        "binary-album-video-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/sendMediaGroup", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let photo_part = UploadPart::from_bytes(
        "photo0",
        "album-photo.jpg",
        b"binary-album-photo-data".to_vec(),
    )?;
    let video_part = UploadPart::from_bytes(
        "video0",
        "album-video.mp4",
        b"binary-album-video-data".to_vec(),
    )?;
    let request = SendMediaGroupRequest::new(
        1_i64,
        vec![
            InputMediaPhoto::new(photo_part.attach_uri())
                .caption("photo item")
                .into(),
            InputMediaVideo::new(video_part.attach_uri()).into(),
        ],
    )?;
    let messages = client
        .messages()
        .send_media_group_upload(&request, &[photo_part, video_part])
        .await?;
    assert_eq!(messages.len(), 2);
    assert_eq!(messages[0].message_id.0, 104);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn advanced_get_available_gifts_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"gifts":[]}}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/getAvailableGifts", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let request = AdvancedGetAvailableGiftsRequest::new();
    let value = client
        .advanced()
        .get_available_gifts::<serde_json::Value>(&request)
        .await?;
    assert!(value.is_object());

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn answer_web_app_query_typed_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"inline_message_id":"inline-msg-1"}}"#;
    const CHECKS: [&str; 3] = [
        "\"web_app_query_id\":\"query-1\"",
        "\"type\":\"article\"",
        "\"id\":\"result-1\"",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/answerWebAppQuery", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let result = InlineQueryResult::new(serde_json::json!({
        "type": "article",
        "id": "result-1",
        "title": "Mini App result",
        "input_message_content": {
            "message_text": "Mini App accepted"
        }
    }))?;
    let request = AdvancedAnswerWebAppQueryRequest::new("query-1", result);
    let sent = client
        .advanced()
        .answer_web_app_query_typed(&request)
        .await?;
    assert_eq!(sent.inline_message_id, "inline-msg-1");

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn answer_inline_query_with_typed_button_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":true}"#;
    const CHECKS: [&str; 5] = [
        "\"inline_query_id\":\"inline-q-1\"",
        "\"type\":\"article\"",
        "\"id\":\"result-inline-1\"",
        "\"button\":{\"text\":\"Open Mini App\"",
        "\"web_app\":{\"url\":\"https://example.com/mini-app\"}",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/answerInlineQuery", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let inline_result =
        InlineQueryResult::article("result-inline-1", "Inline title", "Inline message text")?;
    let request = AnswerInlineQueryRequest::new("inline-q-1", vec![inline_result]).button(
        InlineQueryResultsButton::web_app("Open Mini App", "https://example.com/mini-app"),
    );
    let ok = client.updates().answer_inline_query(&request).await?;
    assert!(ok);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn create_invoice_link_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":"https://t.me/$1234"}"#;
    let (base_url, handle) = spawn_server("/bot123:abc/createInvoiceLink", 200, response)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let request = CreateInvoiceLinkRequest::new(
        "Pro Plan",
        "Monthly subscription",
        "order-1",
        "USD",
        vec![LabeledPrice::new("Pro Plan", 499)],
    )?;
    let link = client.payments().create_invoice_link(&request).await?;
    assert_eq!(link, "https://t.me/$1234");

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn send_sticker_upload_multipart_success() -> Result<(), DynError> {
    let response = r#"{"ok":true,"result":{"message_id":101,"date":1710000001,"chat":{"id":1,"type":"private"}}}"#;
    const CHECKS: [&str; 4] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"chat_id\"",
        "name=\"sticker\"; filename=\"sticker.webp\"",
        "binary-sticker-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/sendSticker", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let request = SendStickerRequest::for_upload(1_i64);
    let file = UploadFile::from_bytes("sticker.webp", b"binary-sticker-data".to_vec())?;
    let message = client
        .stickers()
        .send_sticker_upload(&request, &file)
        .await?;
    assert_eq!(message.message_id.0, 101);

    join_server(handle)?;
    Ok(())
}

#[tokio::test]
async fn upload_sticker_file_upload_multipart_success() -> Result<(), DynError> {
    let response =
        r#"{"ok":true,"result":{"file_id":"sticker_file","file_unique_id":"sticker_unique"}}"#;
    const CHECKS: [&str; 5] = [
        "Content-Type: multipart/form-data; boundary=",
        "name=\"user_id\"",
        "name=\"sticker_format\"",
        "name=\"sticker\"; filename=\"upload-sticker.webp\"",
        "binary-upload-sticker-data",
    ];
    let (base_url, handle) =
        spawn_server_with_checks("/bot123:abc/uploadStickerFile", 200, response, &CHECKS)?;

    let client = Client::builder(base_url)?.bot_token("123:abc")?.build()?;
    let request = UploadStickerFileRequest::new(1.into(), StickerFormat::Static);
    let file = UploadFile::from_bytes(
        "upload-sticker.webp",
        b"binary-upload-sticker-data".to_vec(),
    )?;
    let uploaded = client
        .stickers()
        .upload_sticker_file_upload(&request, &file)
        .await?;
    assert_eq!(uploaded.file_id, "sticker_file");

    join_server(handle)?;
    Ok(())
}
