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 MISSING_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_UNLIKELY_MISSING";
const PRESENT_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT";
const PRESENT_ENV_VALUE: &str = "glagol-std-import-env-alpha-value";
const PRESENT_I32_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_I32";
const PRESENT_I32_ENV_VALUE: &str = "42";
const PRESENT_U32_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_U32";
const PRESENT_U32_ENV_VALUE: &str = "42";
const PRESENT_I64_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_I64";
const PRESENT_I64_ENV_VALUE: &str = "42000000000";
const PRESENT_U64_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_U64";
const PRESENT_U64_ENV_VALUE: &str = "4294967296";
const PRESENT_F64_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_F64";
const PRESENT_F64_ENV_VALUE: &str = "42.5";
const PRESENT_BOOL_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_PRESENT_BOOL";
const PRESENT_BOOL_ENV_VALUE: &str = "true";
const INVALID_ENV_NAME: &str = "GLAGOL_STD_IMPORT_ENV_ALPHA_INVALID";
const INVALID_ENV_VALUE: &str = "bad";

const EXPECTED_STD_TIME_OUTPUT: &str = concat!(
    "test \"explicit std time monotonic facade\" ... ok\n",
    "test \"explicit std time sleep zero facade\" ... ok\n",
    "test \"explicit std time facade all\" ... ok\n",
    "3 test(s) passed\n",
);

const EXPECTED_STD_RANDOM_OUTPUT: &str = concat!(
    "test \"explicit std random i32 non-negative facade\" ... ok\n",
    "test \"explicit std random facade all\" ... ok\n",
    "2 test(s) passed\n",
);

const EXPECTED_STD_ENV_OUTPUT: &str = concat!(
    "test \"explicit std env get missing facade\" ... ok\n",
    "test \"explicit std env get result missing facade\" ... ok\n",
    "test \"explicit std env has missing facade\" ... ok\n",
    "test \"explicit std env get or missing facade\" ... ok\n",
    "test \"explicit std env get option missing facade\" ... ok\n",
    "test \"explicit std env has present facade\" ... ok\n",
    "test \"explicit std env get or present facade\" ... ok\n",
    "test \"explicit std env get option present facade\" ... ok\n",
    "test \"explicit std env get i32 result present facade\" ... ok\n",
    "test \"explicit std env get i32 or zero invalid facade\" ... ok\n",
    "test \"explicit std env get u32 result present facade\" ... ok\n",
    "test \"explicit std env get u32 or zero invalid facade\" ... ok\n",
    "test \"explicit std env get i64 result present facade\" ... ok\n",
    "test \"explicit std env get i64 or zero missing facade\" ... ok\n",
    "test \"explicit std env get u64 result present facade\" ... ok\n",
    "test \"explicit std env get u64 or zero missing facade\" ... ok\n",
    "test \"explicit std env get f64 result present facade\" ... ok\n",
    "test \"explicit std env get f64 or zero invalid facade\" ... ok\n",
    "test \"explicit std env get bool result present facade\" ... ok\n",
    "test \"explicit std env get bool or false invalid facade\" ... ok\n",
    "test \"explicit std env typed option facade\" ... ok\n",
    "test \"explicit std env typed custom fallback facade\" ... ok\n",
    "test \"explicit std env facade all\" ... ok\n",
    "23 test(s) passed\n",
);

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

const EXPECTED_STD_PROCESS_OUTPUT: &str = concat!(
    "test \"explicit std process argc facade\" ... ok\n",
    "test \"explicit std process arg result oob facade\" ... ok\n",
    "test \"explicit std process arg option facade\" ... ok\n",
    "test \"explicit std process has arg facade\" ... ok\n",
    "test \"explicit std process arg fallback missing facade\" ... ok\n",
    "test \"explicit std process arg fallback present facade\" ... ok\n",
    "test \"explicit std process arg i32 missing facade\" ... ok\n",
    "test \"explicit std process arg i32 invalid fallback facade\" ... ok\n",
    "test \"explicit std process arg u32 missing facade\" ... ok\n",
    "test \"explicit std process arg u32 invalid fallback facade\" ... ok\n",
    "test \"explicit std process arg i64 missing facade\" ... ok\n",
    "test \"explicit std process arg i64 invalid fallback facade\" ... ok\n",
    "test \"explicit std process arg u64 missing facade\" ... ok\n",
    "test \"explicit std process arg u64 invalid fallback facade\" ... ok\n",
    "test \"explicit std process arg f64 missing facade\" ... ok\n",
    "test \"explicit std process arg f64 invalid fallback facade\" ... ok\n",
    "test \"explicit std process arg bool missing facade\" ... ok\n",
    "test \"explicit std process arg bool invalid fallback facade\" ... ok\n",
    "test \"explicit std process typed option facade\" ... ok\n",
    "test \"explicit std process typed custom fallback facade\" ... ok\n",
    "test \"explicit std process facade all\" ... ok\n",
    "21 test(s) passed\n",
);

#[test]
fn explicit_std_time_import_loads_repo_root_standard_source() {
    assert_host_facade_project(
        "time",
        &["monotonic_ms", "sleep_ms_zero"],
        EXPECTED_STD_TIME_OUTPUT,
        |_| {},
        None,
    );
}

#[test]
fn explicit_std_random_import_loads_repo_root_standard_source() {
    assert_host_facade_project(
        "random",
        &["random_i32", "random_i32_non_negative"],
        EXPECTED_STD_RANDOM_OUTPUT,
        |_| {},
        None,
    );
}

#[test]
fn explicit_std_env_import_loads_repo_root_standard_source() {
    assert_host_facade_project(
        "env",
        &[
            "get",
            "get_result",
            "get_option",
            "has",
            "get_or",
            "get_i32_result",
            "get_i32_option",
            "get_i32_or_zero",
            "get_i32_or",
            "get_u32_result",
            "get_u32_option",
            "get_u32_or_zero",
            "get_u32_or",
            "get_i64_result",
            "get_i64_option",
            "get_i64_or_zero",
            "get_i64_or",
            "get_u64_result",
            "get_u64_option",
            "get_u64_or_zero",
            "get_u64_or",
            "get_f64_result",
            "get_f64_option",
            "get_f64_or_zero",
            "get_f64_or",
            "get_bool_result",
            "get_bool_option",
            "get_bool_or_false",
            "get_bool_or",
        ],
        EXPECTED_STD_ENV_OUTPUT,
        |command| {
            command.env_remove(MISSING_ENV_NAME);
            command.env(PRESENT_ENV_NAME, PRESENT_ENV_VALUE);
            command.env(PRESENT_I32_ENV_NAME, PRESENT_I32_ENV_VALUE);
            command.env(PRESENT_U32_ENV_NAME, PRESENT_U32_ENV_VALUE);
            command.env(PRESENT_I64_ENV_NAME, PRESENT_I64_ENV_VALUE);
            command.env(PRESENT_U64_ENV_NAME, PRESENT_U64_ENV_VALUE);
            command.env(PRESENT_F64_ENV_NAME, PRESENT_F64_ENV_VALUE);
            command.env(PRESENT_BOOL_ENV_NAME, PRESENT_BOOL_ENV_VALUE);
            command.env(INVALID_ENV_NAME, INVALID_ENV_VALUE);
        },
        None,
    );
}

#[test]
fn explicit_std_fs_import_loads_repo_root_standard_source() {
    let cwd = temp_root("fs");
    assert_host_facade_project(
        "fs",
        &[
            "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",
        ],
        EXPECTED_STD_FS_OUTPUT,
        |_| {},
        Some(&cwd),
    );
}

#[test]
fn explicit_std_process_import_loads_repo_root_standard_source() {
    assert_host_facade_project(
        "process",
        &[
            "argc",
            "arg",
            "arg_result",
            "arg_option",
            "has_arg",
            "arg_or",
            "arg_or_empty",
            "arg_i32_result",
            "arg_i32_option",
            "arg_i32_or_zero",
            "arg_i32_or",
            "arg_u32_result",
            "arg_u32_option",
            "arg_u32_or_zero",
            "arg_u32_or",
            "arg_i64_result",
            "arg_i64_option",
            "arg_i64_or_zero",
            "arg_i64_or",
            "arg_u64_result",
            "arg_u64_option",
            "arg_u64_or_zero",
            "arg_u64_or",
            "arg_f64_result",
            "arg_f64_option",
            "arg_f64_or_zero",
            "arg_f64_or",
            "arg_bool_result",
            "arg_bool_option",
            "arg_bool_or_false",
            "arg_bool_or",
        ],
        EXPECTED_STD_PROCESS_OUTPUT,
        |_| {},
        None,
    );
}

fn assert_host_facade_project<F>(
    module: &str,
    helpers: &[&str],
    expected: &str,
    configure: F,
    test_cwd: Option<&Path>,
) where
    F: Fn(&mut Command),
{
    let compiler_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let project = compiler_root.join(format!("../examples/projects/std-import-{}", module));
    let slovo_module = compiler_root.join(format!("../lib/std/{}.slo", module));

    assert!(
        !project.join(format!("src/{}.slo", module)).exists(),
        "std-import-{} must not carry a local {} module copy",
        module,
        module
    );
    assert!(
        read(&project.join("src/main.slo"))
            .starts_with(&format!("(module main)\n\n(import std.{} (", module)),
        "std-import-{} must exercise explicit `std.{}` import syntax",
        module,
        module
    );

    let slovo_source = read(&slovo_module);
    assert!(
        slovo_source.starts_with(&format!("(module {} (export ", module)),
        "repo-root Slovo std/{}.slo must export imported helpers directly",
        module
    );
    if matches!(module, "env" | "fs" | "process") {
        assert!(
            slovo_source
                .contains("(import std.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))"),
            "Slovo std/{}.slo must compose option helpers through the exp-109 result bridge helpers",
            module
        );
    }
    for helper in helpers {
        assert!(
            slovo_source.contains(&format!("(fn {} ", helper)),
            "Slovo std/{}.slo is missing helper `{}`",
            module,
            helper
        );
    }

    let fmt = run_glagol_configured(
        [
            OsStr::new("fmt"),
            OsStr::new("--check"),
            project.as_os_str(),
        ],
        Path::new(env!("CARGO_MANIFEST_DIR")),
        |_| {},
    );
    assert_success("std host facade source search fmt --check", &fmt);

    let check = run_glagol_configured(
        [OsStr::new("check"), project.as_os_str()],
        Path::new(env!("CARGO_MANIFEST_DIR")),
        |_| {},
    );
    assert_success_stdout(check, "", "std host facade source search check");

    let test = run_glagol_configured(
        [OsStr::new("test"), project.as_os_str()],
        test_cwd.unwrap_or(Path::new(env!("CARGO_MANIFEST_DIR"))),
        configure,
    );
    assert_success_stdout(test, expected, "std host facade source search test");
}

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

fn temp_root(name: &str) -> PathBuf {
    let path = env::temp_dir().join(format!(
        "glagol-standard-host-facade-source-search-{}-{}-{}",
        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\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
        context,
        output.status.code(),
        stdout,
        stderr
    );
}

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    assert_success(context, &output);
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert_eq!(stdout, expected, "{}", context);
}
