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

const MISSING_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_UNLIKELY_MISSING";
const PRESENT_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT";
const PRESENT_ENV_VALUE: &str = "glagol-std-layout-local-env-alpha-value";
const PRESENT_I32_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_I32";
const PRESENT_I32_ENV_VALUE: &str = "42";
const PRESENT_U32_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_U32";
const PRESENT_U32_ENV_VALUE: &str = "42";
const PRESENT_I64_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_I64";
const PRESENT_I64_ENV_VALUE: &str = "42000000000";
const PRESENT_U64_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_U64";
const PRESENT_U64_ENV_VALUE: &str = "4294967296";
const PRESENT_F64_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_F64";
const PRESENT_F64_ENV_VALUE: &str = "42.5";
const PRESENT_BOOL_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_PRESENT_BOOL";
const PRESENT_BOOL_ENV_VALUE: &str = "true";
const INVALID_ENV_NAME: &str = "GLAGOL_STD_LAYOUT_LOCAL_ENV_ALPHA_INVALID";
const INVALID_ENV_VALUE: &str = "bad";

const EXPECTED_TEST_OUTPUT: &str = concat!(
    "test \"explicit local env get missing facade\" ... ok\n",
    "test \"explicit local env get result missing facade\" ... ok\n",
    "test \"explicit local env has missing facade\" ... ok\n",
    "test \"explicit local env get or missing facade\" ... ok\n",
    "test \"explicit local env get option missing facade\" ... ok\n",
    "test \"explicit local env has present facade\" ... ok\n",
    "test \"explicit local env get or present facade\" ... ok\n",
    "test \"explicit local env get option present facade\" ... ok\n",
    "test \"explicit local env get i32 result present facade\" ... ok\n",
    "test \"explicit local env get i32 or zero invalid facade\" ... ok\n",
    "test \"explicit local env get u32 result present facade\" ... ok\n",
    "test \"explicit local env get u32 or zero invalid facade\" ... ok\n",
    "test \"explicit local env get i64 result present facade\" ... ok\n",
    "test \"explicit local env get i64 or zero missing facade\" ... ok\n",
    "test \"explicit local env get u64 result present facade\" ... ok\n",
    "test \"explicit local env get u64 or zero missing facade\" ... ok\n",
    "test \"explicit local env get f64 result present facade\" ... ok\n",
    "test \"explicit local env get f64 or zero invalid facade\" ... ok\n",
    "test \"explicit local env get bool result present facade\" ... ok\n",
    "test \"explicit local env get bool or false invalid facade\" ... ok\n",
    "test \"explicit local env typed option facade\" ... ok\n",
    "test \"explicit local env typed custom fallback facade\" ... ok\n",
    "test \"explicit local env facade all\" ... ok\n",
    "23 test(s) passed\n",
);

const STANDARD_ENV_SOURCE_FACADE_ALPHA: &[&str] = &[
    "get",
    "get_result",
    "get_option",
    "has",
    "get_or",
    "get_i32_result",
    "get_i32_option",
    "get_i32_or_zero",
    "get_i32_or",
    "get_u32_result",
    "get_u32_option",
    "get_u32_or_zero",
    "get_u32_or",
    "get_i64_result",
    "get_i64_option",
    "get_i64_or_zero",
    "get_i64_or",
    "get_u64_result",
    "get_u64_option",
    "get_u64_or_zero",
    "get_u64_or",
    "get_f64_result",
    "get_f64_option",
    "get_f64_or_zero",
    "get_f64_or",
    "get_bool_result",
    "get_bool_option",
    "get_bool_or_false",
    "get_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",
];

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

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

    assert_local_env_fixture_is_source_authored(&project);

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

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

    let test = run_glagol_configured([OsStr::new("test"), project.as_os_str()], |command| {
        configure_local_env_contract(command);
    });
    assert_success_stdout(
        test,
        EXPECTED_TEST_OUTPUT,
        "std layout local env test output",
    );
}

fn assert_local_env_fixture_is_source_authored(project: &Path) {
    let env = read(&project.join("src/env.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!(
        env.starts_with("(module env (export "),
        "env.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!(
        result.starts_with("(module result (export "),
        "result.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import env ("),
        "main.slo must stay an explicit local env import"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "env fixture must not depend on automatic or package std imports"
    );
    assert!(
        env.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))")
            && env.contains("(import string (parse_i32_result parse_u32_result parse_i64_result parse_u64_result parse_f64_result parse_bool_result))")
            && env.contains("(std.env.get name)")
            && env.contains("(std.env.get_result name)"),
        "env.slo must stay a local source facade over env lookup plus local string and result bridge helpers"
    );
    assert!(
        !env.contains("std.string.") && !main.contains("std.") && !env.contains("(import std."),
        "env.slo and main.slo must stay local and must not depend on repo std imports"
    );

    let mut non_string_std = string.clone();
    for allowed in LOCAL_STRING_STD_NAMES {
        non_string_std = non_string_std.replace(allowed, "");
    }
    assert!(
        !non_string_std.contains("std."),
        "local string fixture must use only the 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 promoted std.result runtime names"
    );
    assert!(
        !env.contains("set")
            && !env.contains("unset")
            && !env.contains("enumer")
            && !env.contains("platform")
            && !env.contains("host_error")
            && !env.contains("errno"),
        "env fixture must not claim deferred mutation, enumeration, or rich host error APIs"
    );
    assert!(
        main.contains(MISSING_ENV_NAME)
            && main.contains(PRESENT_ENV_NAME)
            && main.contains(PRESENT_ENV_VALUE)
            && main.contains(PRESENT_I32_ENV_NAME)
            && main.contains(PRESENT_U32_ENV_NAME)
            && main.contains(PRESENT_I64_ENV_NAME)
            && main.contains(PRESENT_U64_ENV_NAME)
            && main.contains(PRESENT_F64_ENV_NAME)
            && main.contains(PRESENT_BOOL_ENV_NAME)
            && main.contains(INVALID_ENV_NAME),
        "main.slo must use deterministic missing, present, and invalid env names"
    );

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

    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 configure_local_env_contract(command: &mut Command) {
    command
        .env_remove(MISSING_ENV_NAME)
        .env(PRESENT_ENV_NAME, PRESENT_ENV_VALUE)
        .env(PRESENT_I32_ENV_NAME, PRESENT_I32_ENV_VALUE)
        .env(PRESENT_U32_ENV_NAME, PRESENT_U32_ENV_VALUE)
        .env(PRESENT_I64_ENV_NAME, PRESENT_I64_ENV_VALUE)
        .env(PRESENT_U64_ENV_NAME, PRESENT_U64_ENV_VALUE)
        .env(PRESENT_F64_ENV_NAME, PRESENT_F64_ENV_VALUE)
        .env(PRESENT_BOOL_ENV_NAME, PRESENT_BOOL_ENV_VALUE)
        .env(INVALID_ENV_NAME, INVALID_ENV_VALUE);
}

fn run_glagol<I, S>(args: I) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    run_glagol_configured(args, |_| {})
}

fn run_glagol_configured<I, S, F>(args: I, configure: F) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
    F: FnOnce(&mut Command),
{
    let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
    command
        .args(args)
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")));
    configure(&mut command);
    command.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);
}
