//! `secrets::` SDK bridge integration tests — runs a real Rhai engine
//! against an in-memory `SecretsService` impl. Mirrors `sdk_kv.rs`: the
//! engine runs under `spawn_blocking` so the bridge's `block_on` has a
//! reachable runtime.
//!
//! This exercises the Rhai⇄JSON plumbing + the static `secrets` module
//! (set/get/delete/list, the missing→() contract, and the
//! String/Map/Array type round-trip). Encryption + authz + the
//! cross-app boundary are unit-tested at the service layer in
//! `manager-core::secrets_service`.

use std::collections::BTreeMap;
use std::sync::Arc;

use async_trait::async_trait;
use picloud_executor_core::{Engine, ExecRequest, InvocationType, Limits};
use picloud_shared::{
    AppId, ExecutionId, NoopDeadLetterService, NoopDocsService, NoopEventEmitter, NoopHttpService,
    NoopKvService, NoopModuleSource, RequestId, ScriptId, ScriptSandbox, SdkCallCx, SecretsError,
    SecretsListPage, SecretsService, Services,
};
use serde_json::{json, Value};
use tokio::sync::Mutex;

/// In-memory secrets store keyed by `(app_id, name)`. Stores the JSON
/// value directly — the bridge test only cares about the Rhai plumbing,
/// not the at-rest encryption (which the service layer owns).
#[derive(Default)]
struct InMemorySecrets {
    data: Mutex<BTreeMap<(AppId, String), Value>>,
}

#[async_trait]
impl SecretsService for InMemorySecrets {
    async fn get(&self, cx: &SdkCallCx, name: &str) -> Result<Option<Value>, SecretsError> {
        picloud_shared::validate_secret_name(name)?;
        Ok(self
            .data
            .lock()
            .await
            .get(&(cx.app_id, name.to_string()))
            .cloned())
    }

    async fn set(&self, cx: &SdkCallCx, name: &str, value: Value) -> Result<(), SecretsError> {
        picloud_shared::validate_secret_name(name)?;
        self.data
            .lock()
            .await
            .insert((cx.app_id, name.to_string()), value);
        Ok(())
    }

    async fn delete(&self, cx: &SdkCallCx, name: &str) -> Result<bool, SecretsError> {
        picloud_shared::validate_secret_name(name)?;
        Ok(self
            .data
            .lock()
            .await
            .remove(&(cx.app_id, name.to_string()))
            .is_some())
    }

    async fn list(
        &self,
        cx: &SdkCallCx,
        cursor: Option<&str>,
        limit: u32,
    ) -> Result<SecretsListPage, SecretsError> {
        let data = self.data.lock().await;
        let mut names: Vec<String> = data
            .iter()
            .filter(|((a, _), _)| *a == cx.app_id)
            .map(|((_, n), _)| n.clone())
            .filter(|n| cursor.is_none_or(|c| n.as_str() > c))
            .collect();
        names.sort();
        let take = if limit == 0 {
            usize::MAX
        } else {
            limit as usize
        };
        let next_cursor = if names.len() > take {
            names.truncate(take);
            names.last().cloned()
        } else {
            None
        };
        Ok(SecretsListPage { names, next_cursor })
    }
}

fn make_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(picloud_shared::NoopFilesService),
        Arc::new(picloud_shared::NoopPubsubService),
        Arc::new(InMemorySecrets::default()),
        Arc::new(picloud_shared::NoopEmailService),
        Arc::new(picloud_shared::NoopUsersService),
        Arc::new(picloud_shared::NoopQueueService),
        Arc::new(picloud_shared::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: "secrets-test".into(),
        invocation_type: InvocationType::Http,
        path: "/secrets-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,
    }
}

async fn run_script(engine: Arc<Engine>, src: &str, req: ExecRequest) -> Value {
    let src = src.to_string();
    tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .expect("spawn_blocking should not panic")
        .expect("script execution should succeed")
        .body
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn set_then_get_string_round_trips() {
    let engine = make_engine();
    let src = r#"
        secrets::set("stripe_key", "sk_live_xxx");
        secrets::get("stripe_key")
    "#;
    let body = run_script(engine, src, baseline_request(AppId::new())).await;
    // A String comes back a String, not a JSON-quoted "\"sk_live_xxx\"".
    assert_eq!(body, json!("sk_live_xxx"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn set_then_get_map_round_trips() {
    let engine = make_engine();
    let src = r#"
        secrets::set("oauth", #{ client_id: "abc", client_secret: "xyz" });
        secrets::get("oauth")
    "#;
    let body = run_script(engine, src, baseline_request(AppId::new())).await;
    assert_eq!(body, json!({ "client_id": "abc", "client_secret": "xyz" }));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_missing_returns_unit() {
    let engine = make_engine();
    let src = r#"
        let v = secrets::get("nope");
        #{ is_unit: type_of(v) == "()" }
    "#;
    let body = run_script(engine, src, baseline_request(AppId::new())).await;
    assert_eq!(body, json!({ "is_unit": true }));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn delete_returns_was_present() {
    let engine = make_engine();
    let src = r#"
        secrets::set("k", "v");
        let first = secrets::delete("k");
        let second = secrets::delete("k");
        #{ first: first, second: second }
    "#;
    let body = run_script(engine, src, baseline_request(AppId::new())).await;
    assert_eq!(body, json!({ "first": true, "second": false }));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_returns_names_and_cursor() {
    let engine = make_engine();
    let src = r#"
        secrets::set("a", 1);
        secrets::set("b", 2);
        secrets::set("c", 3);
        let page = secrets::list(#{ cursor: (), limit: 2 });
        page
    "#;
    let body = run_script(engine, src, baseline_request(AppId::new())).await;
    assert_eq!(body["names"], json!(["a", "b"]));
    assert_eq!(body["next_cursor"], json!("b"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn empty_name_throws() {
    let engine = make_engine();
    let src = r#" secrets::set("", "v"); #{ ok: true } "#;
    let app = AppId::new();
    let out = tokio::task::spawn_blocking(move || engine.execute(src, baseline_request(app)))
        .await
        .expect("spawn_blocking");
    assert!(out.is_err(), "empty secret name must throw");
}
