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

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

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

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

    assert_local_num_fixture_is_source_authored(&project);

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

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

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

fn assert_local_num_fixture_is_source_authored(project: &Path) {
    let num_source = read(&project.join("src/num.slo"));
    let main = read(&project.join("src/main.slo"));

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

    let mut non_num_std = num_source.clone();
    for allowed in [
        "std.num.i32_to_i64",
        "std.num.i32_to_f64",
        "std.num.i64_to_f64",
        "std.num.i64_to_i32_result",
        "std.num.f64_to_i32_result",
        "std.num.f64_to_i64_result",
        "std.num.i32_to_string",
        "std.num.u32_to_string",
        "std.num.i64_to_string",
        "std.num.u64_to_string",
        "std.num.f64_to_string",
    ] {
        non_num_std = non_num_std.replace(allowed, "");
    }
    assert!(
        !non_num_std.contains("std.") && !main.contains("std."),
        "num fixture must not introduce other compiler-known std names"
    );
    assert!(
        !num_source.contains("saturat")
            && !num_source.contains("round")
            && !num_source.contains("floor")
            && !num_source.contains("ceil")
            && !num_source.contains("generic")
            && !main.contains("saturat")
            && !main.contains("round")
            && !main.contains("floor")
            && !main.contains("ceil")
            && !main.contains("generic"),
        "num fixture must not claim deferred numeric conversion policies or generic abstractions"
    );

    for helper in STANDARD_NUM_SOURCE_FACADE_ALPHA {
        assert!(
            num_source.contains(&format!("(fn {} ", helper)),
            "num.slo is missing source facade `{}`",
            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);
}
