//! `email::` SDK bridge integration tests — runs a real Rhai engine
//! against a recording `EmailService`. Verifies the Rhai map → DTO
//! plumbing (address coercion, the text-only vs multipart split). The
//! SMTP transport, validation, and authz are unit-tested at the service
//! layer in `manager-core::email_service`.

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

use async_trait::async_trait;
use picloud_executor_core::{Engine, ExecRequest, InvocationType, Limits};
use picloud_shared::{
    AppId, EmailError, EmailService, ExecutionId, NoopDeadLetterService, NoopDocsService,
    NoopEventEmitter, NoopHttpService, NoopKvService, NoopModuleSource, OutboundEmail, RequestId,
    ScriptId, ScriptSandbox, SdkCallCx, Services, TriggerEvent,
};
use serde_json::{json, Value};

#[derive(Default)]
struct RecordingEmail {
    sent: Mutex<Vec<OutboundEmail>>,
}

#[async_trait]
impl EmailService for RecordingEmail {
    async fn send(&self, _cx: &SdkCallCx, email: OutboundEmail) -> Result<(), EmailError> {
        self.sent.lock().unwrap().push(email);
        Ok(())
    }
}

fn engine_with(rec: Arc<RecordingEmail>) -> 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(picloud_shared::NoopSecretsService),
        rec,
        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: "email-test".into(),
        invocation_type: InvocationType::Http,
        path: "/email-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(engine: Arc<Engine>, src: &str) -> Result<(), ()> {
    let src = src.to_string();
    let app = AppId::new();
    tokio::task::spawn_blocking(move || engine.execute(&src, baseline_request(app)))
        .await
        .expect("spawn_blocking")
        .map(|_| ())
        .map_err(|_| ())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn send_parses_single_recipient_text() {
    let rec = Arc::new(RecordingEmail::default());
    let engine = engine_with(rec.clone());
    run(
        engine,
        r#"
        email::send(#{
            to: "alice@example.com",
            from: "alerts@myapp.com",
            subject: "Build complete",
            text: "done"
        });
        #{ ok: true }
    "#,
    )
    .await
    .unwrap();
    let g = rec.sent.lock().unwrap();
    let e = g.last().unwrap();
    assert_eq!(e.to, vec!["alice@example.com".to_string()]);
    assert_eq!(e.from, "alerts@myapp.com");
    assert_eq!(e.subject, "Build complete");
    assert_eq!(e.text.as_deref(), Some("done"));
    // email::send forces text-only even if html were present.
    assert!(e.html.is_none());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn send_html_carries_both_parts_and_lists() {
    let rec = Arc::new(RecordingEmail::default());
    let engine = engine_with(rec.clone());
    run(
        engine,
        r#"
        email::send_html(#{
            to: ["alice@x.com", "bob@y.com"],
            cc: ["dave@z.com"],
            bcc: ["audit@myapp.com"],
            from: "alerts@myapp.com",
            reply_to: "support@myapp.com",
            subject: "hi",
            text: "plain",
            html: "<p>rich</p>"
        });
        #{ ok: true }
    "#,
    )
    .await
    .unwrap();
    let g = rec.sent.lock().unwrap();
    let e = g.last().unwrap();
    assert_eq!(
        e.to,
        vec!["alice@x.com".to_string(), "bob@y.com".to_string()]
    );
    assert_eq!(e.cc, vec!["dave@z.com".to_string()]);
    assert_eq!(e.bcc, vec!["audit@myapp.com".to_string()]);
    assert_eq!(e.reply_to.as_deref(), Some("support@myapp.com"));
    assert_eq!(e.text.as_deref(), Some("plain"));
    assert_eq!(e.html.as_deref(), Some("<p>rich</p>"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn inbound_email_event_visible_to_handler() {
    // A handler invoked by an email:receive trigger sees the normalized
    // message at ctx.event.email (built by the engine's ctx renderer).
    let rec = Arc::new(RecordingEmail::default());
    let engine = engine_with(rec);
    let mut req = baseline_request(AppId::new());
    req.event = Some(TriggerEvent::Email {
        from: "sender@external.com".into(),
        to: vec!["alice@myapp.com".into()],
        cc: vec!["bob@myapp.com".into()],
        subject: "Re: question".into(),
        text: Some("hello".into()),
        html: None,
        received_at: chrono::DateTime::parse_from_rfc3339("2026-08-15T12:00:00Z")
            .unwrap()
            .with_timezone(&chrono::Utc),
        message_id: Some("<abc@external.com>".into()),
    });
    let src = r#"
        let e = ctx.event;
        #{
            source: e.source,
            op: e.op,
            from: e.email.from,
            to0: e.email.to[0],
            cc0: e.email.cc[0],
            subject: e.email.subject,
            text: e.email.text,
            html_is_unit: type_of(e.email.html) == "()",
            message_id: e.email.message_id
        }
    "#;
    let src = src.to_string();
    let body = tokio::task::spawn_blocking(move || engine.execute(&src, req))
        .await
        .unwrap()
        .unwrap()
        .body;
    assert_eq!(body["source"], json!("email"));
    assert_eq!(body["op"], json!("receive"));
    assert_eq!(body["from"], json!("sender@external.com"));
    assert_eq!(body["to0"], json!("alice@myapp.com"));
    assert_eq!(body["cc0"], json!("bob@myapp.com"));
    assert_eq!(body["subject"], json!("Re: question"));
    assert_eq!(body["text"], json!("hello"));
    assert_eq!(body["html_is_unit"], json!(true));
    assert_eq!(body["message_id"], json!("<abc@external.com>"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn send_html_without_html_throws() {
    let rec = Arc::new(RecordingEmail::default());
    let engine = engine_with(rec.clone());
    let res = run(
        engine,
        r#"
        email::send_html(#{ to: "a@b.com", from: "c@d.com", subject: "x", text: "y" });
        #{ ok: true }
    "#,
    )
    .await;
    assert!(res.is_err(), "send_html without html must throw");
    assert!(rec.sent.lock().unwrap().is_empty());
}
