use mlua::prelude::*;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

/// Maximum sleep duration (1 day) to prevent accidental process freeze.
const MAX_SLEEP_SECS: f64 = 86_400.0;

/// Create the `senl.time` Lua module table.
///
/// # Provided functions
///
/// - `now()` — epoch seconds as `f64`
/// - `millis()` — epoch milliseconds as `i64`
/// - `sleep(seconds)` — block for `seconds` (0 ..= 86400, must be finite)
/// - `measure(fn)` — returns `(elapsed_secs, ...fn_results)`
pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
    let t = lua.create_table()?;

    t.set(
        "now",
        lua.create_function(|_, _: ()| {
            let dur = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .map_err(LuaError::external)?;
            Ok(dur.as_secs_f64())
        })?,
    )?;

    t.set(
        "millis",
        lua.create_function(|_, _: ()| {
            let dur = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .map_err(LuaError::external)?;
            let ms: i64 = dur.as_millis().try_into().unwrap_or(i64::MAX);
            Ok(ms)
        })?,
    )?;

    t.set(
        "sleep",
        lua.create_function(|_, seconds: f64| {
            if !seconds.is_finite() || seconds < 0.0 {
                return Err(LuaError::external(format!(
                    "sleep duration must be a finite non-negative number, got {seconds}"
                )));
            }
            if seconds > MAX_SLEEP_SECS {
                return Err(LuaError::external(format!(
                    "sleep duration must not exceed {MAX_SLEEP_SECS} seconds"
                )));
            }
            std::thread::sleep(Duration::from_secs_f64(seconds));
            Ok(())
        })?,
    )?;

    t.set(
        "measure",
        lua.create_function(|_, func: LuaFunction| {
            let start = std::time::Instant::now();
            let result: LuaMultiValue = func.call(())?;
            let elapsed = start.elapsed().as_secs_f64();
            let mut ret = vec![LuaValue::Number(elapsed)];
            ret.extend(result);
            Ok(LuaMultiValue::from_vec(ret))
        })?,
    )?;

    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 now_returns_positive_number() {
        let ts: f64 = eval(
            r#"
            local time = require("senl.time")
            return time.now()
        "#,
        );
        assert!(ts > 1_000_000_000.0, "timestamp should be after year 2001");
    }

    #[test]
    fn millis_returns_positive_integer() {
        let ms: i64 = eval(
            r#"
            local time = require("senl.time")
            return time.millis()
        "#,
        );
        assert!(
            ms > 1_000_000_000_000,
            "millis timestamp should be after year 2001"
        );
    }

    #[test]
    fn now_and_millis_consistent() {
        let diff: f64 = eval(
            r#"
            local time = require("senl.time")
            local sec = time.now()
            local ms = time.millis()
            return math.abs(sec * 1000 - ms)
        "#,
        );
        assert!(diff < 100.0, "now() and millis() should be within 100ms");
    }

    #[test]
    fn sleep_pauses_execution() {
        let elapsed: f64 = eval(
            r#"
            local time = require("senl.time")
            local before = time.now()
            time.sleep(0.05)
            return time.now() - before
        "#,
        );
        assert!(
            elapsed >= 0.04,
            "sleep(0.05) should pause at least 40ms, got {elapsed}s"
        );
    }

    #[test]
    fn sleep_negative_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"
                local time = require("senl.time")
                time.sleep(-1)
            "#,
            )
            .eval();
        assert!(result.is_err());
    }

    #[test]
    fn sleep_zero_is_valid() {
        let ok: bool = eval(
            r#"
            local time = require("senl.time")
            time.sleep(0)
            return true
        "#,
        );
        assert!(ok);
    }

    #[test]
    fn measure_returns_elapsed_and_result() {
        let (elapsed, value): (f64, String) = {
            let lua = runtime::create_lua(&[], &[]).unwrap();
            lua.load(
                r#"
                local time = require("senl.time")
                local elapsed, result = time.measure(function()
                    time.sleep(0.05)
                    return "done"
                end)
                return elapsed, result
            "#,
            )
            .eval()
            .unwrap()
        };
        assert!(elapsed >= 0.04, "measure should report at least 40ms");
        assert_eq!(value, "done");
    }

    #[test]
    fn measure_propagates_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"
                local time = require("senl.time")
                return time.measure(function()
                    error("boom")
                end)
            "#,
            )
            .eval();
        assert!(result.is_err());
    }

    #[test]
    fn measure_no_return_value() {
        let elapsed: f64 = eval(
            r#"
            local time = require("senl.time")
            local elapsed = time.measure(function() end)
            return elapsed
        "#,
        );
        assert!(elapsed >= 0.0);
    }

    #[test]
    fn sleep_nan_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"
                local time = require("senl.time")
                time.sleep(0/0)
            "#,
            )
            .eval();
        assert!(result.is_err());
    }

    #[test]
    fn sleep_infinity_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"
                local time = require("senl.time")
                time.sleep(math.huge)
            "#,
            )
            .eval();
        assert!(result.is_err());
    }

    #[test]
    fn sleep_neg_infinity_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"
                local time = require("senl.time")
                time.sleep(-math.huge)
            "#,
            )
            .eval();
        assert!(result.is_err());
    }

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