// Integration tests that exercise the public SDK surface only.

use sandlet_sdk::{
    Agent, AgentBuilder, ErrorKind, FunctionModule, FunctionSpec, RunCancellation, RunLimits,
    RunRequest, ScriptSource,
};
use std::time::{SystemTime, UNIX_EPOCH};

fn echo_module() -> FunctionModule {
    FunctionModule::builder("echo")
        .doc("Echo module for integration tests.")
        .function(FunctionSpec::new(
            "echo",
            "upper",
            "Uppercase a string.",
            vec![String::from("value")],
            "String",
        ))
        .build(|runtime, _params| {
            sandlet_sdk::register_fn(
                runtime,
                "echo_upper",
                "Uppercase a string.",
                &["value"],
                |v: std::sync::Arc<str>| -> std::sync::Arc<str> { v.to_uppercase().into() },
            )
        })
}

fn make_agent() -> Agent {
    AgentBuilder::new()
        .name("test-agent")
        .module(sandlet_sdk::builtin::strings::module())
        .module(echo_module())
        .build()
        .expect("build succeeds")
}

fn unique_test_dir(prefix: &str) -> std::path::PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time should be after unix epoch")
        .as_nanos();
    std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()))
}

#[test]
fn inline_script_execution_through_agent() {
    let agent = make_agent();
    let request = RunRequest {
        script: ScriptSource::Inline {
            source: r#"fn main() -> String { strings_uppercase("hello") }"#.into(),
        },
        entrypoint: "main".into(),
        args: vec![],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits::default(),
    };
    let result = agent.run_once(request).expect("run succeeds");
    assert_eq!(result.output, "HELLO");
}

#[test]
fn file_backed_script_execution_through_agent() {
    let agent = make_agent();
    let tmp = unique_test_dir("sandlet-public-api-test");
    let script_path = tmp.join("test.roto");
    std::fs::create_dir_all(&tmp).unwrap();
    std::fs::write(
        &script_path,
        r#"fn main() -> String { echo_upper("world") }"#,
    )
    .unwrap();

    let request = RunRequest {
        script: ScriptSource::File {
            path: script_path.to_string_lossy().to_string(),
        },
        entrypoint: "main".into(),
        args: vec![],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits::default(),
    };
    let result = agent.run_once(request).expect("run succeeds");
    assert_eq!(result.output, "WORLD");
}

#[test]
fn diagnostic_rich_failure_from_public_api() {
    let agent = make_agent();
    let request = RunRequest {
        script: ScriptSource::Inline {
            source: r#"fn broken { "#.into(),
        },
        entrypoint: "main".into(),
        args: vec![],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits::default(),
    };
    let result = agent.run_once(request);
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(
        err.diagnostics()
            .iter()
            .any(|d| d.code.as_deref() == Some("roto-compile-error")),
        "expected compile diagnostic on public api failure path"
    );
}

#[test]
fn agent_metadata_reflects_modules() {
    let agent = make_agent();
    let meta = agent.metadata();
    assert_eq!(meta.agent_name, "test-agent");
    assert_eq!(meta.modules.len(), 2);
    let names: Vec<_> = meta.modules.iter().map(|m| m.name.as_str()).collect();
    assert!(names.contains(&"strings"));
    assert!(names.contains(&"echo"));
}

#[test]
fn run_with_args_through_public_api() {
    let agent = make_agent();
    let request = RunRequest {
        script: ScriptSource::Inline {
            source: r#"fn main(args: List[String]) -> String {
    match args.get(0) {
        Some(value) -> strings_uppercase(value),
        None -> "empty",
    }
}"#
            .into(),
        },
        entrypoint: "main".into(),
        args: vec![String::from("hello")],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits::default(),
    };
    let result = agent.run_once(request).expect("run succeeds");
    assert_eq!(result.output, "HELLO");
}

#[test]
fn invalid_entrypoint_returns_error() {
    let agent = make_agent();
    let request = RunRequest {
        script: ScriptSource::Inline {
            source: r#"fn main() -> String { "ok" }"#.into(),
        },
        entrypoint: "not_main".into(),
        args: vec![],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits::default(),
    };
    let result = agent.run_once(request);
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.message_str().contains("entrypoint must be 'main'"));
}

#[test]
fn output_too_large_returns_error() {
    let agent = make_agent();
    let request = RunRequest {
        script: ScriptSource::Inline {
            source: r#"fn main() -> String { "long output" }"#.into(),
        },
        entrypoint: "main".into(),
        args: vec![],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits {
            max_script_bytes: 64 * 1024,
            max_args: 64,
            max_arg_bytes: 64 * 1024,
            max_output_bytes: 4,
            timeout_s: 0,
            max_temp_bytes: 64 * 1024,
            metadata: None,
        },
    };
    let result = agent.run_once(request);
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.message_str().contains("max_output_bytes"));
}

#[test]
fn explicit_cancellation_through_public_api() {
    let agent = make_agent();
    let request = RunRequest {
        script: ScriptSource::Inline {
            source: r#"fn main() -> String { "ok" }"#.into(),
        },
        entrypoint: "main".into(),
        args: vec![],
        parameters: std::collections::HashMap::new(),
        limits: RunLimits::default(),
    };
    let cancellation = RunCancellation::none();
    cancellation.cancel();

    let err = agent
        .run_once_with_cancellation(request, cancellation)
        .unwrap_err();

    assert_eq!(err.kind(), ErrorKind::Cancelled);
    assert!(
        err.diagnostics()
            .iter()
            .any(|d| d.code.as_deref() == Some("execution-cancelled"))
    );
}
