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

const EXPECTED_TEST_OUTPUT: &str = concat!(
    "test \"explicit local cli arg text missing\" ... ok\n",
    "test \"explicit local cli arg text option\" ... ok\n",
    "test \"explicit local cli arg i32 missing\" ... ok\n",
    "test \"explicit local cli arg i32 fallback missing\" ... ok\n",
    "test \"explicit local cli arg u32 missing\" ... ok\n",
    "test \"explicit local cli arg u32 fallback missing\" ... ok\n",
    "test \"explicit local cli arg i64 missing\" ... ok\n",
    "test \"explicit local cli arg i64 fallback missing\" ... ok\n",
    "test \"explicit local cli arg u64 missing\" ... ok\n",
    "test \"explicit local cli arg u64 fallback missing\" ... ok\n",
    "test \"explicit local cli arg f64 missing\" ... ok\n",
    "test \"explicit local cli arg f64 fallback missing\" ... ok\n",
    "test \"explicit local cli arg bool missing\" ... ok\n",
    "test \"explicit local cli arg bool fallback missing\" ... ok\n",
    "test \"explicit local cli typed option\" ... ok\n",
    "test \"explicit local cli typed custom fallback\" ... ok\n",
    "test \"explicit local cli facade all\" ... ok\n",
    "17 test(s) passed\n",
);

const STANDARD_CLI_SOURCE_FACADE_ALPHA: &[&str] = &[
    "arg_text_result",
    "arg_text_option",
    "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_RESULT_BRIDGE_HELPERS: &[&str] = &[
    "ok_or_none_i32",
    "ok_or_none_u32",
    "ok_or_none_i64",
    "ok_or_none_u64",
    "ok_or_none_string",
    "ok_or_none_f64",
    "ok_or_none_bool",
];

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

    assert_local_cli_fixture_is_source_authored(&project);

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

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

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

fn assert_local_cli_fixture_is_source_authored(project: &Path) {
    let cli = read(&project.join("src/cli.slo"));
    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!(
        cli.starts_with("(module cli (export "),
        "cli.slo must stay an explicit local module export"
    );
    assert!(
        process.starts_with("(module process (export "),
        "process.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!(
        string.starts_with("(module string (export "),
        "string.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import cli ("),
        "main.slo must stay an explicit local cli import"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "local cli fixture must not depend on automatic or package std imports"
    );
    assert!(
        cli.contains("(import process (arg_result))")
            && cli.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))"
            )
            && cli.contains(
                "(import string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))"
            )
            && !cli.contains("(import std.")
            && !main.contains("std."),
        "cli fixture must stay local while using local process, result, and parse helper imports"
    );

    assert!(
        !cli.contains("std."),
        "cli.slo must not use compiler-known std names directly"
    );

    let mut non_process_std = process.clone();
    for allowed in [
        "std.process.arg_result",
        "std.process.argc",
        "std.process.arg",
        "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_process_std = non_process_std.replace(allowed, "");
    }
    assert!(
        !non_process_std.contains("std."),
        "local process fixture must use only the existing promoted std.process runtime names"
    );

    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"
    );

    let mut non_result_std = result.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 result fixture must use only the existing promoted std.result runtime names"
    );

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

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

    for helper in LOCAL_RESULT_BRIDGE_HELPERS {
        assert!(
            result.contains(&format!("(fn {} ", helper)),
            "result.slo is missing local result bridge helper `{}`",
            helper
        );
    }

    assert!(
        process.contains("(fn arg_result "),
        "process.slo is missing local arg_result wrapper"
    );

    for helper in [
        "parse_i32_result",
        "parse_u32_result",
        "parse_i64_result",
        "parse_u64_result",
        "parse_f64_result",
        "parse_bool_result",
    ] {
        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);
}
