use mlua::prelude::*;

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

    t.set(
        "read",
        lua.create_function(|_, path: String| {
            std::fs::read_to_string(&path).map_err(LuaError::external)
        })?,
    )?;

    t.set(
        "write",
        lua.create_function(|_, (path, content): (String, String)| {
            std::fs::write(&path, content).map_err(LuaError::external)?;
            Ok(true)
        })?,
    )?;

    t.set(
        "copy",
        lua.create_function(|_, (src, dst): (String, String)| {
            std::fs::copy(&src, &dst).map_err(LuaError::external)?;
            Ok(true)
        })?,
    )?;

    t.set(
        "exists",
        lua.create_function(|_, path: String| Ok(std::path::Path::new(&path).exists()))?,
    )?;

    t.set(
        "is_dir",
        lua.create_function(|_, path: String| Ok(std::path::Path::new(&path).is_dir()))?,
    )?;

    t.set(
        "mkdir",
        lua.create_function(|_, path: String| {
            std::fs::create_dir_all(&path).map_err(LuaError::external)?;
            Ok(true)
        })?,
    )?;

    t.set(
        "remove",
        lua.create_function(|_, path: String| {
            let p = std::path::Path::new(&path);
            if p.is_dir() {
                std::fs::remove_dir_all(p).map_err(LuaError::external)?;
            } else {
                std::fs::remove_file(p).map_err(LuaError::external)?;
            }
            Ok(true)
        })?,
    )?;

    t.set(
        "walk",
        lua.create_function(|lua, dir: String| {
            let table = lua.create_table()?;
            let mut i = 1;
            for entry in walkdir::WalkDir::new(&dir).max_depth(256) {
                match entry {
                    Ok(e) if e.file_type().is_file() => {
                        table.set(i, e.path().to_string_lossy().to_string())?;
                        i += 1;
                    }
                    Ok(_) => {}
                    Err(e) => eprintln!("senl: fs.walk: {e}"),
                }
            }
            Ok(table)
        })?,
    )?;

    t.set(
        "glob",
        lua.create_function(|lua, pattern: String| {
            let table = lua.create_table()?;
            let mut i = 1;
            for entry in glob::glob(&pattern).map_err(|e| LuaError::external(e.to_string()))? {
                match entry {
                    Ok(path) => {
                        table.set(i, path.to_string_lossy().to_string())?;
                        i += 1;
                    }
                    Err(e) => eprintln!("senl: fs.glob: {e}"),
                }
            }
            Ok(table)
        })?,
    )?;

    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 read_and_write_file() {
        let dir = std::env::temp_dir().join("senl_test_fs_rw");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("test.txt");
        let path_str = path.to_string_lossy();

        let b: bool = eval(&format!(
            r#"
            local fs = require("senl.fs")
            fs.write("{path_str}", "hello senl")
            return fs.read("{path_str}") == "hello senl"
        "#
        ));
        assert!(b);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn exists_and_is_dir() {
        let dir = std::env::temp_dir().join("senl_test_fs_exists");
        std::fs::create_dir_all(&dir).unwrap();
        let dir_str = dir.to_string_lossy();

        let b: bool = eval(&format!(
            r#"
            local fs = require("senl.fs")
            return fs.exists("{dir_str}") and fs.is_dir("{dir_str}")
        "#
        ));
        assert!(b);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn mkdir_creates_nested_dirs() {
        let base = std::env::temp_dir().join("senl_test_fs_mkdir");
        let _ = std::fs::remove_dir_all(&base);
        let nested = base.join("a").join("b").join("c");
        let nested_str = nested.to_string_lossy();

        let b: bool = eval(&format!(
            r#"
            local fs = require("senl.fs")
            fs.mkdir("{nested_str}")
            return fs.is_dir("{nested_str}")
        "#
        ));
        assert!(b);
        let _ = std::fs::remove_dir_all(&base);
    }

    #[test]
    fn copy_file() {
        let dir = std::env::temp_dir().join("senl_test_fs_copy");
        std::fs::create_dir_all(&dir).unwrap();
        let src = dir.join("src.txt");
        let dst = dir.join("dst.txt");
        std::fs::write(&src, "copy me").unwrap();
        let src_str = src.to_string_lossy();
        let dst_str = dst.to_string_lossy();

        let s: String = eval(&format!(
            r#"
            local fs = require("senl.fs")
            fs.copy("{src_str}", "{dst_str}")
            return fs.read("{dst_str}")
        "#
        ));
        assert_eq!(s, "copy me");
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn remove_file() {
        let dir = std::env::temp_dir().join("senl_test_fs_remove");
        std::fs::create_dir_all(&dir).unwrap();
        let file = dir.join("to_remove.txt");
        std::fs::write(&file, "delete me").unwrap();
        let file_str = file.to_string_lossy();

        let b: bool = eval(&format!(
            r#"
            local fs = require("senl.fs")
            fs.remove("{file_str}")
            return not fs.exists("{file_str}")
        "#
        ));
        assert!(b);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn walk_returns_files() {
        let dir = std::env::temp_dir().join("senl_test_fs_walk");
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(dir.join("sub")).unwrap();
        std::fs::write(dir.join("a.txt"), "").unwrap();
        std::fs::write(dir.join("sub").join("b.txt"), "").unwrap();
        let dir_str = dir.to_string_lossy();

        let n: i64 = eval(&format!(
            r#"
            local fs = require("senl.fs")
            return #fs.walk("{dir_str}")
        "#
        ));
        assert_eq!(n, 2);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn glob_matches_pattern() {
        let dir = std::env::temp_dir().join("senl_test_fs_glob");
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("a.txt"), "").unwrap();
        std::fs::write(dir.join("b.txt"), "").unwrap();
        std::fs::write(dir.join("c.rs"), "").unwrap();
        let pattern = dir.join("*.txt").to_string_lossy().to_string();

        let n: i64 = eval(&format!(
            r#"
            local fs = require("senl.fs")
            return #fs.glob("{pattern}")
        "#
        ));
        assert_eq!(n, 2);
        let _ = std::fs::remove_dir_all(&dir);
    }

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