//! Generic `SiteHandler` over a descriptor `Executor`. One instance per warm
//! session; lets any migrated adapter's `CommandModule` serve daemon commands
//! without a hand-written handler.

use async_trait::async_trait;
use night_fury_daemon_core::protocol::Response;
use serde_json::Value;
use tail_fin_core::Executor;

use crate::handlers::response::{err_for, ok_value};
use crate::handlers::SiteHandler;

pub struct ExecutorHandler {
    executor: Box<dyn Executor>,
}

impl ExecutorHandler {
    pub fn new(executor: Box<dyn Executor>) -> Self {
        Self { executor }
    }
}

#[async_trait]
impl SiteHandler for ExecutorHandler {
    async fn handle(&self, id: &str, cmd: &str, params: &Value) -> Response {
        match self.executor.execute(cmd, params.clone()).await {
            Ok(value) => ok_value(id, value),
            Err(e) => err_for(id, &e),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use tail_fin_common::TailFinError;

    struct OkExec;
    #[async_trait]
    impl Executor for OkExec {
        async fn execute(&self, wire: &str, params: Value) -> Result<Value, TailFinError> {
            Ok(json!({ "wire": wire, "params": params }))
        }
    }

    struct ErrExec;
    #[async_trait]
    impl Executor for ErrExec {
        async fn execute(&self, _wire: &str, _params: Value) -> Result<Value, TailFinError> {
            Err(TailFinError::Api("boom".into()))
        }
    }

    #[tokio::test]
    async fn ok_result_becomes_ok_response_with_data() {
        let h = ExecutorHandler::new(Box::new(OkExec));
        let resp = h.handle("req-1", "reddit.hot", &json!({"limit": 5})).await;
        let s = serde_json::to_string(&resp).unwrap();
        assert!(s.contains("\"ok\":true"), "expected ok response: {s}");
        assert!(
            s.contains("reddit.hot"),
            "data should carry the echoed wire: {s}"
        );
        assert!(s.contains("\"limit\":5"));
    }

    #[tokio::test]
    async fn err_result_becomes_err_response() {
        let h = ExecutorHandler::new(Box::new(ErrExec));
        let resp = h.handle("req-2", "reddit.hot", &json!({})).await;
        let s = serde_json::to_string(&resp).unwrap();
        assert!(s.contains("\"ok\":false"), "expected err response: {s}");
        assert!(s.contains("boom"), "error message should surface: {s}");
    }

    struct TimeoutExec;
    #[async_trait]
    impl Executor for TimeoutExec {
        async fn execute(&self, _wire: &str, _params: Value) -> Result<Value, TailFinError> {
            Err(TailFinError::Timeout("harvest timed out".into()))
        }
    }

    #[tokio::test]
    async fn err_result_carries_error_kind_on_the_wire() {
        let h = ExecutorHandler::new(Box::new(TimeoutExec));
        let resp = h.handle("req-3", "grok.ask", &json!({})).await;
        let s = serde_json::to_string(&resp).unwrap();
        assert!(s.contains("\"ok\":false"), "expected err response: {s}");
        assert!(
            s.contains("\"error_kind\":\"timeout\""),
            "error_kind must ride the wire: {s}"
        );
    }

    #[tokio::test]
    async fn api_err_result_carries_api_kind() {
        let h = ExecutorHandler::new(Box::new(ErrExec));
        let resp = h.handle("req-4", "reddit.hot", &json!({})).await;
        let s = serde_json::to_string(&resp).unwrap();
        assert!(
            s.contains("\"error_kind\":\"api\""),
            "Api errors carry kind=api: {s}"
        );
    }
}
