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

const EXPECTED_STD_IO_OUTPUT: &str = concat!(
    "test \"explicit std io i64 zero facade\" ... ok\n",
    "test \"explicit std io u32 zero facade\" ... ok\n",
    "test \"explicit std io u64 zero facade\" ... ok\n",
    "test \"explicit std io f64 zero facade\" ... ok\n",
    "test \"explicit std io value facade\" ... ok\n",
    "test \"explicit std io stdin result facade\" ... ok\n",
    "test \"explicit std io stdin option facade\" ... ok\n",
    "test \"explicit std io stdin text fallback facade\" ... ok\n",
    "test \"explicit std io stdin typed result facade\" ... ok\n",
    "test \"explicit std io stdin typed option facade\" ... ok\n",
    "test \"explicit std io stdin typed fallback facade\" ... ok\n",
    "test \"explicit std io helpers all\" ... ok\n",
    "12 test(s) passed\n",
);

const STANDARD_IO_SOURCE_FACADE_ALPHA: &[&str] = &[
    "print_i32_zero",
    "print_i64_zero",
    "print_u32_zero",
    "print_u64_zero",
    "print_f64_zero",
    "print_string_zero",
    "print_bool_zero",
    "print_i32_value",
    "print_i64_value",
    "print_u32_value",
    "print_u64_value",
    "print_f64_value",
    "print_string_value",
    "print_bool_value",
    "read_stdin_result",
    "read_stdin_option",
    "read_stdin_or",
    "read_stdin_i32_result",
    "read_stdin_i32_option",
    "read_stdin_i32_or_zero",
    "read_stdin_i32_or",
    "read_stdin_u32_result",
    "read_stdin_u32_option",
    "read_stdin_u32_or_zero",
    "read_stdin_u32_or",
    "read_stdin_i64_result",
    "read_stdin_i64_option",
    "read_stdin_i64_or_zero",
    "read_stdin_i64_or",
    "read_stdin_u64_result",
    "read_stdin_u64_option",
    "read_stdin_u64_or_zero",
    "read_stdin_u64_or",
    "read_stdin_f64_result",
    "read_stdin_f64_option",
    "read_stdin_f64_or_zero",
    "read_stdin_f64_or",
    "read_stdin_bool_result",
    "read_stdin_bool_option",
    "read_stdin_bool_or_false",
    "read_stdin_bool_or",
];

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

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

    let slovo_source = read(&slovo_io);
    assert!(
        slovo_source.starts_with("(module io (export "),
        "repo-root Slovo std/io.slo must export imported helpers directly"
    );
    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_source.contains(
            "(import std.string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))",
        ) && slovo_source.contains("(std.io.read_stdin_result)"),
        "repo-root Slovo std/io.slo must use the promoted stdin result runtime plus standard source bridge imports"
    );
    for helper in STANDARD_IO_SOURCE_FACADE_ALPHA {
        assert!(
            slovo_source.contains(&format!("(fn {} ", helper)),
            "Slovo std/io.slo is missing helper `{}`",
            helper
        );
    }

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

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

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(
        test,
        EXPECTED_STD_IO_OUTPUT,
        "std io 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);
}
