//! Dispatch orchestration: registry → SecurityFloor → permission → Tool::run.

use crate::floor::{FloorDecision, SecurityFloor};
use crate::registry::ToolRegistry;
use crate::tool::{AnyToolInput, ToolContext, ToolEventSink};

/// Outcome of a dispatch call. Matches `AnyTool::run`'s return shape:
/// `Ok` is a successful structured result, `Err` is a structured error.
pub type DispatchResult = Result<serde_json::Value, serde_json::Value>;

/// Dispatch a tool call through the harness.
///
/// 1. Resolve the tool via the registry (built-in vs dynamic, with collision
///    policy and per-client/per-protocol filters applied).
/// 2. Run the hardcoded [`SecurityFloor`]. Cannot be bypassed by settings.
/// 3. (Permission check is handled by the caller for now — the existing
///    [`crate::permission::check::check_permission`] takes a different
///    operation type and is wired in by the connector. This is documented in
///    `2026-04-28-tool-harness-design.md`.)
/// 4. Run the tool, awaiting its final result.
pub async fn dispatch(
    registry: &ToolRegistry,
    floor: &SecurityFloor,
    name: &str,
    input: serde_json::Value,
    events: ToolEventSink,
    ctx: &ToolContext,
) -> DispatchResult {
    let tool = match registry.resolve(name, ctx) {
        Some(t) => t,
        None => return Err(serde_json::json!({
            "error": format!("unknown tool: {name}"),
        })),
    };

    if let FloorDecision::Block { reason } = floor.check(name, &input, ctx) {
        return Err(serde_json::json!({ "error": reason }));
    }

    tool.run(AnyToolInput::Final(input), events, ctx).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{PermissionConfig, SandboxConfig, WhitelistConfig};
    use crate::permission::check::PermissionContext;
    use crate::permission::whitelist::CompiledWhitelist;
    use crate::registry::CollisionPolicy;
    use crate::tool::{AnyTool, ClientKind, ProtocolKind, Tool, ToolInput, ToolKind};
    use async_trait::async_trait;
    use schemars::JsonSchema;
    use serde::{Deserialize, Serialize};
    use std::path::PathBuf;
    use std::sync::Arc;

    #[derive(Serialize, Deserialize, JsonSchema)]
    struct EchoIn { msg: String }
    #[derive(Serialize, Deserialize)]
    struct EchoOut { echoed: String }

    #[derive(Default)]
    struct Echo;

    #[async_trait]
    impl Tool for Echo {
        type Input = EchoIn;
        type Output = EchoOut;
        const NAME: &'static str = "echo";
        fn kind() -> ToolKind { ToolKind::Other }
        async fn run(
            self: Arc<Self>, input: ToolInput<EchoIn>,
            _e: ToolEventSink, _c: &ToolContext,
        ) -> Result<EchoOut, EchoOut> {
            let i = match input { ToolInput::Final(i) => i, _ => unreachable!() };
            Ok(EchoOut { echoed: i.msg })
        }
    }

    fn ctx() -> ToolContext {
        let wl = CompiledWhitelist::compile(&WhitelistConfig::default()).unwrap();
        let pc = PermissionContext::new("c".to_string(), None, wl);
        ToolContext::for_test(
            "c", ClientKind::claude(), ProtocolKind::acp(),
            PathBuf::from("/tmp"),
            SandboxConfig::default(), PermissionConfig::default(), pc,
        )
    }

    fn registry_with_echo() -> ToolRegistry {
        let any: Arc<dyn AnyTool> = <Echo as Tool>::erase(Arc::new(Echo));
        ToolRegistry::new(vec![any], CollisionPolicy::BuiltInWins)
    }

    #[tokio::test]
    async fn dispatch_runs_known_tool() {
        let r = registry_with_echo();
        let f = SecurityFloor::new();
        let (sink, _rx) = ToolEventSink::new();
        let out = dispatch(&r, &f, "echo",
            serde_json::json!({ "msg": "hi" }), sink, &ctx()).await.unwrap();
        assert_eq!(out["echoed"], "hi");
    }

    #[tokio::test]
    async fn dispatch_rejects_unknown_tool() {
        let r = registry_with_echo();
        let f = SecurityFloor::new();
        let (sink, _rx) = ToolEventSink::new();
        let err = dispatch(&r, &f, "nope",
            serde_json::json!({}), sink, &ctx()).await.unwrap_err();
        assert!(err["error"].as_str().unwrap().contains("unknown tool"));
    }

    #[tokio::test]
    async fn dispatch_blocks_on_security_floor() {
        // We can't easily inject a "terminal" tool here, but we exercise the
        // floor by registering a tool literally named "terminal".
        #[derive(Default)] struct Term;
        #[derive(Serialize, Deserialize, JsonSchema)] struct In { command: String }
        #[derive(Serialize, Deserialize)] struct Out;
        #[async_trait]
        impl Tool for Term {
            type Input = In; type Output = Out;
            const NAME: &'static str = "terminal";
            fn kind() -> ToolKind { ToolKind::Execute }
            async fn run(
                self: Arc<Self>, _i: ToolInput<In>,
                _e: ToolEventSink, _c: &ToolContext,
            ) -> Result<Out, Out> { Ok(Out) }
        }

        let any: Arc<dyn AnyTool> = <Term as Tool>::erase(Arc::new(Term));
        let r = ToolRegistry::new(vec![any], CollisionPolicy::BuiltInWins);
        let f = SecurityFloor::new();
        let (sink, _rx) = ToolEventSink::new();
        let err = dispatch(&r, &f, "terminal",
            serde_json::json!({ "command": "rm -rf /" }), sink, &ctx())
            .await.unwrap_err();
        assert!(err["error"].as_str().unwrap().to_lowercase().contains("blocked"));
    }
}
