use std::{
    env,
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::atomic::{AtomicUsize, Ordering},
};

static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0);

const EXPECTED_TEST_OUTPUT: &str = concat!(
    "test \"explicit local fs write text status facade\" ... ok\n",
    "test \"explicit local fs read text facade\" ... ok\n",
    "test \"explicit local fs write text result facade\" ... ok\n",
    "test \"explicit local fs read text result facade\" ... ok\n",
    "test \"explicit local fs read text via handle facade\" ... ok\n",
    "test \"explicit local fs open read close handle facade\" ... ok\n",
    "test \"explicit local fs closed handle read err facade\" ... ok\n",
    "test \"explicit local fs invalid close err facade\" ... ok\n",
    "test \"explicit local fs exists file facade\" ... ok\n",
    "test \"explicit local fs exists missing facade\" ... ok\n",
    "test \"explicit local fs create dir result facade\" ... ok\n",
    "test \"explicit local fs is dir facade\" ... ok\n",
    "test \"explicit local fs create dir ok facade\" ... ok\n",
    "test \"explicit local fs remove file result facade\" ... ok\n",
    "test \"explicit local fs remove file ok facade\" ... ok\n",
    "test \"explicit local fs read text option missing facade\" ... ok\n",
    "test \"explicit local fs read text option present facade\" ... ok\n",
    "test \"explicit local fs read text or missing facade\" ... ok\n",
    "test \"explicit local fs read text or present facade\" ... ok\n",
    "test \"explicit local fs write text ok facade\" ... ok\n",
    "test \"explicit local fs read i32 result present facade\" ... ok\n",
    "test \"explicit local fs read i32 or zero invalid facade\" ... ok\n",
    "test \"explicit local fs read u32 result present facade\" ... ok\n",
    "test \"explicit local fs read u32 or zero invalid facade\" ... ok\n",
    "test \"explicit local fs read i64 result present facade\" ... ok\n",
    "test \"explicit local fs read i64 or zero missing facade\" ... ok\n",
    "test \"explicit local fs read u64 result present facade\" ... ok\n",
    "test \"explicit local fs read u64 or zero missing facade\" ... ok\n",
    "test \"explicit local fs read f64 result present facade\" ... ok\n",
    "test \"explicit local fs read f64 or zero invalid facade\" ... ok\n",
    "test \"explicit local fs read bool result present facade\" ... ok\n",
    "test \"explicit local fs read bool or false invalid facade\" ... ok\n",
    "test \"explicit local fs typed option facade\" ... ok\n",
    "test \"explicit local fs typed custom fallback facade\" ... ok\n",
    "test \"explicit local fs facade all\" ... ok\n",
    "35 test(s) passed\n",
);

const STANDARD_FS_SOURCE_FACADE_ALPHA: &[&str] = &[
    "read_text",
    "read_text_result",
    "read_text_option",
    "write_text_status",
    "write_text_result",
    "exists",
    "is_file",
    "is_dir",
    "remove_file_result",
    "create_dir_result",
    "remove_file_ok",
    "create_dir_ok",
    "open_text_read_result",
    "read_open_text_result",
    "close_result",
    "read_text_via_handle_result",
    "close_ok",
    "read_text_or",
    "write_text_ok",
    "read_i32_result",
    "read_i32_option",
    "read_i32_or_zero",
    "read_i32_or",
    "read_u32_result",
    "read_u32_option",
    "read_u32_or_zero",
    "read_u32_or",
    "read_i64_result",
    "read_i64_option",
    "read_i64_or_zero",
    "read_i64_or",
    "read_u64_result",
    "read_u64_option",
    "read_u64_or_zero",
    "read_u64_or",
    "read_f64_result",
    "read_f64_option",
    "read_f64_or_zero",
    "read_f64_or",
    "read_bool_result",
    "read_bool_option",
    "read_bool_or_false",
    "read_bool_or",
];

const LOCAL_RESULT_BRIDGE_HELPERS: &[&str] = &[
    "ok_or_none_i32",
    "ok_or_none_u32",
    "ok_or_none_i64",
    "ok_or_none_u64",
    "ok_or_none_string",
    "ok_or_none_f64",
    "ok_or_none_bool",
];

#[test]
fn standard_fs_source_facade_project_checks_formats_and_tests() {
    let project =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-fs");

    assert_local_fs_fixture_is_source_authored(&project);

    let fmt = run_glagol([
        OsStr::new("fmt"),
        OsStr::new("--check"),
        project.as_os_str(),
    ]);
    assert_success("std layout local fs fmt --check", &fmt);

    let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
    assert_success_stdout(check, "", "std layout local fs check");

    let test_cwd = temp_root("test");
    let test = run_glagol_in([OsStr::new("test"), project.as_os_str()], &test_cwd);
    assert_success_stdout(
        test,
        EXPECTED_TEST_OUTPUT,
        "std layout local fs test output",
    );
}

fn assert_local_fs_fixture_is_source_authored(project: &Path) {
    let fs_source = read(&project.join("src/fs.slo"));
    let result_source = read(&project.join("src/result.slo"));
    let string_source = read(&project.join("src/string.slo"));
    let main = read(&project.join("src/main.slo"));

    assert!(
        fs_source.starts_with("(module fs (export "),
        "fs.slo must stay an explicit local module export"
    );
    assert!(
        string_source.starts_with("(module string (export "),
        "string.slo must stay an explicit local module export"
    );
    assert!(
        result_source.starts_with("(module result (export "),
        "result.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import fs ("),
        "main.slo must stay an explicit local fs import"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "fs fixture must not depend on automatic or package std imports"
    );
    assert!(
        main.contains("\"glagol-std-layout-local-fs-alpha.txt\"")
            && main.contains("\"std fs source search alpha\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-missing.txt\"")
            && main.contains("\"std fs source fallback alpha\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-i32.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-u32.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-i64.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-u64.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-f64.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-bool.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-invalid.txt\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-dir\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-dir-helper\"")
            && main.contains("\"glagol-std-layout-local-fs-alpha-remove.txt\""),
        "fs fixture must use deterministic relative paths and text content"
    );
    assert!(
        fs_source.contains("(import result (ok_or_none_string ok_or_none_i32 ok_or_none_u32 ok_or_none_i64 ok_or_none_u64 ok_or_none_f64 ok_or_none_bool))")
            && fs_source.contains("(import string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))")
            && fs_source.contains("(std.fs.read_text path)")
            && fs_source.contains("(std.fs.read_text_result path)")
            && fs_source.contains("(std.fs.write_text path text)")
            && fs_source.contains("(std.fs.write_text_result path text)")
            && fs_source.contains("(std.fs.exists path)")
            && fs_source.contains("(std.fs.is_file path)")
            && fs_source.contains("(std.fs.is_dir path)")
            && fs_source.contains("(std.fs.remove_file_result path)")
            && fs_source.contains("(std.fs.create_dir_result path)")
            && fs_source.contains("(std.fs.open_text_read_result path)")
            && fs_source.contains("(std.fs.read_open_text_result handle)")
            && fs_source.contains("(std.fs.close_result handle)"),
        "fs.slo must stay a local source facade over fs calls plus local string and result bridge helpers"
    );
    assert!(
        !fs_source.contains("std.string.")
            && !main.contains("std.")
            && !fs_source.contains("(import std."),
        "fs fixture must stay local and must not depend on repo std imports"
    );

    let mut non_string_std = string_source.clone();
    for allowed in [
        "std.string.len",
        "std.string.concat",
        "std.string.parse_i32_result",
        "std.string.parse_u32_result",
        "std.string.parse_i64_result",
        "std.string.parse_u64_result",
        "std.string.parse_f64_result",
        "std.string.parse_bool_result",
    ] {
        non_string_std = non_string_std.replace(allowed, "");
    }
    assert!(
        !non_string_std.contains("std."),
        "local string fixture must use only the promoted std.string runtime names"
    );

    let mut non_result_std = result_source.clone();
    for allowed in [
        "std.result.is_ok",
        "std.result.is_err",
        "std.result.unwrap_ok",
        "std.result.unwrap_err",
    ] {
        non_result_std = non_result_std.replace(allowed, "");
    }
    assert!(
        !non_result_std.contains("std."),
        "local result fixture must use only the promoted std.result runtime names"
    );
    assert!(
        !fs_source.contains("binary")
            && !fs_source.contains("list_dir")
            && !fs_source.contains("walk")
            && !fs_source.contains("stream")
            && !fs_source.contains("async")
            && !fs_source.contains("host_error")
            && !main.contains("binary")
            && !main.contains("list_dir")
            && !main.contains("walk")
            && !main.contains("stream")
            && !main.contains("async")
            && !main.contains("host_error"),
        "fs fixture must not claim deferred fs APIs or richer host error semantics"
    );

    for helper in STANDARD_FS_SOURCE_FACADE_ALPHA {
        assert!(
            fs_source.contains(&format!("(fn {} ", helper)),
            "fs.slo is missing source facade `{}`",
            helper
        );
        assert!(
            main.contains(helper),
            "main.slo does not explicitly import/use `{}`",
            helper
        );
    }

    for helper in LOCAL_RESULT_BRIDGE_HELPERS {
        assert!(
            result_source.contains(&format!("(fn {} ", helper)),
            "result.slo is missing local result bridge helper `{}`",
            helper
        );
    }

    for helper in [
        "parse_i32_result",
        "parse_u32_result",
        "parse_i64_result",
        "parse_u64_result",
        "parse_f64_result",
        "parse_bool_result",
    ] {
        assert!(
            string_source.contains(&format!("(fn {} ", helper)),
            "string.slo is missing local parse helper `{}`",
            helper
        );
    }
}

fn run_glagol<I, S>(args: I) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    run_glagol_in(args, Path::new(env!("CARGO_MANIFEST_DIR")))
}

fn run_glagol_in<I, S>(args: I, cwd: &Path) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    Command::new(env!("CARGO_BIN_EXE_glagol"))
        .args(args)
        .current_dir(cwd)
        .output()
        .expect("run glagol")
}

fn temp_root(name: &str) -> PathBuf {
    let path = env::temp_dir().join(format!(
        "glagol-standard-fs-source-facade-{}-{}-{}",
        name,
        std::process::id(),
        NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
    ));
    let _ = fs::remove_dir_all(&path);
    fs::create_dir_all(&path).unwrap_or_else(|err| panic!("create `{}`: {}", path.display(), err));
    path
}

fn read(path: &Path) -> String {
    fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
}

fn assert_success(context: &str, output: &Output) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "{} stdout drifted", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
