//! End-to-end integration test for `#[klieo_macros::tool]`.

use klieo_bus_memory::MemoryBus;
use klieo_core::error::ToolError;
use klieo_core::tool::{ToolCtx, ToolInvoker};
use klieo_tools::ChainedInvoker;
use std::sync::Arc;
use std::time::Duration;

#[klieo_macros::tool(description = "Greet a person")]
async fn greet(_ctx: &ToolCtx, name: String, excited: Option<bool>) -> Result<String, ToolError> {
    let suffix = if excited.unwrap_or(false) { "!" } else { "." };
    Ok(format!("Hello, {name}{suffix}"))
}

fn ctx() -> ToolCtx {
    let bus = MemoryBus::new();
    ToolCtx::new(bus.pubsub, bus.kv, bus.jobs)
}

#[tokio::test]
async fn macro_generated_tool_can_be_invoked_directly() {
    let tool = GreetTool;
    use klieo_core::tool::Tool as _;
    let out = tool
        .invoke(serde_json::json!({"name": "world", "excited": true}), ctx())
        .await
        .unwrap();
    assert_eq!(out, serde_json::Value::String("Hello, world!".into()));
}

#[tokio::test]
async fn macro_generated_tool_runs_through_chained_invoker() {
    let inv = ChainedInvoker::new()
        .with_default_timeout(Duration::from_secs(1))
        .with_tool(Arc::new(GreetTool))
        .unwrap();
    let out = inv
        .invoke(
            "greet",
            serde_json::json!({"name": "macros", "excited": false}),
            ctx(),
        )
        .await
        .unwrap();
    assert_eq!(out, serde_json::Value::String("Hello, macros.".into()));
}

#[tokio::test]
async fn macro_generated_schema_rejects_invalid_args_via_chained_invoker() {
    let inv = ChainedInvoker::new()
        .with_tool(Arc::new(GreetTool))
        .unwrap();
    // Missing required "name" field. ChainedInvoker validates via
    // schema first; the schema marks `name: String` as required.
    let err = inv
        .invoke("greet", serde_json::json!({"excited": true}), ctx())
        .await
        .unwrap_err();
    assert!(matches!(err, ToolError::InvalidArgs(_)));
}

#[tokio::test]
async fn macro_generated_tool_exposes_description_and_name() {
    use klieo_core::tool::Tool as _;
    let tool = GreetTool;
    assert_eq!(tool.name(), "greet");
    assert_eq!(tool.description(), "Greet a person");
    let schema = tool.json_schema();
    // Sanity check: schema should reference the args struct's `name` field.
    let serialized = schema.to_string();
    assert!(
        serialized.contains("name"),
        "schema must reference name field, got: {serialized}"
    );
}

#[tokio::test]
async fn original_function_is_still_callable_directly() {
    // The macro must preserve the original fn so users can call it
    // outside the Tool surface (unit tests, internal use).
    let result = greet(&ctx(), "direct".into(), Some(true)).await.unwrap();
    assert_eq!(result, "Hello, direct!");
}

#[klieo_macros::tool(description = "Store a value in KV")]
async fn kv_set(ctx: &ToolCtx, key: String, value: String) -> Result<String, ToolError> {
    #[allow(unused_imports)]
    use klieo_core::KvStore;
    ctx.kv
        .put("test", &key, value.into_bytes().into())
        .await
        .map_err(|e| ToolError::Permanent(e.to_string()))?;
    Ok("ok".into())
}

#[tokio::test]
async fn macro_generated_tool_uses_kv_from_ctx() {
    #[allow(unused_imports)]
    use klieo_core::KvStore;
    let bus = MemoryBus::new();
    let ctx = ToolCtx::new(bus.pubsub.clone(), bus.kv.clone(), bus.jobs.clone());
    let inv = ChainedInvoker::new()
        .with_tool(Arc::new(KvSetTool))
        .unwrap();
    let out = inv
        .invoke(
            "kv_set",
            serde_json::json!({"key": "k1", "value": "v1"}),
            ctx.clone(),
        )
        .await
        .unwrap();
    assert_eq!(out, serde_json::Value::String("ok".into()));
    // Verify the value actually landed in the shared KV.
    let entry = ctx.kv.get("test", "k1").await.unwrap().unwrap();
    assert_eq!(entry.value.as_ref(), b"v1");
}

#[test]
fn macro_generated_tool_implements_debug() {
    let tool = GreetTool;
    let debug_str = format!("{:?}", tool);
    assert!(
        debug_str.contains("GreetTool"),
        "Debug impl must mention struct name, got: {debug_str}"
    );
}

/// Greet a person warmly.
#[klieo_macros::tool]
async fn greet_doc(_ctx: &ToolCtx, name: String) -> Result<String, ToolError> {
    Ok(format!("Hi, {name}"))
}

#[tokio::test]
async fn doc_comment_falls_back_to_description() {
    use klieo_core::tool::Tool as _;
    let t = GreetDocTool;
    assert_eq!(t.name(), "greet_doc");
    assert_eq!(t.description(), "Greet a person warmly.");
}

/// Fetch data by id.
#[klieo_macros::tool(rename = "fetchData")]
async fn fetch_data(_ctx: &ToolCtx, _id: u32) -> Result<String, ToolError> {
    Ok("ok".into())
}

#[tokio::test]
async fn rename_replaces_tool_name_for_llm() {
    use klieo_core::tool::Tool as _;
    let t = FetchDataTool;
    assert_eq!(t.name(), "fetchData");
    assert_eq!(t.description(), "Fetch data by id.");
}

/// Fetch data from the API by id.
#[klieo_macros::tool(rename = "fetchById")]
async fn fetch_by_id(_ctx: &ToolCtx, _id: u32) -> Result<String, ToolError> {
    Ok("ok".into())
}

#[tokio::test]
async fn rename_and_doc_comment_both_apply() {
    use klieo_core::tool::Tool as _;
    let t = FetchByIdTool;
    assert_eq!(t.name(), "fetchById");
    assert_eq!(t.description(), "Fetch data from the API by id.");
}

#[klieo_macros::tool(description = "Explicit always wins")]
/// This doc comment should be ignored when description is explicit.
async fn explicit_wins(_ctx: &ToolCtx) -> Result<String, ToolError> {
    Ok("ok".into())
}

#[tokio::test]
async fn explicit_description_overrides_doc_comment() {
    use klieo_core::tool::Tool as _;
    let t = ExplicitWinsTool;
    assert_eq!(t.description(), "Explicit always wins");
}
