use mlua::prelude::*;
use sha2::{Digest, Sha256};

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

    t.set(
        "sha256",
        lua.create_function(|_, content: String| {
            let hash = Sha256::digest(content.as_bytes());
            Ok(format!("{hash:x}"))
        })?,
    )?;

    t.set(
        "sha256_file",
        lua.create_function(|_, path: String| {
            use std::io::Read;
            let file = std::fs::File::open(&path).map_err(LuaError::external)?;
            let mut reader = std::io::BufReader::new(file);
            let mut hasher = Sha256::new();
            let mut buf = [0u8; 8192];
            loop {
                let n = reader.read(&mut buf).map_err(LuaError::external)?;
                if n == 0 {
                    break;
                }
                hasher.update(&buf[..n]);
            }
            Ok(format!("{:x}", hasher.finalize()))
        })?,
    )?;

    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 sha256_known_value() {
        let s: String = eval(
            r#"
            local hash = require("senl.hash")
            return hash.sha256("hello")
        "#,
        );
        assert_eq!(
            s,
            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
        );
    }

    #[test]
    fn sha256_empty_string() {
        let s: String = eval(
            r#"
            local hash = require("senl.hash")
            return hash.sha256("")
        "#,
        );
        assert_eq!(
            s,
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
    }

    #[test]
    fn sha256_file() {
        let dir = std::env::temp_dir().join("senl_test_hash");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("test.txt");
        std::fs::write(&path, "hello").unwrap();
        let path_str = path.to_string_lossy();

        let s: String = eval(&format!(
            r#"
            local hash = require("senl.hash")
            return hash.sha256_file("{path_str}")
        "#
        ));
        assert_eq!(
            s,
            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
        );
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn sha256_file_nonexistent_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"local hash = require("senl.hash"); return hash.sha256_file("/tmp/__senl_nonexistent_hash__")"#,
            )
            .eval();
        assert!(result.is_err());
    }
}
