//! Integration tests for the v1.1.0 stdlib utility modules.
//!
//! These exist alongside `sdk_contract.rs` rather than inside it
//! because the stateless utilities aren't part of the same versioned
//! SDK contract surface — `sdk_contract.rs` covers things that bump
//! `SDK_VERSION` when they change; stdlib additions don't.

use std::collections::BTreeMap;

use picloud_executor_core::{Engine, ExecError, ExecRequest, InvocationType, Limits};
use picloud_shared::{AppId, ExecutionId, RequestId, ScriptId, ScriptSandbox, Services};
use serde_json::{json, Value};

// ----------------------------------------------------------------------------
// Test harness — duplicated from sdk_contract.rs (each integration test
// crate has its own; there is no tests/common/).
// ----------------------------------------------------------------------------

fn engine() -> Engine {
    Engine::new(Limits::default(), Services::default())
}

fn baseline_request() -> ExecRequest {
    let execution_id = ExecutionId::new();
    ExecRequest {
        execution_id,
        request_id: RequestId::new(),
        script_id: ScriptId::new(),
        script_name: "stdlib".into(),
        invocation_type: InvocationType::Http,
        path: "/stdlib-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: AppId::new(),
        script_owner: None,
        principal: None,
        trigger_depth: 0,
        root_execution_id: execution_id,
        is_dead_letter_handler: false,
        event: None,
    }
}

fn run(source: &str) -> Value {
    engine()
        .execute(source, baseline_request())
        .expect("stdlib test should execute cleanly")
        .body
}

fn run_err(source: &str) -> ExecError {
    engine()
        .execute(source, baseline_request())
        .expect_err("stdlib test expected to throw")
}

fn assert_runtime_err(err: ExecError, needle: &str) {
    match err {
        ExecError::Runtime(msg) => assert!(
            msg.contains(needle),
            "runtime error did not contain `{needle}`: {msg}"
        ),
        other => panic!("expected Runtime error containing `{needle}`, got {other:?}"),
    }
}

// ============================================================================
// regex
// ============================================================================

#[test]
fn regex_is_match_true_and_false() {
    assert_eq!(run(r#"regex::is_match("^h", "hello")"#), json!(true));
    assert_eq!(run(r#"regex::is_match("^x", "hello")"#), json!(false));
}

#[test]
fn regex_find_returns_first_match() {
    assert_eq!(run(r#"regex::find("\\d+", "abc 42 def 99")"#), json!("42"));
}

#[test]
fn regex_find_returns_unit_when_no_match() {
    // () serializes to JSON null via dynamic_to_json.
    assert_eq!(run(r#"regex::find("\\d+", "abc")"#), Value::Null);
}

#[test]
fn regex_find_all_returns_array() {
    assert_eq!(
        run(r#"regex::find_all("\\d+", "a1 b22 c333")"#),
        json!(["1", "22", "333"])
    );
}

#[test]
fn regex_replace_first_only() {
    assert_eq!(
        run(r#"regex::replace("a", "banana", "X")"#),
        json!("bXnana")
    );
}

#[test]
fn regex_replace_all() {
    assert_eq!(
        run(r#"regex::replace_all("a", "banana", "X")"#),
        json!("bXnXnX")
    );
}

#[test]
fn regex_split() {
    assert_eq!(
        run(r#"regex::split(",\\s*", "a, b,c,  d")"#),
        json!(["a", "b", "c", "d"])
    );
}

#[test]
fn regex_captures_extracts_groups() {
    assert_eq!(
        run(r#"regex::captures("(\\d+)-(\\w+)", "42-abc")"#),
        json!(["42-abc", "42", "abc"])
    );
}

#[test]
fn regex_captures_returns_unit_when_no_match() {
    assert_eq!(run(r#"regex::captures("(\\d+)", "abc")"#), Value::Null);
}

#[test]
fn regex_invalid_pattern_throws() {
    assert_runtime_err(run_err(r#"regex::is_match("(", "x")"#), "invalid regex");
}

// ============================================================================
// random
// ============================================================================

#[test]
fn random_int_within_range() {
    // Run a few times to exercise the bounds — each call is independent.
    let body = run(r"
        let n = random::int(10, 20);
        n >= 10 && n <= 20
    ");
    assert_eq!(body, json!(true));
}

#[test]
fn random_int_throws_when_min_greater_than_max() {
    assert_runtime_err(run_err("random::int(20, 10)"), "min");
}

#[test]
fn random_float_in_unit_interval() {
    let body = run(r"
        let f = random::float();
        f >= 0.0 && f < 1.0
    ");
    assert_eq!(body, json!(true));
}

#[test]
fn random_bytes_returns_blob_of_correct_length() {
    assert_eq!(run("random::bytes(16).len()"), json!(16));
}

#[test]
fn random_bytes_rejects_negative() {
    assert_runtime_err(run_err("random::bytes(-1)"), "random::bytes");
}

#[test]
fn random_bytes_rejects_oversize() {
    assert_runtime_err(run_err("random::bytes(70000)"), "random::bytes");
}

#[test]
fn random_string_produces_alphanumeric_of_correct_length() {
    let body = run(r#"
        let s = random::string(32);
        s.len == 32 && regex::is_match("^[A-Za-z0-9]+$", s)
    "#);
    assert_eq!(body, json!(true));
}

#[test]
fn random_uuid_has_canonical_format() {
    let body = run(
        r#"regex::is_match("^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", random::uuid())"#,
    );
    assert_eq!(body, json!(true));
}

// ============================================================================
// time
// ============================================================================

#[test]
fn time_now_ms_is_positive() {
    let body = run("time::now_ms() > 0");
    assert_eq!(body, json!(true));
}

#[test]
fn time_now_string_looks_like_iso() {
    let body = run(r#"regex::is_match("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", time::now())"#);
    assert_eq!(body, json!(true));
}

#[test]
fn time_parse_format_round_trip() {
    let body = run(r"
        let ms = 1700000000000;
        time::parse(time::format(ms)) == ms
    ");
    assert_eq!(body, json!(true));
}

#[test]
fn time_add_seconds() {
    assert_eq!(run("time::add_seconds(0, 60)"), json!(60_000));
    assert_eq!(run("time::add_seconds(1000, -1)"), json!(0));
}

#[test]
fn time_diff_seconds_truncates() {
    assert_eq!(run("time::diff_seconds(0, 65_500)"), json!(65));
}

#[test]
fn time_parse_rejects_garbage() {
    assert_runtime_err(run_err(r#"time::parse("nonsense")"#), "time::parse");
}

// ============================================================================
// json
// ============================================================================

#[test]
fn json_parse_then_stringify_round_trip() {
    let body = run(r#"
        let src = `{"a":1,"b":"x"}`;
        json::stringify(json::parse(src)) == src
    "#);
    assert_eq!(body, json!(true));
}

#[test]
fn json_stringify_compact() {
    assert_eq!(run(r"json::stringify(#{ a: 1 })"), json!(r#"{"a":1}"#));
}

#[test]
fn json_stringify_pretty_has_newlines() {
    let body = run(r#"json::stringify_pretty(#{ a: 1 }).contains("\n")"#);
    assert_eq!(body, json!(true));
}

#[test]
fn json_parse_invalid_throws() {
    assert_runtime_err(run_err(r#"json::parse("not json")"#), "json::parse");
}

// ============================================================================
// base64
// ============================================================================

#[test]
fn base64_encode_string() {
    assert_eq!(run(r#"base64::encode("hi")"#), json!("aGk="));
}

#[test]
fn base64_decode_then_re_encode_round_trip() {
    assert_eq!(
        run(r#"base64::encode(base64::decode("aGVsbG8="))"#),
        json!("aGVsbG8=")
    );
}

#[test]
fn base64_encode_url_has_no_padding() {
    let body = run(r#"
        let s = base64::encode_url("hello world!?");
        !s.contains("=") && !s.contains("+") && !s.contains("/")
    "#);
    assert_eq!(body, json!(true));
}

#[test]
fn base64_decode_url_round_trip() {
    assert_eq!(
        run(r#"base64::encode_url(base64::decode_url("aGVsbG8"))"#),
        json!("aGVsbG8")
    );
}

#[test]
fn base64_decode_invalid_throws() {
    assert_runtime_err(run_err(r#"base64::decode("!!!")"#), "base64::decode");
}

// ============================================================================
// hex
// ============================================================================

#[test]
fn hex_encode_produces_lowercase() {
    assert_eq!(run(r#"hex::encode("Z")"#), json!("5a"));
}

#[test]
fn hex_decode_then_re_encode_round_trip() {
    // mixed-case input → lowercase output proves both case-insensitive
    // decode and lowercase encode.
    assert_eq!(
        run(r#"hex::encode(hex::decode("DeAdBeEf"))"#),
        json!("deadbeef")
    );
}

#[test]
fn hex_decode_returns_correct_length() {
    assert_eq!(run(r#"hex::decode("deadbeef").len()"#), json!(4));
}

#[test]
fn hex_decode_invalid_throws() {
    assert_runtime_err(run_err(r#"hex::decode("xyz")"#), "hex::decode");
}

// ============================================================================
// url
// ============================================================================

#[test]
fn url_encode_basic() {
    assert_eq!(run(r#"url::encode("hello world")"#), json!("hello%20world"));
}

#[test]
fn url_encode_preserves_unreserved() {
    assert_eq!(
        run(r#"url::encode("abcXYZ123-_.~")"#),
        json!("abcXYZ123-_.~")
    );
}

#[test]
fn url_decode_round_trip() {
    assert_eq!(
        run(r#"url::decode(url::encode("hello world!?"))"#),
        json!("hello world!?")
    );
}

#[test]
fn url_encode_query_basic() {
    // Map keys come out alphabetically (Rhai's Map is a BTreeMap).
    assert_eq!(
        run(r#"url::encode_query(#{ a: "1", b: "x y" })"#),
        json!("a=1&b=x%20y")
    );
}

#[test]
fn url_encode_query_coerces_non_strings() {
    // Numbers and bools shouldn't throw; they coerce via to_string().
    let body = run(r"url::encode_query(#{ n: 42, b: true })");
    // Order is alphabetical: b before n.
    assert_eq!(body, json!("b=true&n=42"));
}

#[test]
fn url_decode_rejects_invalid_utf8() {
    assert_runtime_err(run_err(r#"url::decode("%FF%FE%80")"#), "url::decode");
}
