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

const EXPECTED_TEST_OUTPUT: &str = concat!(
    "test \"local math i32 helpers\" ... ok\n",
    "test \"local math i64 helpers\" ... ok\n",
    "test \"local math f64 helpers\" ... ok\n",
    "test \"explicit local math import i32 helpers\" ... ok\n",
    "test \"explicit local math import i64 helpers\" ... ok\n",
    "test \"explicit local math import f64 helpers\" ... ok\n",
    "test \"explicit local math import all helpers\" ... ok\n",
    "7 test(s) passed\n",
);

const STANDARD_MATH_HELPERS_ALPHA: &[&str] = &[
    "abs_i32",
    "neg_i32",
    "rem_i32",
    "bit_and_i32",
    "bit_or_i32",
    "bit_xor_i32",
    "is_even_i32",
    "is_odd_i32",
    "min_i32",
    "max_i32",
    "clamp_i32",
    "square_i32",
    "cube_i32",
    "is_zero_i32",
    "is_positive_i32",
    "is_negative_i32",
    "in_range_i32",
    "abs_i64",
    "neg_i64",
    "rem_i64",
    "bit_and_i64",
    "bit_or_i64",
    "bit_xor_i64",
    "is_even_i64",
    "is_odd_i64",
    "min_i64",
    "max_i64",
    "clamp_i64",
    "square_i64",
    "cube_i64",
    "is_zero_i64",
    "is_positive_i64",
    "is_negative_i64",
    "in_range_i64",
    "abs_f64",
    "neg_f64",
    "min_f64",
    "max_f64",
    "clamp_f64",
    "square_f64",
    "cube_f64",
    "is_zero_f64",
    "is_positive_f64",
    "is_negative_f64",
    "in_range_f64",
];

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

    assert_local_math_fixture_is_source_authored(&project);

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

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

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

fn assert_local_math_fixture_is_source_authored(project: &Path) {
    let math = read(&project.join("src/math.slo"));
    let main = read(&project.join("src/main.slo"));
    let math_export_line = math.lines().next().unwrap_or_default();

    assert!(
        math.starts_with("(module math (export "),
        "math.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import math ("),
        "main.slo must stay an explicit local math import"
    );
    assert!(
        !math.contains("std.") && !main.contains("std."),
        "exp-32 fixture must not use compiler-known std runtime names"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "exp-32 fixture must not depend on automatic or package std imports"
    );

    for helper in STANDARD_MATH_HELPERS_ALPHA {
        assert!(
            math.contains(&format!("(fn {} ", helper)),
            "math.slo is missing source helper `{}`",
            helper
        );
        assert!(
            math_export_line.contains(helper),
            "math.slo does not explicitly export `{}`",
            helper
        );
        assert!(
            main.matches(helper).count() >= 2,
            "main.slo does not explicitly import and 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);
}
