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

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

const STANDARD_IO_SOURCE_VALUE_HELPERS_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 standard_io_source_value_helper_project_checks_formats_and_tests() {
    let project =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-io");

    assert_local_io_fixture_is_source_authored(&project);

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

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

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

fn assert_local_io_fixture_is_source_authored(project: &Path) {
    let io_source = read(&project.join("src/io.slo"));
    let result_source = read(&project.join("src/result.slo"));
    let string_source = read(&project.join("src/string.slo"));
    let main = read(&project.join("src/main.slo"));

    assert!(
        io_source.starts_with("(module io (export "),
        "io.slo must stay an explicit local module export"
    );
    assert!(
        result_source.starts_with("(module result (export "),
        "result.slo must stay an explicit local module export"
    );
    assert!(
        string_source.starts_with("(module string (export "),
        "string.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import io ("),
        "main.slo must stay an explicit local io import"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "io fixture must not depend on automatic or package std imports"
    );

    let mut non_io_std = io_source.clone();
    for allowed in [
        "std.io.print_i32",
        "std.io.print_i64",
        "std.io.print_u32",
        "std.io.print_u64",
        "std.io.print_f64",
        "std.io.print_string",
        "std.io.print_bool",
        "std.io.read_stdin_result",
    ] {
        non_io_std = non_io_std.replace(allowed, "");
    }
    assert!(
        !non_io_std.contains("std.") && !main.contains("std."),
        "io fixture must use only the existing promoted std.io print and stdin-result runtime names"
    );
    assert!(
        io_source.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))",
        ) && io_source.contains(
            "(import string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))",
        ) && io_source.contains("(std.io.read_stdin_result)")
            && !io_source.contains("eprint")
            && !io_source.contains("read_line")
            && !io_source.contains("stream")
            && !io_source.contains("async")
            && !main.contains("eprint")
            && !main.contains("read_line")
            && !main.contains("stream")
            && !main.contains("async"),
        "io fixture must stay local while using only the released stdin-result lane and must not claim deferred io helpers"
    );

    let mut non_string_std = string_source.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 io string fixture must use only the promoted std.string runtime names"
    );

    let mut non_result_std = result_source.clone();
    for allowed in [
        "std.result.is_ok",
        "std.result.is_err",
        "std.result.unwrap_ok",
        "std.result.unwrap_err",
    ] {
        non_result_std = non_result_std.replace(allowed, "");
    }
    assert!(
        !non_result_std.contains("std."),
        "local io result fixture must use only the promoted std.result runtime names"
    );

    for helper in STANDARD_IO_SOURCE_VALUE_HELPERS_ALPHA {
        assert!(
            io_source.contains(&format!("(fn {} ", helper)),
            "io.slo is missing source helper `{}`",
            helper
        );
        assert!(
            main.contains(helper),
            "main.slo does not explicitly import/use `{}`",
            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);
}
