//! Bot command HTTP endpoint.
//!
//! `POST /api/bot/command` lets the web UI invoke the same deterministic bot
//! commands available in Matrix without going through the LLM.
//!
//! Dispatches to [`crate::service::bot_command::execute`], which owns all
//! parsing and routing logic. This handler is a thin OpenAPI adapter: it
//! receives JSON, calls the service, and maps typed errors to HTTP status codes.

use crate::http::context::{AppContext, OpenApiResult};
use crate::service::bot_command as svc;
use poem::http::StatusCode;
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Tags)]
enum BotCommandTags {
    BotCommand,
}

/// Body for `POST /api/bot/command`.
#[derive(Object, Deserialize)]
struct BotCommandRequest {
    /// The command keyword without the leading slash (e.g. `"status"`, `"start"`).
    command: String,
    /// Any text after the command keyword, trimmed (may be empty).
    #[oai(default)]
    args: String,
}

/// Response body for `POST /api/bot/command`.
#[derive(Object, Serialize)]
struct BotCommandResponse {
    /// Markdown-formatted response text.
    response: String,
}

pub struct BotCommandApi {
    pub ctx: Arc<AppContext>,
}

#[OpenApi(tag = "BotCommandTags::BotCommand")]
impl BotCommandApi {
    /// Execute a slash command without LLM invocation.
    ///
    /// Dispatches to the same handlers used by the Matrix and Slack bots.
    /// Returns a markdown-formatted response that the frontend can display
    /// directly in the chat panel.
    ///
    /// # Errors
    /// - `400 Bad Request` — project root not set, or invalid command arguments.
    /// - `404 Not Found` — unrecognised command keyword.
    /// - `500 Internal Server Error` — command execution failed.
    #[oai(path = "/bot/command", method = "post")]
    async fn run_command(
        &self,
        body: Json<BotCommandRequest>,
    ) -> OpenApiResult<Json<BotCommandResponse>> {
        let project_root = self
            .ctx
            .state
            .get_project_root()
            .map_err(|e| poem::Error::from_string(e, StatusCode::BAD_REQUEST))?;

        let cmd = body.command.trim().to_ascii_lowercase();
        let args = body.args.trim();

        let response = svc::execute(&cmd, args, &project_root, &self.ctx.services.agents)
            .await
            .map_err(|e| match e {
                svc::Error::UnknownCommand(msg) => {
                    poem::Error::from_string(msg, StatusCode::NOT_FOUND)
                }
                svc::Error::BadArgs(msg) => poem::Error::from_string(msg, StatusCode::BAD_REQUEST),
                svc::Error::CommandFailed(msg) => {
                    poem::Error::from_string(msg, StatusCode::INTERNAL_SERVER_ERROR)
                }
            })?;

        Ok(Json(BotCommandResponse { response }))
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    fn test_api(dir: &TempDir) -> BotCommandApi {
        BotCommandApi {
            ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
        }
    }

    #[tokio::test]
    async fn help_command_returns_response() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "help".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_ok());
        let resp = result.unwrap().0;
        assert!(!resp.response.is_empty());
    }

    #[tokio::test]
    async fn unknown_command_returns_error_message() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "nonexistent_xyz".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_err(), "unknown command should return HTTP 404");
    }

    #[tokio::test]
    async fn start_without_number_returns_usage() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "start".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_err(), "start with no args should return HTTP 400");
    }

    #[tokio::test]
    async fn delete_without_number_returns_usage() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "delete".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(
            result.is_err(),
            "delete with no args should return HTTP 400"
        );
    }

    #[tokio::test]
    async fn git_command_returns_response() {
        let dir = TempDir::new().unwrap();
        // Initialise a bare git repo so the git command has something to query.
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(dir.path())
            .output()
            .ok();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "git".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn timer_list_returns_response_not_unknown_command() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "timer".to_string(),
            args: "list".to_string(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(
            result.is_ok(),
            "timer list should succeed, got err: {:?}",
            result.err().map(|e| e.to_string())
        );
        let resp = result.unwrap().0;
        assert!(
            !resp.response.contains("Unknown command"),
            "timer list should not return 'Unknown command': {}",
            resp.response
        );
    }

    // -- htop (web-UI slash-command path) ------------------------------------

    #[tokio::test]
    async fn htop_returns_dashboard_not_unknown_command() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "htop".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_ok());
        let resp = result.unwrap().0;
        assert!(
            !resp.response.contains("Unknown command"),
            "htop should not return 'Unknown command': {}",
            resp.response
        );
        assert!(
            resp.response.contains("htop"),
            "htop response should contain 'htop': {}",
            resp.response
        );
    }

    #[tokio::test]
    async fn htop_with_duration_returns_dashboard() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "htop".to_string(),
            args: "10m".to_string(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_ok());
        let resp = result.unwrap().0;
        assert!(
            !resp.response.contains("Unknown command"),
            "htop 10m should not return 'Unknown command': {}",
            resp.response
        );
    }

    #[tokio::test]
    async fn htop_stop_returns_response_not_unknown_command() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "htop".to_string(),
            args: "stop".to_string(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_ok());
        let resp = result.unwrap().0;
        assert!(
            !resp.response.contains("Unknown command"),
            "htop stop should not return 'Unknown command': {}",
            resp.response
        );
    }

    // -- rmtree ----------------------------------------------------------------

    #[tokio::test]
    async fn rmtree_without_number_returns_usage() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "rmtree".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(
            result.is_err(),
            "rmtree with no args should return HTTP 400"
        );
    }

    #[tokio::test]
    async fn rmtree_with_non_numeric_arg_returns_usage() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "rmtree".to_string(),
            args: "foo".to_string(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(
            result.is_err(),
            "rmtree with non-numeric arg should return HTTP 400"
        );
    }

    #[tokio::test]
    async fn rmtree_does_not_return_unknown_command() {
        let dir = TempDir::new().unwrap();
        let api = test_api(&dir);
        let body = BotCommandRequest {
            command: "rmtree".to_string(),
            args: "999".to_string(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_ok());
        let resp = result.unwrap().0;
        assert!(
            !resp.response.contains("Unknown command"),
            "/rmtree should not return 'Unknown command': {}",
            resp.response
        );
    }

    // -- htop bot-command path (regression: htop must remain in command registry) --

    #[test]
    fn htop_is_registered_in_bot_command_registry() {
        let commands = crate::chat::commands::commands();
        assert!(
            commands.iter().any(|c| c.name == "htop"),
            "htop must be registered in the bot command registry so /help lists it"
        );
    }

    #[tokio::test]
    async fn run_command_requires_project_root() {
        // Create a context with no project root set.
        let dir = TempDir::new().unwrap();
        let ctx = AppContext::new_test(dir.path().to_path_buf());
        // Clear the project root.
        *ctx.state.project_root.lock().unwrap() = None;
        let api = BotCommandApi { ctx: Arc::new(ctx) };
        let body = BotCommandRequest {
            command: "status".to_string(),
            args: String::new(),
        };
        let result = api.run_command(Json(body)).await;
        assert!(result.is_err(), "should fail when no project root is set");
    }
}
