use std::{
    env,
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::atomic::{AtomicUsize, Ordering},
};

static NEXT_TEMP_ID: AtomicUsize = AtomicUsize::new(0);

const EXPECTED_TEST_OUTPUT: &str = concat!(
    "test \"explicit std json document scalar string\" ... ok\n",
    "test \"explicit std json document scalar bool\" ... ok\n",
    "test \"explicit std json document scalar integer\" ... ok\n",
    "test \"explicit std json document scalar float null\" ... ok\n",
    "test \"explicit std json document scalar failures\" ... ok\n",
    "test \"explicit std json document scalar all\" ... ok\n",
    "6 test(s) passed\n",
);

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 explicit_std_json_document_scalar_helpers_check_and_test() {
    let project = write_project(
        "std-json-document-scalar-beta21",
        r#"
(module main)

(import std.json (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))

(fn imported_json_document_string_ok () -> bool
  (if (= (std.result.unwrap_ok (parse_string_document_result "  \"slovo\"  ")) "slovo")
    (= (std.result.unwrap_ok (parse_string_document_result "\n\t\"slo\\\"vo\"\t")) "slo\"vo")
    false))

(fn imported_json_document_bool_ok () -> bool
  (if (std.result.unwrap_ok (parse_bool_document_result " true "))
    (= (std.result.unwrap_ok (parse_bool_document_result "\nfalse\t")) false)
    false))

(fn imported_json_document_integer_ok () -> bool
  (if (= (std.result.unwrap_ok (parse_i32_document_result " -7 ")) -7)
    (if (= (std.result.unwrap_ok (parse_u32_document_result "\n7\t")) 7u32)
      (if (= (std.result.unwrap_ok (parse_i64_document_result " -8 ")) -8i64)
        (= (std.result.unwrap_ok (parse_u64_document_result "9 ")) 9u64)
        false)
      false)
    false))

(fn imported_json_document_float_null_ok () -> bool
  (if (= (std.result.unwrap_ok (parse_f64_document_result " 1e2 ")) 100.0)
    (std.result.unwrap_ok (parse_null_document_result "\nnull\t"))
    false))

(fn imported_json_document_failures_ok () -> bool
  (if (= (std.result.unwrap_err (parse_string_document_result "\"slovo\" x")) 1)
    (if (= (std.result.unwrap_err (parse_bool_document_result " TRUE ")) 1)
      (if (= (std.result.unwrap_err (parse_i32_document_result " 01 ")) 1)
        (if (= (std.result.unwrap_err (parse_u32_document_result " -1 ")) 1)
          (if (= (std.result.unwrap_err (parse_i64_document_result " 8i64 ")) 1)
            (if (= (std.result.unwrap_err (parse_u64_document_result " ")) 1)
              (if (= (std.result.unwrap_err (parse_f64_document_result " 01.0 ")) 1)
                (= (std.result.unwrap_err (parse_null_document_result " NULL ")) 1)
                false)
              false)
            false)
          false)
        false)
      false)
    false))

(fn imported_json_document_scalar_all_ok () -> bool
  (if (imported_json_document_string_ok)
    (if (imported_json_document_bool_ok)
      (if (imported_json_document_integer_ok)
        (if (imported_json_document_float_null_ok)
          (imported_json_document_failures_ok)
          false)
        false)
      false)
    false))

(fn main () -> i32
  (if (imported_json_document_scalar_all_ok)
    42
    1))

(test "explicit std json document scalar string"
  (imported_json_document_string_ok))

(test "explicit std json document scalar bool"
  (imported_json_document_bool_ok))

(test "explicit std json document scalar integer"
  (imported_json_document_integer_ok))

(test "explicit std json document scalar float null"
  (imported_json_document_float_null_ok))

(test "explicit std json document scalar failures"
  (imported_json_document_failures_ok))

(test "explicit std json document scalar all"
  (= (main) 42))
"#,
    );

    let source = read(&project.join("src/main.slo"));
    let std_json = read(&std_json_path());

    assert!(
        !project.join("src/json.slo").exists(),
        "beta21 fixture must exercise repo-root std.json, not a local module copy"
    );
    assert!(
        source.starts_with("(module main)\n\n(import std.json ("),
        "beta21 fixture must use an explicit std.json import"
    );
    assert_json_document_scalar_helpers_are_source_authored(&std_json);

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

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

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(test, EXPECTED_TEST_OUTPUT, "std json document scalar test");
}

#[test]
fn json_document_scalar_helpers_are_not_compiler_known_runtime_calls() {
    let std_json = read(&std_json_path());
    assert_json_document_scalar_helpers_are_source_authored(&std_json);

    for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
        let fixture = write_fixture(
            helper,
            &format!(
                "(module main)\n\n(fn main () -> i32\n  (std.result.unwrap_err (std.json.{} \"invalid\")))\n",
                helper
            ),
        );
        let output = run_glagol([fixture.as_os_str()]);
        assert_failure_stderr_contains(
            &format!("direct std.json.{} runtime call", helper),
            &output,
            &format!(
                "standard library call `std.json.{}` is not supported",
                helper
            ),
        );
    }
}

fn assert_json_document_scalar_helpers_are_source_authored(std_json: &str) {
    assert!(
        std_json.starts_with("(module json (export "),
        "lib/std/json.slo must stay a source-authored module export"
    );

    for helper in STANDARD_JSON_DOCUMENT_SCALAR_BETA21 {
        assert!(
            std_json.contains(&format!("(fn {} ", helper)),
            "lib/std/json.slo is missing source facade `{}`",
            helper
        );
        assert!(
            !std_json.contains(&format!("std.json.{}", helper)),
            "std.json.{} must remain source-authored, not a compiler-known runtime call",
            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!(
            !std_json.contains(private_prefix),
            "lib/std/json.slo must not introduce private JSON document runtime symbol `{}`",
            private_prefix
        );
    }
}

fn run_glagol<I, S>(args: I) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    Command::new(env!("CARGO_BIN_EXE_glagol"))
        .args(args)
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run glagol")
}

fn write_project(name: &str, source: &str) -> PathBuf {
    let root = temp_root(name);
    let src = root.join("src");
    fs::create_dir_all(&src).unwrap_or_else(|err| panic!("create `{}`: {}", src.display(), err));
    fs::write(
        root.join("slovo.toml"),
        format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            name
        ),
    )
    .unwrap_or_else(|err| panic!("write project manifest: {}", err));
    fs::write(src.join("main.slo"), source.trim_start())
        .unwrap_or_else(|err| panic!("write project main.slo: {}", err));
    root
}

fn write_fixture(name: &str, source: &str) -> PathBuf {
    let mut path = env::temp_dir();
    path.push(format!(
        "glagol-standard-json-document-scalar-beta21-{}-{}-{}.slo",
        name,
        std::process::id(),
        NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
    ));
    fs::write(&path, source.trim_start())
        .unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}

fn temp_root(name: &str) -> PathBuf {
    let root = env::temp_dir().join(format!(
        "glagol-standard-json-document-scalar-beta21-{}-{}-{}",
        name,
        std::process::id(),
        NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed)
    ));
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(&root).unwrap_or_else(|err| panic!("create `{}`: {}", root.display(), err));
    root
}

fn std_json_path() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/json.slo")
}

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\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
        context,
        output.status.code(),
        stdout,
        stderr
    );
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    assert_success(context, &output);
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert_eq!(stdout, expected, "{}", context);
}

fn assert_failure_stderr_contains(context: &str, output: &Output, needle: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !output.status.success(),
        "{} unexpectedly passed\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert!(
        stdout.is_empty(),
        "{} rejected compile wrote stdout:\n{}",
        context,
        stdout
    );
    assert!(
        stderr.contains(needle),
        "{} stderr did not contain `{}`:\n{}",
        context,
        needle,
        stderr
    );
}
