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

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

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

const LOCAL_STRING_PARSE_HELPERS: &[&str] = &[
    "parse_i32_result",
    "parse_u32_result",
    "parse_i64_result",
    "parse_u64_result",
    "parse_f64_result",
    "parse_bool_result",
];

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

    assert_local_process_fixture_is_source_authored(&project);

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

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

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(
        test,
        EXPECTED_TEST_OUTPUT,
        "std layout local process test output",
    );
}

fn assert_local_process_fixture_is_source_authored(project: &Path) {
    let process = read(&project.join("src/process.slo"));
    let result = read(&project.join("src/result.slo"));
    let string = read(&project.join("src/string.slo"));
    let main = read(&project.join("src/main.slo"));

    assert!(
        process.starts_with("(module process (export "),
        "process.slo must stay an explicit local module export"
    );
    assert!(
        string.starts_with("(module string (export "),
        "string.slo must stay an explicit local module export"
    );
    assert!(
        result.starts_with("(module result (export "),
        "result.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import process ("),
        "main.slo must stay an explicit local process import"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "process fixture must not depend on automatic or package std imports"
    );
    assert!(
        process.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))")
            && process.contains("(import string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))")
            && !process.contains("std.string.")
            && !process.contains("(import std.")
            && !main.contains("std."),
        "process fixture must stay local while using local result and parse helper imports"
    );

    let mut non_process_std = process.clone();
    for allowed in [
        "std.process.arg_result",
        "std.process.argc",
        "std.process.arg",
    ] {
        non_process_std = non_process_std.replace(allowed, "");
    }
    assert!(
        !non_process_std.contains("std."),
        "process fixture must use only the existing promoted std.process runtime names directly"
    );

    let mut non_string_std = string.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 existing promoted std.string runtime names"
    );

    for helper in [
        "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",
    ] {
        assert!(
            result.contains(&format!("(fn {} ", helper)),
            "result.slo is missing local result bridge helper `{}`",
            helper
        );
    }

    assert!(
        !process.contains("spawn")
            && !process.contains("exit")
            && !process.contains("cwd")
            && !process.contains("signal")
            && !process.contains("shell")
            && !process.contains("flag"),
        "process fixture must not claim deferred process or CLI framework APIs"
    );

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

    for helper in LOCAL_STRING_PARSE_HELPERS {
        assert!(
            string.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>,
{
    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\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);
}
