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

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

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

(fn main () -> i32
  (if (= (std.json.quote_string "slo\"vo") "\"slo\\\"vo\"")
    0
    1))
"#,
    );
    let output = run_glagol([fixture.as_os_str()]);
    assert_success("compile standard json lowering", &output);
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(
        stdout.contains("declare ptr @__glagol_json_quote_string(ptr)")
            && stdout.contains("call ptr @__glagol_json_quote_string(")
            && !stdout.contains("@std.json.quote_string"),
        "standard json LLVM shape drifted\nstdout:\n{}",
        stdout
    );
}

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

(test "quote plain string"
  (= (std.json.quote_string "slovo") "\"slovo\""))

(test "quote embedded quote"
  (= (std.json.quote_string "slo\"vo") "\"slo\\\"vo\""))

(test "quote backslash"
  (= (std.json.quote_string "slo\\vo") "\"slo\\\\vo\""))

(test "quote newline tab"
  (= (std.json.quote_string "line\n\tnext") "\"line\\n\\tnext\""))
"#,
    );
    let output = run_glagol([OsStr::new("test"), fixture.as_os_str()]);
    assert_success("run standard json tests", &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        concat!(
            "test \"quote plain string\" ... ok\n",
            "test \"quote embedded quote\" ... ok\n",
            "test \"quote backslash\" ... ok\n",
            "test \"quote newline tab\" ... ok\n",
            "4 test(s) passed\n",
        ),
        "standard json test runner stdout drifted"
    );
}

#[test]
fn standard_json_diagnostics_cover_promoted_and_deferred_names() {
    let cases = [
        (
            "quote-arity",
            r#"
(module main)

(fn main () -> i32
  (std.json.quote_string))
"#,
            "ArityMismatch",
        ),
        (
            "quote-type",
            r#"
(module main)

(fn main () -> i32
  (std.json.quote_string 42)
  0)
"#,
            "TypeMismatch",
        ),
        (
            "parse-object-deferred",
            r#"
(module main)

(fn main () -> i32
  (std.json.parse_object_result "{}"))
"#,
            "UnsupportedStandardLibraryCall",
        ),
        (
            "promoted-shadow",
            r#"
(module main)

(fn std.json.quote_string ((value string)) -> string
  value)

(fn main () -> i32
  (if (= (std.json.quote_string "x") "\"x\"") 0 1))
"#,
            "DuplicateFunction",
        ),
    ];

    for (name, source, diagnostic) in cases {
        let fixture = write_fixture(name, source);
        let output = run_glagol([fixture.as_os_str()]);
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "compiler unexpectedly accepted `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr
        );
        assert!(
            stdout.is_empty(),
            "rejected compile wrote stdout:\n{}",
            stdout
        );
        assert!(
            stderr.contains(diagnostic),
            "diagnostic `{}` was not reported for `{}`\nstderr:\n{}",
            diagnostic,
            name,
            stderr
        );
    }
}

#[test]
fn hosted_json_quote_smoke_when_clang_is_available() {
    if !clang_is_available() {
        eprintln!("skipping standard json runtime smoke: set GLAGOL_CLANG or install clang");
        return;
    }

    let fixture = write_fixture(
        "hosted",
        r#"
(module main)

(fn main () -> i32
  (if (= (std.json.quote_string "line\nnext") "\"line\\nnext\"")
    0
    1))
"#,
    );
    let binary = fixture.with_extension(env::consts::EXE_EXTENSION);
    let build = run_glagol([
        OsStr::new("build"),
        fixture.as_os_str(),
        OsStr::new("-o"),
        binary.as_os_str(),
    ]);
    assert_success("build standard json hosted smoke", &build);

    let run = Command::new(&binary)
        .output()
        .unwrap_or_else(|err| panic!("run `{}`: {}", binary.display(), err));
    assert_success("run standard json hosted smoke", &run);
}

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-standard-json-{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 clang_is_available() -> bool {
    if env::var_os("GLAGOL_CLANG").is_some() {
        return true;
    }
    Command::new("clang")
        .arg("--version")
        .output()
        .is_ok_and(|output| output.status.success())
}

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