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

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

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

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

    assert_local_string_fixture_is_source_authored(&project);

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

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

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

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

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

    let mut non_string_std = string.clone();
    for allowed in [
        "std.string.parse_bool_result",
        "std.string.parse_f64_result",
        "std.string.parse_i64_result",
        "std.string.parse_u64_result",
        "std.string.parse_i32_result",
        "std.string.parse_u32_result",
        "std.string.byte_at_result",
        "std.string.slice_result",
        "std.string.starts_with",
        "std.string.ends_with",
        "std.string.concat",
        "std.string.len",
    ] {
        non_string_std = non_string_std.replace(allowed, "");
    }
    assert!(
        !non_string_std.contains("std.") && !main.contains("std."),
        "string fixture must use only the existing promoted std.string runtime names"
    );
    assert!(
        !string.contains("locale")
            && !string.contains("unicode")
            && !string.contains("bytes")
            && !string.contains("case_insensitive")
            && !string.contains("regex")
            && !string.contains("host_error"),
        "string fixture must not claim deferred parsing or richer error APIs"
    );

    assert!(
        string.contains(
            "(import 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))"
        ),
        "string fixture must explicitly import the local result bridge helpers"
    );

    for helper in [
        "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
        );
    }

    for helper in STANDARD_STRING_SOURCE_FALLBACK_HELPERS_ALPHA {
        assert!(
            string.contains(&format!("(fn {} ", helper)),
            "string.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);
}
