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

const EXPECTED_STD_STRING_OUTPUT: &str = concat!(
    "test \"explicit std string len concat\" ... ok\n",
    "test \"explicit std string byte_at_result wrapper\" ... ok\n",
    "test \"explicit std string slice_result wrapper\" ... ok\n",
    "test \"explicit std string boundary wrappers\" ... ok\n",
    "test \"explicit std string parse result wrappers\" ... ok\n",
    "test \"explicit std string parse option wrappers\" ... ok\n",
    "test \"explicit std string parse integer fallbacks\" ... ok\n",
    "test \"explicit std string parse float bool fallbacks\" ... ok\n",
    "test \"explicit std string parse custom fallbacks\" ... ok\n",
    "test \"explicit std string search helpers\" ... ok\n",
    "test \"explicit std string ascii trim helpers\" ... ok\n",
    "test \"explicit std string helpers all\" ... ok\n",
    "12 test(s) passed\n",
);

const EXPECTED_STD_NUM_OUTPUT: &str = concat!(
    "test \"explicit std num widening\" ... ok\n",
    "test \"explicit std num checked conversions\" ... ok\n",
    "test \"explicit std num to string\" ... ok\n",
    "test \"explicit std num checked fallbacks\" ... ok\n",
    "test \"explicit std num helpers all\" ... ok\n",
    "5 test(s) passed\n",
);

#[test]
fn explicit_std_string_import_loads_repo_root_standard_source() {
    assert_core_facade_project(
        "string",
        &[
            "len",
            "concat",
            "byte_at_result",
            "slice_result",
            "starts_with",
            "ends_with",
            "contains",
            "index_of_option",
            "last_index_of_option",
            "trim_ascii_start",
            "trim_ascii_end",
            "trim_ascii",
            "parse_i32_result",
            "parse_i32_option",
            "parse_u32_result",
            "parse_u32_option",
            "parse_i64_result",
            "parse_i64_option",
            "parse_u64_result",
            "parse_u64_option",
            "parse_f64_result",
            "parse_f64_option",
            "parse_bool_result",
            "parse_bool_option",
            "parse_i32_or_zero",
            "parse_u32_or_zero",
            "parse_i64_or_zero",
            "parse_u64_or_zero",
            "parse_f64_or_zero",
            "parse_bool_or_false",
            "parse_i32_or",
            "parse_u32_or",
            "parse_i64_or",
            "parse_u64_or",
            "parse_f64_or",
            "parse_bool_or",
        ],
        EXPECTED_STD_STRING_OUTPUT,
    );
}

#[test]
fn explicit_std_num_import_loads_repo_root_standard_source() {
    assert_core_facade_project(
        "num",
        &[
            "i32_to_i64",
            "i32_to_f64",
            "i64_to_f64",
            "i64_to_i32_result",
            "f64_to_i32_result",
            "f64_to_i64_result",
            "i32_to_string",
            "u32_to_string",
            "i64_to_string",
            "u64_to_string",
            "f64_to_string",
            "i64_to_i32_or",
            "f64_to_i32_or",
            "f64_to_i64_or",
        ],
        EXPECTED_STD_NUM_OUTPUT,
    );
}

fn assert_core_facade_project(module: &str, helpers: &[&str], expected: &str) {
    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 module == "string" {
        assert!(
            slovo_source
                .contains("(import std.result (ok_or_none_i32 ok_or_none_u32 ok_or_none_i64 ok_or_none_u64 ok_or_none_f64 ok_or_none_bool))"),
            "std/string.slo must compose option helpers through the exp-109 result bridge helpers"
        );
    }
    for helper in helpers {
        assert!(
            slovo_source.contains(&format!("(fn {} ", helper)),
            "Slovo std/{}.slo is missing helper `{}`",
            module,
            helper
        );
    }

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

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

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