use std::{
    ffi::OsStr,
    fs,
    path::Path,
    process::{Command, Output},
};

const EXPECTED_STD_CLI_OUTPUT: &str = concat!(
    "test \"explicit std cli arg text missing\" ... ok\n",
    "test \"explicit std cli arg text option\" ... ok\n",
    "test \"explicit std cli arg i32 missing\" ... ok\n",
    "test \"explicit std cli arg i32 fallback missing\" ... ok\n",
    "test \"explicit std cli arg u32 missing\" ... ok\n",
    "test \"explicit std cli arg u32 fallback missing\" ... ok\n",
    "test \"explicit std cli arg i64 missing\" ... ok\n",
    "test \"explicit std cli arg i64 fallback missing\" ... ok\n",
    "test \"explicit std cli arg u64 missing\" ... ok\n",
    "test \"explicit std cli arg u64 fallback missing\" ... ok\n",
    "test \"explicit std cli arg f64 missing\" ... ok\n",
    "test \"explicit std cli arg f64 fallback missing\" ... ok\n",
    "test \"explicit std cli arg bool missing\" ... ok\n",
    "test \"explicit std cli arg bool fallback missing\" ... ok\n",
    "test \"explicit std cli typed option\" ... ok\n",
    "test \"explicit std cli typed custom fallback\" ... ok\n",
    "test \"explicit std cli facade all\" ... ok\n",
    "17 test(s) passed\n",
);

const STANDARD_CLI_SOURCE_FACADE_ALPHA: &[&str] = &[
    "arg_text_result",
    "arg_text_option",
    "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",
];

#[test]
fn explicit_std_cli_import_loads_transitive_standard_sources() {
    let compiler_root = Path::new(env!("CARGO_MANIFEST_DIR"));
    let project = compiler_root.join("../examples/projects/std-import-cli");
    let slovo_cli = compiler_root.join("../lib/std/cli.slo");

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

    let slovo_source = read(&slovo_cli);
    assert!(
        slovo_source.starts_with("(module cli (export "),
        "repo-root Slovo std/cli.slo must export imported helpers directly"
    );
    assert!(
        slovo_source.contains("(import std.process (arg_result))")
            && 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_source
                .contains("(import std.string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))"),
        "std/cli.slo must gate transitive standard-source imports"
    );
    for helper in STANDARD_CLI_SOURCE_FACADE_ALPHA {
        assert!(
            slovo_source.contains(&format!("(fn {} ", helper)),
            "Slovo std/cli.slo is missing helper `{}`",
            helper
        );
    }

    let fmt = run_glagol([
        OsStr::new("fmt"),
        OsStr::new("--check"),
        project.as_os_str(),
    ]);
    assert_success("std cli facade source search fmt --check", &fmt);

    let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
    assert_success_stdout(check, "", "std cli facade source search check");

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(
        test,
        EXPECTED_STD_CLI_OUTPUT,
        "std cli facade source search test",
    );
}

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

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);
}
