use mlua::prelude::*;
use std::process::Command;

pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
    let t = lua.create_table()?;

    // exec.run(cmd, {args}) → {stdout, stderr, code}
    t.set(
        "run",
        lua.create_function(|lua, (cmd, args): (String, Option<LuaTable>)| {
            let mut command = Command::new(&cmd);
            if let Some(args) = args {
                for v in args.sequence_values::<String>() {
                    command.arg(v?);
                }
            }
            let output = command.output().map_err(LuaError::external)?;

            let result = lua.create_table()?;
            result.set(
                "stdout",
                String::from_utf8_lossy(&output.stdout).to_string(),
            )?;
            result.set(
                "stderr",
                String::from_utf8_lossy(&output.stderr).to_string(),
            )?;
            result.set("code", output.status.code().unwrap_or(-1))?;
            Ok(result)
        })?,
    )?;

    // exec.capture(cmd, {args}) → string (stdout only, trimmed)
    t.set(
        "capture",
        lua.create_function(|_, (cmd, args): (String, Option<LuaTable>)| {
            let mut command = Command::new(&cmd);
            if let Some(args) = args {
                for v in args.sequence_values::<String>() {
                    command.arg(v?);
                }
            }
            let output = command.output().map_err(LuaError::external)?;
            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr).to_string();
                return Err(LuaError::external(format!(
                    "{cmd} exited with {}: {stderr}",
                    output.status.code().unwrap_or(-1)
                )));
            }
            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
        })?,
    )?;

    Ok(t)
}

#[cfg(test)]
mod tests {
    use crate::runtime;

    fn eval<T: mlua::FromLua>(code: &str) -> T {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        lua.load(code).eval().unwrap()
    }

    #[test]
    fn run_echo() {
        let s: String = eval(
            r#"
            local exec = require("senl.exec")
            local r = exec.run("echo", {"hello"})
            return r.stdout
        "#,
        );
        assert_eq!(s.trim(), "hello");
    }

    #[test]
    fn run_returns_exit_code() {
        let n: i64 = eval(
            r#"
            local exec = require("senl.exec")
            local r = exec.run("false")
            return r.code
        "#,
        );
        assert_eq!(n, 1);
    }

    #[test]
    fn capture_echo() {
        let s: String = eval(
            r#"
            local exec = require("senl.exec")
            return exec.capture("echo", {"world"})
        "#,
        );
        assert_eq!(s, "world");
    }

    #[test]
    fn capture_failing_command_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(r#"local exec = require("senl.exec"); return exec.capture("false")"#)
            .eval();
        assert!(result.is_err());
    }

    #[test]
    fn run_nonexistent_command_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"local exec = require("senl.exec"); return exec.run("__senl_nonexistent_cmd__")"#,
            )
            .eval();
        assert!(result.is_err());
    }
}
