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

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

const JSON_SCALAR_PARSE_NAMES: &[(&str, &str, &str)] = &[
    (
        "parse_bool_value_result",
        "__glagol_json_parse_bool_value_result",
        "(result bool i32)",
    ),
    (
        "parse_i32_value_result",
        "__glagol_json_parse_i32_value_result",
        "(result i32 i32)",
    ),
    (
        "parse_u32_value_result",
        "__glagol_json_parse_u32_value_result",
        "(result u32 i32)",
    ),
    (
        "parse_i64_value_result",
        "__glagol_json_parse_i64_value_result",
        "(result i64 i32)",
    ),
    (
        "parse_u64_value_result",
        "__glagol_json_parse_u64_value_result",
        "(result u64 i32)",
    ),
    (
        "parse_f64_value_result",
        "__glagol_json_parse_f64_value_result",
        "(result f64 i32)",
    ),
];

#[test]
fn json_scalar_parsers_lower_to_private_runtime_helpers() {
    let fixture = write_fixture(
        "lowering",
        r#"
(module main)

(fn main () -> i32
  (std.result.unwrap_ok (std.json.parse_bool_value_result "true"))
  (std.result.unwrap_ok (std.json.parse_i32_value_result "-0"))
  (std.result.unwrap_ok (std.json.parse_u32_value_result "4294967295"))
  (std.result.unwrap_ok (std.json.parse_i64_value_result "-9223372036854775808"))
  (std.result.unwrap_ok (std.json.parse_u64_value_result "18446744073709551615"))
  (std.result.unwrap_ok (std.json.parse_f64_value_result "1e2"))
  0)
"#,
    );
    let output = run_glagol([fixture.as_os_str()]);
    assert_success("compile json scalar parser lowering", &output);
    let stdout = String::from_utf8_lossy(&output.stdout);

    for (_, symbol, _) in JSON_SCALAR_PARSE_NAMES {
        assert!(
            stdout.contains(&format!("@{}", symbol)),
            "missing JSON scalar parser runtime symbol `{}`\nstdout:\n{}",
            symbol,
            stdout
        );
    }
    assert!(
        stdout.contains("declare i64 @__glagol_json_parse_i32_value_result(ptr)")
            && stdout.contains("declare i64 @__glagol_json_parse_u32_value_result(ptr)")
            && stdout.contains("declare i32 @__glagol_json_parse_i64_value_result(ptr, ptr)")
            && stdout.contains("declare i32 @__glagol_json_parse_u64_value_result(ptr, ptr)")
            && stdout.contains("declare i32 @__glagol_json_parse_f64_value_result(ptr, ptr)")
            && stdout.contains("declare i32 @__glagol_json_parse_bool_value_result(ptr, ptr)")
            && stdout.contains("call i64 @__glagol_json_parse_i32_value_result(")
            && stdout.contains("call i64 @__glagol_json_parse_u32_value_result(")
            && stdout.contains("call i32 @__glagol_json_parse_i64_value_result(")
            && stdout.contains("call i32 @__glagol_json_parse_u64_value_result(")
            && stdout.contains("call i32 @__glagol_json_parse_f64_value_result(")
            && stdout.contains("call i32 @__glagol_json_parse_bool_value_result(")
            && !stdout.contains("@std.json.parse_i32_value_result"),
        "JSON scalar parser LLVM shape drifted\nstdout:\n{}",
        stdout
    );
}

#[test]
fn test_runner_enforces_json_token_scalar_contract() {
    let fixture = write_fixture(
        "test-runner",
        r#"
(module main)

(test "json bool true ok"
  (std.result.unwrap_ok (std.json.parse_bool_value_result "true")))

(test "json bool uppercase err"
  (= (std.result.unwrap_err (std.json.parse_bool_value_result "TRUE")) 1))

(test "json i32 negative zero ok"
  (= (std.result.unwrap_ok (std.json.parse_i32_value_result "-0")) 0))

(test "json i32 leading zero err"
  (= (std.result.unwrap_err (std.json.parse_i32_value_result "01")) 1))

(test "json i32 plus err"
  (= (std.result.unwrap_err (std.json.parse_i32_value_result "+1")) 1))

(test "json u32 negative err"
  (= (std.result.unwrap_err (std.json.parse_u32_value_result "-1")) 1))

(test "json i64 max ok"
  (= (std.result.unwrap_ok (std.json.parse_i64_value_result "9223372036854775807")) 9223372036854775807i64))

(test "json u64 max ok"
  (= (std.result.unwrap_ok (std.json.parse_u64_value_result "18446744073709551615")) 18446744073709551615u64))

(test "json f64 exponent ok"
  (= (std.result.unwrap_ok (std.json.parse_f64_value_result "1e2")) 100.0))

(test "json f64 leading zero err"
  (= (std.result.unwrap_err (std.json.parse_f64_value_result "01.0")) 1))

(test "json f64 nonfinite err"
  (= (std.result.unwrap_err (std.json.parse_f64_value_result "1e309")) 1))

(test "json no leading whitespace"
  (= (std.result.unwrap_err (std.json.parse_i64_value_result " 1")) 1))

(test "json no trailing whitespace"
  (= (std.result.unwrap_err (std.json.parse_f64_value_result "1 ")) 1))
"#,
    );
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run json scalar parser tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"json bool true ok\" ... ok\n",
            "test \"json bool uppercase err\" ... ok\n",
            "test \"json i32 negative zero ok\" ... ok\n",
            "test \"json i32 leading zero err\" ... ok\n",
            "test \"json i32 plus err\" ... ok\n",
            "test \"json u32 negative err\" ... ok\n",
            "test \"json i64 max ok\" ... ok\n",
            "test \"json u64 max ok\" ... ok\n",
            "test \"json f64 exponent ok\" ... ok\n",
            "test \"json f64 leading zero err\" ... ok\n",
            "test \"json f64 nonfinite err\" ... ok\n",
            "test \"json no leading whitespace\" ... ok\n",
            "test \"json no trailing whitespace\" ... ok\n",
            "13 test(s) passed\n",
        ),
        "json scalar parser test runner stdout drifted"
    );
}

#[test]
fn hosted_json_scalar_parsers_smoke_when_toolchain_is_available() {
    let fixture = write_fixture(
        "runtime-smoke",
        r#"
(module main)

(fn main () -> i32
  (std.io.print_bool (std.result.unwrap_ok (std.json.parse_bool_value_result "true")))
  (std.io.print_i32 (std.result.unwrap_ok (std.json.parse_i32_value_result "-0")))
  (std.io.print_u32 (std.result.unwrap_ok (std.json.parse_u32_value_result "4294967295")))
  (std.io.print_i64 (std.result.unwrap_ok (std.json.parse_i64_value_result "-9223372036854775808")))
  (std.io.print_u64 (std.result.unwrap_ok (std.json.parse_u64_value_result "18446744073709551615")))
  (std.io.print_string (std.num.f64_to_string (std.result.unwrap_ok (std.json.parse_f64_value_result "1e2"))))
  (std.result.unwrap_err (std.json.parse_i32_value_result "01")))
"#,
    );
    let binary = unique_path("json-scalar-parsing-beta17-bin");
    let build = run_glagol([
        OsStr::new("build"),
        fixture.as_os_str(),
        OsStr::new("-o"),
        binary.as_os_str(),
    ]);
    if !build.status.success() {
        let stderr = String::from_utf8_lossy(&build.stderr);
        assert!(
            stderr.contains("ToolchainUnavailable"),
            "json scalar parser build failed unexpectedly\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&build.stdout),
            stderr
        );
        return;
    }

    let run = Command::new(&binary)
        .output()
        .unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
    assert_eq!(
        run.status.code(),
        Some(1),
        "json scalar parser binary exit code drifted\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&run.stdout),
        String::from_utf8_lossy(&run.stderr)
    );
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        concat!(
            "true\n",
            "0\n",
            "4294967295\n",
            "-9223372036854775808\n",
            "18446744073709551615\n",
            "100.0\n",
        ),
        "json scalar parser binary stdout drifted"
    );
    assert!(
        run.stderr.is_empty(),
        "json scalar parser binary wrote stderr:\n{}",
        String::from_utf8_lossy(&run.stderr)
    );
}

#[test]
fn json_scalar_parser_diagnostics_cover_promoted_names_and_shadowing() {
    for (name, _, return_type) in JSON_SCALAR_PARSE_NAMES {
        let arity = write_fixture(
            &format!("{name}-arity"),
            &format!(
                "(module main)\n\n(fn main () -> {}\n  (std.json.{}))\n",
                return_type, name
            ),
        );
        assert_rejected(
            &format!("std.json.{name} arity"),
            run_glagol([arity.as_os_str()]),
            "wrong number of arguments",
        );

        let type_mismatch = write_fixture(
            &format!("{name}-type"),
            &format!(
                "(module main)\n\n(fn main () -> {}\n  (std.json.{} 1))\n",
                return_type, name
            ),
        );
        assert_rejected(
            &format!("std.json.{name} type"),
            run_glagol([type_mismatch.as_os_str()]),
            &format!(
                "cannot call `std.json.{}` with argument of wrong type",
                name
            ),
        );
    }

    let source_shadow = write_fixture(
        "source-shadow",
        r#"
(module main)

(fn std.json.parse_i32_value_result ((text string)) -> (result i32 i32)
  (err i32 i32 1))

(fn main () -> i32
  0)
"#,
    );
    assert_rejected(
        "std.json.parse_i32_value_result source shadow",
        run_glagol([source_shadow.as_os_str()]),
        "DuplicateFunction",
    );

    let helper_shadow = write_fixture(
        "helper-shadow",
        r#"
(module main)

(fn __glagol_json_parse_i32_value_result ((text string)) -> (result i32 i32)
  (err i32 i32 1))

(fn main () -> i32
  0)
"#,
    );
    assert_rejected(
        "__glagol_json_parse_i32_value_result helper shadow",
        run_glagol([helper_shadow.as_os_str()]),
        "DuplicateFunction",
    );
}

#[test]
fn deferred_json_parser_families_remain_unsupported() {
    for name in [
        "parse_object_result",
        "parse_array_result",
        "parse_value_result",
        "tokenize_result",
        "schema_validate_result",
        "stream_parse_result",
    ] {
        let fixture = write_fixture(
            name,
            &format!(
                "(module main)\n\n(fn main () -> i32\n  (std.json.{} \"[]\")\n  0)\n",
                name
            ),
        );
        assert_rejected(
            &format!("std.json.{name} deferred"),
            run_glagol([fixture.as_os_str()]),
            &format!("standard library call `std.json.{}` is not supported", name),
        );
    }
}

#[test]
fn unsupported_json_diagnostics_list_beta17_promoted_scalar_parsers() {
    let fixture = write_fixture(
        "unsupported-guidance",
        r#"
(module main)

(fn main () -> i32
  (std.json.parse_value_result "null")
  0)
"#,
    );
    let output = run_glagol([fixture.as_os_str()]);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !output.status.success(),
        "unsupported JSON value parser unexpectedly compiled\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        stderr
    );
    assert!(
        stderr.contains("standard library call `std.json.parse_value_result` is not supported"),
        "unsupported JSON parser diagnostic drifted\nstderr:\n{}",
        stderr
    );
    for (name, _, _) in JSON_SCALAR_PARSE_NAMES {
        let promoted = format!("std.json.{name}");
        assert!(
            stderr.contains(&promoted),
            "unsupported std guidance omitted promoted beta17 name `{}`\nstderr:\n{}",
            promoted,
            stderr
        );
    }
}

fn write_fixture(name: &str, source: &str) -> PathBuf {
    let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
    let dir = env::temp_dir().join(format!("glagol-json-scalar-beta17-{id}-{name}"));
    fs::create_dir_all(&dir).unwrap_or_else(|err| panic!("create `{}`: {}", dir.display(), err));
    let path = dir.join("main.slo");
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}

fn unique_path(name: &str) -> PathBuf {
    let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
    env::temp_dir().join(format!("glagol-{name}-{id}{}", env::consts::EXE_SUFFIX))
}

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 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_rejected(context: &str, output: Output, expected: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !output.status.success(),
        "{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert!(
        stdout.is_empty(),
        "{} rejected compile wrote stdout:\n{}",
        context,
        stdout
    );
    assert!(
        stderr.contains(expected),
        "{} diagnostic drifted; expected `{}`\nstderr:\n{}",
        context,
        expected,
        stderr
    );
}
