//! Raw integration coverage for channel web-provider and startup paths.
//!
//! These tests intentionally drive debug/test-support seams with loopback or
//! in-memory inputs so coverage reaches production branches without real
//! channel credentials or external inference providers.

use std::time::Duration;

use openhuman_core::openhuman::channels::start_channels;
use openhuman_core::openhuman::channels::test_support::{
    lock_agent_handler, run_dispatch_harness, DispatchHarnessOptions, TestMemoryEntry,
};
use openhuman_core::openhuman::channels::web::{
    all_web_channel_controller_schemas, all_web_channel_registered_controllers, channel_web_cancel,
    channel_web_chat, schemas, start_chat, subscribe_web_channel_events,
    test_support as web_test_support, ChatRequestMetadata,
};
use openhuman_core::openhuman::config::Config;
use tempfile::tempdir;
use tokio::time::timeout;

fn isolated_config() -> (tempfile::TempDir, Config) {
    let tmp = tempdir().expect("tempdir");
    let workspace = tmp.path().join("workspace");
    std::fs::create_dir_all(&workspace).expect("workspace");

    let mut config = Config::default();
    config.workspace_dir = workspace;
    config.config_path = tmp.path().join("config.toml");
    config.api_key = None;
    config.api_url = Some("http://127.0.0.1:9".to_string());
    config.inference_url = None;
    config.memory.auto_save = false;
    config.browser.enabled = false;
    (tmp, config)
}

#[test]
fn web_error_debug_export_covers_provider_config_and_retry_branches() {
    let rate_limited = web_test_support::classify_error_for_test(
        r#"openrouter API error (429 Too Many Requests): {"retry_after": 1.2}"#,
    );
    assert_eq!(rate_limited.error_type, "rate_limited");
    assert_eq!(rate_limited.source, "provider");
    assert_eq!(rate_limited.retry_after_ms, Some(2_000));
    assert_eq!(rate_limited.provider.as_deref(), Some("openrouter"));
    assert!(rate_limited.retryable);

    let action_budget = web_test_support::classify_error_for_test(
        "Rate limit exceeded: action budget exhausted while running web_fetch",
    );
    assert_eq!(action_budget.error_type, "action_budget_exceeded");
    assert_eq!(action_budget.source, "openhuman_budget");
    assert!(action_budget.provider.is_none());

    let non_retryable = web_test_support::classify_error_for_test(
        "zai API error (429 Too Many Requests): code=1311 insufficient balance",
    );
    assert_eq!(non_retryable.error_type, "rate_limited");
    assert!(!non_retryable.retryable);
    assert_eq!(non_retryable.provider.as_deref(), Some("zai"));

    let exhausted = web_test_support::classify_error_for_test(
        "All providers/models failed. Attempts: openhuman API error (503 Service Unavailable)",
    );
    assert_eq!(exhausted.error_type, "provider_error");
    assert_eq!(exhausted.fallback_available, Some(false));

    let detail = web_test_support::extracted_provider_detail_for_test(
        r#"custom_openai API error (404 Not Found): {"error":{"message":"Model `missing-model` does not exist"}}"#,
    )
    .expect("provider detail");
    assert!(detail.contains("missing-model"));

    assert_eq!(
        web_test_support::retry_after_secs_for_test("retry-after: 0"),
        Some(0)
    );
    assert!(web_test_support::is_non_retryable_rate_limit_for_test(
        "package not active"
    ));
}

/// Serialize tests that run a web chat task whose outcome depends on the
/// process-global forced-error seam (`set_forced_run_chat_task_error_for_test`).
/// Without this, the forced error one test installs leaks into another's chat
/// run, crossing their expected error types (e.g. `rate_limited` vs
/// `cancelled`) under cargo-llvm-cov's multi-threaded execution.
fn web_chat_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
    LOCK.get_or_init(|| std::sync::Mutex::new(()))
        .lock()
        .unwrap_or_else(|e| e.into_inner())
}

#[tokio::test]
async fn web_controllers_validate_inputs_and_emit_structured_forced_errors() {
    let _chat_lock = web_chat_lock();
    let controller_schemas = all_web_channel_controller_schemas();
    assert_eq!(controller_schemas.len(), 4);
    assert!(controller_schemas
        .iter()
        .any(|schema| schema.function == "web_chat"));
    assert!(controller_schemas
        .iter()
        .any(|schema| schema.function == "web_cancel"));
    assert!(controller_schemas
        .iter()
        .any(|schema| schema.function == "web_queue_status"));
    assert!(controller_schemas
        .iter()
        .any(|schema| schema.function == "web_queue_clear"));
    assert_eq!(all_web_channel_registered_controllers().len(), 4);
    assert_eq!(schemas("missing").function, "unknown");

    let err = channel_web_chat(
        "client",
        "thread",
        "   ",
        None,
        None,
        None,
        None,
        None,
        ChatRequestMetadata::default(),
    )
    .await
    .expect_err("blank messages are rejected");
    assert!(err.contains("message is required"));

    let cancel = channel_web_cancel("client", "missing-thread")
        .await
        .expect("cancel without in-flight request is ok")
        .into_cli_compatible_json()
        .expect("json");
    assert_eq!(cancel["result"]["cancelled"], false);

    web_test_support::set_forced_run_chat_task_error_for_test(Some(
        "openrouter API error (429 Too Many Requests): Retry-After: 7",
    ))
    .await;

    let mut rx = subscribe_web_channel_events();
    let accepted = channel_web_chat(
        "client-a",
        "thread-a",
        "Summarize this safely.",
        Some(" hint:reasoning ".to_string()),
        Some(0.2),
        None,
        Some("zh-CN".to_string()),
        None,
        ChatRequestMetadata::default(),
    )
    .await
    .expect("chat request accepted")
    .into_cli_compatible_json()
    .expect("json");
    let request_id = accepted["result"]["request_id"]
        .as_str()
        .expect("request id")
        .to_string();

    let event = timeout(Duration::from_secs(10), async {
        loop {
            let event = rx.recv().await.expect("web channel event");
            if event.request_id == request_id && event.event == "chat_error" {
                break event;
            }
        }
    })
    .await
    .expect("chat_error event");

    assert_eq!(event.error_type.as_deref(), Some("rate_limited"));
    assert_eq!(event.error_source.as_deref(), Some("provider"));
    assert_eq!(event.error_retry_after_ms, Some(7_000));
    assert_eq!(event.error_provider.as_deref(), Some("openrouter"));
    web_test_support::set_forced_run_chat_task_error_for_test(None).await;
}

#[tokio::test]
async fn web_chat_cancel_aborts_in_flight_thread_without_real_provider() {
    let _chat_lock = web_chat_lock();
    // Clear any forced error a sibling test may have leaked before this chat
    // runs, so the cancellation path produces a real `cancelled` error rather
    // than inheriting a stale forced `rate_limited`/`inference` one.
    web_test_support::set_forced_run_chat_task_error_for_test(None).await;
    let mut rx = subscribe_web_channel_events();
    let request_id = start_chat(
        "cancel-client",
        "cancel-thread",
        "This request should be cancelled before inference completes.",
        None,
        None,
        None,
        None,
        None,
        ChatRequestMetadata::default(),
    )
    .await
    .expect("start chat");

    let cancel = channel_web_cancel("cancel-client", "cancel-thread")
        .await
        .expect("cancel")
        .into_cli_compatible_json()
        .expect("json");
    assert_eq!(cancel["result"]["cancelled"], true);
    assert_eq!(cancel["result"]["request_id"], request_id);

    let event = timeout(Duration::from_secs(10), async {
        loop {
            let event = rx.recv().await.expect("web channel event");
            if event.request_id == request_id && event.event == "chat_error" {
                break event;
            }
        }
    })
    .await
    .expect("cancel event");

    assert_eq!(event.error_type.as_deref(), Some("cancelled"));
    assert_eq!(event.message.as_deref(), Some("Cancelled"));
}

#[tokio::test]
async fn startup_no_channels_initializes_runtime_and_exits_cleanly() {
    // `start_channels` calls `register_agent_handlers()`, which re-registers the
    // real `AGENT_RUN_TURN_METHOD` handler on the process-global native registry
    // (latest-wins). The dispatch harness installs a *mock* handler on that same
    // slot. Without this shared guard, the two race inside the test binary and
    // the real handler can clobber the mock mid-run, flaking
    // `dispatch_harness_covers_streaming_history_timeout_and_memory_paths` on
    // `handler_had_progress`. Hold the guard across the whole startup call.
    let _agent_handler_guard = lock_agent_handler().await;
    let (_tmp, config) = isolated_config();
    timeout(Duration::from_secs(20), start_channels(config))
        .await
        .expect("startup should not hang")
        .expect("no-channel startup should be ok");
}

#[tokio::test]
async fn dispatch_harness_covers_streaming_history_timeout_and_memory_paths() {
    let streaming = run_dispatch_harness(DispatchHarnessOptions {
        channel_name: "web".to_string(),
        content: "please inspect the remembered project".to_string(),
        streaming: true,
        supports_reactions: true,
        seed_history_len: 3,
        memory_entries: vec![TestMemoryEntry {
            key: "project".to_string(),
            content: "The project uses a loopback mock.".to_string(),
            score: Some(0.95),
        }],
        response_text: Some("streamed dispatch response".to_string()),
        ..DispatchHarnessOptions::default()
    })
    .await;
    assert!(streaming.handler_had_progress);
    assert!(streaming.start_typing_calls >= 1);
    assert!(streaming.stop_typing_calls >= 1);
    assert!(streaming
        .sends
        .iter()
        .any(|send| send.kind == "finalize_draft" || send.kind == "send"));
    assert!(streaming.handler_history_text.contains("loopback mock"));
    assert!(streaming.retained_history_len >= 1);

    let failed = run_dispatch_harness(DispatchHarnessOptions {
        channel_name: "telegram".to_string(),
        content: "force a handler failure".to_string(),
        thread_ts: Some("thread-42".to_string()),
        handler_error: Some("handler failed deliberately".to_string()),
        timeout_secs: 1,
        ..DispatchHarnessOptions::default()
    })
    .await;
    assert!(failed
        .sends
        .iter()
        .any(|send| send.content.contains("handler failed deliberately")));
}
