//! `retry::*` SDK bridge integration tests — runs real Rhai scripts
//! that build a Policy, wrap a closure, and verify the retry / on_codes
//! behavior end-to-end. Sleep delays are kept to 1ms so the suite runs
//! fast.

use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};

use picloud_executor_core::{Engine, ExecRequest, InvocationType, Limits};
use picloud_shared::{
    AppId, ExecutionId, NoopDeadLetterService, NoopDocsService, NoopEmailService, NoopEventEmitter,
    NoopFilesService, NoopHttpService, NoopInvokeService, NoopKvService, NoopModuleSource,
    NoopPubsubService, NoopQueueService, NoopSecretsService, NoopUsersService, RequestId, ScriptId,
    ScriptSandbox, Services,
};
use serde_json::Value;

fn build_engine() -> Arc<Engine> {
    let services = Services::new(
        Arc::new(NoopKvService),
        Arc::new(NoopDocsService),
        Arc::new(NoopDeadLetterService),
        Arc::new(NoopEventEmitter),
        Arc::new(NoopModuleSource),
        Arc::new(NoopHttpService),
        Arc::new(NoopFilesService),
        Arc::new(NoopPubsubService),
        Arc::new(NoopSecretsService),
        Arc::new(NoopEmailService),
        Arc::new(NoopUsersService),
        Arc::new(NoopQueueService),
        Arc::new(NoopInvokeService),
        Arc::new(picloud_shared::NoopVarsService),
    );
    Arc::new(Engine::new(Limits::default(), services))
}

fn baseline_request(app_id: AppId) -> ExecRequest {
    let execution_id = ExecutionId::new();
    ExecRequest {
        execution_id,
        request_id: RequestId::new(),
        script_id: ScriptId::new(),
        script_name: "retry-test".into(),
        invocation_type: InvocationType::Http,
        path: "/retry-test".into(),
        method: String::new(),
        headers: BTreeMap::new(),
        body: Value::Null,
        params: BTreeMap::new(),
        query: BTreeMap::new(),
        rest: String::new(),
        sandbox_overrides: ScriptSandbox::default(),
        app_id,
        script_owner: None,
        principal: None,
        trigger_depth: 0,
        root_execution_id: execution_id,
        is_dead_letter_handler: false,
        event: None,
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retry_run_succeeds_first_try() {
    let engine = build_engine();
    let src = r"
        let p = retry::policy(#{ max_attempts: 3, base_ms: 1 });
        let v = retry::run(p, || 42);
        #{ statusCode: 200, body: v }
    "
    .to_string();
    let req = baseline_request(AppId::new());
    let resp = tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(resp.body, serde_json::json!(42));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retry_run_surfaces_after_max_attempts() {
    let engine = build_engine();
    let src = r#"
        let p = retry::policy(#{ max_attempts: 3, base_ms: 1, jitter_pct: 0 });
        retry::run(p, || { throw "boom" });
    "#
    .to_string();
    let req = baseline_request(AppId::new());
    let res = tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .unwrap();
    let err = res.unwrap_err().to_string();
    assert!(err.contains("boom"), "expected 'boom' in error, got: {err}");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retry_on_codes_filters_unmatched_errors() {
    let engine = build_engine();
    // Throw a message NOT in the filter; retry::run must surface
    // immediately (no retries).
    let src = r#"
        let p = retry::policy(#{ max_attempts: 5, base_ms: 1, jitter_pct: 0 });
        let p2 = retry::on_codes(p, ["http: 503"]);
        retry::run(p2, || { throw "other failure" });
    "#
    .to_string();
    let req = baseline_request(AppId::new());
    let res = tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .unwrap();
    let err = res.unwrap_err().to_string();
    assert!(
        err.contains("other failure"),
        "expected immediate surface, got: {err}"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retry_policy_clamps_visible_at_script_layer() {
    // jitter_pct = 999 should clamp to 100 silently; the policy is then
    // usable.
    let engine = build_engine();
    let src = r#"
        let p = retry::policy(#{
            max_attempts: 30, base_ms: 100, jitter_pct: 999, backoff: "constant"
        });
        let v = retry::run(p, || 1);
        #{ statusCode: 200, body: v }
    "#
    .to_string();
    let req = baseline_request(AppId::new());
    let resp = tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(resp.body, serde_json::json!(1));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retry_policy_rejects_bogus_backoff() {
    let engine = build_engine();

    // Pin the REASON. A bare `is_err()` would pass if `retry::policy` were not
    // registered at all (ErrorFunctionNotFound), or on a typo in the script
    // source, or if EVERY policy were rejected — none of which is "an invalid
    // backoff is rejected". The message must name `backoff`.
    let src = r#"retry::policy(#{ backoff: "bogus" });"#.to_string();
    let req = baseline_request(AppId::new());
    let err = tokio::task::spawn_blocking({
        let engine = engine.clone();
        move || engine.execute(&src, req)
    })
    .await
    .unwrap()
    .unwrap_err()
    .to_string();
    assert!(
        err.to_lowercase().contains("backoff"),
        "the rejection must be about the backoff value specifically, got: {err}"
    );

    // Control: the SAME call with a VALID backoff succeeds — proving `retry::policy`
    // is registered and does not reject everything, so the failure above is
    // specific to the bad value.
    let ok_src = r#"
        let p = retry::policy(#{ backoff: "constant", max_attempts: 2, base_ms: 1 });
        retry::run(p, || 7)
    "#
    .to_string();
    let req = baseline_request(AppId::new());
    let resp = tokio::task::spawn_blocking(move || engine.execute(&ok_src, req))
        .await
        .unwrap()
        .expect("a valid backoff must be accepted");
    assert_eq!(resp.body, serde_json::json!(7));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retry_run_eventually_succeeds() {
    // Use an in-engine counter to simulate "fails 2 times then succeeds".
    let engine = build_engine();
    let counter = Arc::new(Mutex::new(0_i64));
    let _ = counter; // counter via Rhai-side `let` instead

    let src = r#"
        let attempts = 0;
        let p = retry::policy(#{ max_attempts: 5, base_ms: 1, jitter_pct: 0 });
        let v = retry::run(p, || {
            attempts += 1;
            if attempts < 3 { throw "transient" } else { attempts }
        });
        #{ statusCode: 200, body: v }
    "#
    .to_string();
    let req = baseline_request(AppId::new());
    let resp = tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(resp.body, serde_json::json!(3));
}
