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

const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!(
    "test \"explicit local json quote escapes facade\" ... ok\n",
    "test \"explicit local json scalar values facade\" ... ok\n",
    "test \"explicit local json primitive scalar parse success facade\" ... ok\n",
    "test \"explicit local json primitive scalar parse failure facade\" ... ok\n",
    "test \"explicit local json string token parse success facade\" ... ok\n",
    "test \"explicit local json string token parse failure facade\" ... ok\n",
    "test \"explicit local json document parse trimmed success facade\" ... ok\n",
    "test \"explicit local json document parse plain success facade\" ... ok\n",
    "test \"explicit local json document parse trailing failure facade\" ... ok\n",
    "test \"explicit local json fields facade\" ... ok\n",
    "test \"explicit local json arrays objects facade\" ... ok\n",
    "test \"explicit local json facade all\" ... ok\n",
    "12 test(s) passed\n",
);

const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!(
    "test \"explicit std json quote escapes facade\" ... ok\n",
    "test \"explicit std json scalar values facade\" ... ok\n",
    "test \"explicit std json primitive scalar parse success facade\" ... ok\n",
    "test \"explicit std json primitive scalar parse failure facade\" ... ok\n",
    "test \"explicit std json string token parse success facade\" ... ok\n",
    "test \"explicit std json string token parse failure facade\" ... ok\n",
    "test \"explicit std json document parse trimmed success facade\" ... ok\n",
    "test \"explicit std json document parse plain success facade\" ... ok\n",
    "test \"explicit std json document parse trailing failure facade\" ... ok\n",
    "test \"explicit std json fields facade\" ... ok\n",
    "test \"explicit std json arrays objects facade\" ... ok\n",
    "test \"explicit std json facade all\" ... ok\n",
    "12 test(s) passed\n",
);

const STANDARD_JSON_SOURCE_FACADE_ALPHA: &[&str] = &[
    "quote_string",
    "null_value",
    "bool_value",
    "i32_value",
    "u32_value",
    "i64_value",
    "u64_value",
    "f64_value",
    "parse_string_value_result",
    "parse_bool_value_result",
    "parse_i32_value_result",
    "parse_u32_value_result",
    "parse_i64_value_result",
    "parse_u64_value_result",
    "parse_f64_value_result",
    "parse_null_value_result",
    "parse_string_document_result",
    "parse_bool_document_result",
    "parse_i32_document_result",
    "parse_u32_document_result",
    "parse_i64_document_result",
    "parse_u64_document_result",
    "parse_f64_document_result",
    "parse_null_document_result",
    "field_string",
    "field_bool",
    "field_i32",
    "field_u32",
    "field_i64",
    "field_u64",
    "field_f64",
    "field_null",
    "array0",
    "array1",
    "array2",
    "array3",
    "object0",
    "object1",
    "object2",
    "object3",
];

const STANDARD_JSON_RUNTIME_NAMES: &[&str] = &[
    "std.json.quote_string",
    "std.string.concat",
    "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",
    "std.json.parse_string_value_result",
    "std.json.parse_bool_value_result",
    "std.json.parse_i32_value_result",
    "std.json.parse_u32_value_result",
    "std.json.parse_i64_value_result",
    "std.json.parse_u64_value_result",
    "std.json.parse_f64_value_result",
];

const STANDARD_JSON_ALLOWED_STD_NAMES: &[&str] = &[
    "(import std.string (trim_ascii))",
    "std.json.quote_string",
    "std.string.concat",
    "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",
    "std.json.parse_string_value_result",
    "std.json.parse_bool_value_result",
    "std.json.parse_i32_value_result",
    "std.json.parse_u32_value_result",
    "std.json.parse_i64_value_result",
    "std.json.parse_u64_value_result",
    "std.json.parse_f64_value_result",
];

const STANDARD_JSON_DOCUMENT_SCALAR_BETA21: &[&str] = &[
    "parse_string_document_result",
    "parse_bool_document_result",
    "parse_i32_document_result",
    "parse_u32_document_result",
    "parse_i64_document_result",
    "parse_u64_document_result",
    "parse_f64_document_result",
    "parse_null_document_result",
];

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

    assert_local_json_fixture_is_source_authored(&project);

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

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

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

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

    assert_std_import_json_fixture_uses_repo_std(&project);

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

    let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
    assert_success_stdout(check, "", "std import json check");

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(
        test,
        EXPECTED_STD_IMPORT_TEST_OUTPUT,
        "std import json test output",
    );
}

fn assert_local_json_fixture_is_source_authored(project: &Path) {
    let json = read(&project.join("src/json.slo"));
    let main = read(&project.join("src/main.slo"));

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

fn assert_std_import_json_fixture_uses_repo_std(project: &Path) {
    let std_json = read(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/json.slo"));
    let main = read(&project.join("src/main.slo"));

    assert!(
        !project.join("src/json.slo").exists(),
        "std import json fixture must use repo-root std/json.slo, not a local copy"
    );
    assert!(
        main.starts_with("(module main)\n\n(import std.json ("),
        "std import json fixture must use explicit `std.json` import syntax"
    );
    assert_json_source_shape(&std_json, &main, "repo std.json fixture");
}

fn assert_json_source_shape(json: &str, main: &str, context: &str) {
    for runtime_name in STANDARD_JSON_RUNTIME_NAMES {
        assert!(
            json.contains(runtime_name),
            "{} must wrap or compose `{}`",
            context,
            runtime_name
        );
    }
    assert_std_only_contains(json, STANDARD_JSON_ALLOWED_STD_NAMES, context);
    assert_deferred_json_surface_absent(json, main, context);
    assert_json_document_scalar_helpers_are_source_authored(json, context);

    for helper in STANDARD_JSON_SOURCE_FACADE_ALPHA {
        assert!(
            json.contains(&format!("(fn {} ", helper)),
            "{} is missing source facade `{}`",
            context,
            helper
        );
        assert!(
            main.contains(helper),
            "{} main fixture import/use is missing `{}`",
            context,
            helper
        );
    }
}

fn assert_json_document_scalar_helpers_are_source_authored(json: &str, context: &str) {
    for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
        assert!(
            !json.contains(&format!("std.json.{}", helper)),
            "{} must keep `{}` source-authored, not compiler-known",
            context,
            helper
        );
    }
    for private_prefix in [
        "__glagol_json_parse_string_document",
        "__glagol_json_parse_bool_document",
        "__glagol_json_parse_i32_document",
        "__glagol_json_parse_u32_document",
        "__glagol_json_parse_i64_document",
        "__glagol_json_parse_u64_document",
        "__glagol_json_parse_f64_document",
        "__glagol_json_parse_null_document",
    ] {
        assert!(
            !json.contains(private_prefix),
            "{} must not introduce private JSON document runtime symbol `{}`",
            context,
            private_prefix
        );
    }
}

fn assert_deferred_json_surface_absent(json: &str, main: &str, context: &str) {
    for deferred in [
        "parse_object",
        "parse_array",
        "parse_value",
        "tokenize",
        "tokenizer",
        "schema",
        "stream",
        "unicode",
        "map",
    ] {
        assert!(
            !json.contains(deferred) && !main.contains(deferred),
            "{} must not claim deferred JSON `{}` policies",
            context,
            deferred
        );
    }
}

fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
    let mut remaining = source.to_string();
    for name in allowed {
        remaining = remaining.replace(name, "");
    }
    assert!(
        !remaining.contains("std."),
        "{} introduced unexpected compiler-known std names",
        context
    );
}

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