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

#[test]
fn numeric_struct_fields_alpha_fixture_emits_numeric_fields_and_tests_pass() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/numeric-struct-fields.slo");
    let compile = run_glagol([fixture.as_os_str()]);
    let stdout = String::from_utf8_lossy(&compile.stdout);
    let stderr = String::from_utf8_lossy(&compile.stderr);

    assert!(
        compile.status.success(),
        "compiler rejected numeric struct field fixture\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.contains("define { i64, double } @make_record")
            && stdout.contains("insertvalue { i64, double }")
            && stdout.contains("extractvalue { i64, double }")
            && stdout.contains("add i64")
            && stdout.contains("fadd double")
            && stdout.contains("call void @print_i64(i64")
            && stdout.contains("call void @print_f64(double"),
        "LLVM output did not contain expected numeric-in-struct aggregate shape\nstdout:\n{}",
        stdout
    );
    assert!(stderr.is_empty(), "stderr was not empty:\n{}", stderr);

    let run = run_glagol([OsStr::new("--run-tests"), fixture.as_os_str()]);
    assert_success_stdout(
        run,
        concat!(
            "test \"numeric struct i64 field access\" ... ok\n",
            "test \"numeric struct f64 field access\" ... ok\n",
            "test \"numeric struct i64 arithmetic after access\" ... ok\n",
            "test \"numeric struct f64 arithmetic after access\" ... ok\n",
            "test \"numeric struct i64 comparison after access\" ... ok\n",
            "test \"numeric struct f64 comparison after access\" ... ok\n",
            "test \"numeric struct local param return call flow\" ... ok\n",
            "test \"numeric struct i64 format after access\" ... ok\n",
            "test \"numeric struct f64 format after access\" ... ok\n",
            "9 test(s) passed\n",
        ),
    );
}

#[test]
fn numeric_struct_fields_alpha_formatter_and_lowering_are_visible() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/numeric-struct-fields.slo");

    let formatted = run_glagol([OsStr::new("--format"), fixture.as_os_str()]);
    assert_success_stdout(
        formatted,
        &std::fs::read_to_string(&fixture).expect("read numeric struct field fixture"),
    );

    let surface = run_glagol([
        OsStr::new("--inspect-lowering=surface"),
        fixture.as_os_str(),
    ]);
    assert_success_stdout(
        surface,
        &std::fs::read_to_string(
            Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../tests/numeric-struct-fields.surface.lower"),
        )
        .expect("read surface snapshot"),
    );

    let checked = run_glagol([
        OsStr::new("--inspect-lowering=checked"),
        fixture.as_os_str(),
    ]);
    assert_success_stdout(
        checked,
        &std::fs::read_to_string(
            Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../tests/numeric-struct-fields.checked.lower"),
        )
        .expect("read checked snapshot"),
    );
}

#[test]
fn f64_struct_fields_keep_non_finite_literals_rejected() {
    let fixture = write_fixture(
        "non-finite",
        r#"
(module main)

(struct Sample
  (value f64))

(fn main () -> i32
  (let sample Sample (Sample (value inf)))
  0)
"#,
    );
    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 non-finite f64 struct field literal\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert!(
        stdout.is_empty(),
        "rejected compile wrote stdout:\n{}",
        stdout
    );
    assert!(
        stderr.contains("UnsupportedFloatLiteral")
            && stderr.contains("f64 literals must be finite"),
        "non-finite literal rejection drifted\nstderr:\n{}",
        stderr
    );
}

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

fn glagol_bin() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_glagol"))
}

fn write_fixture(name: &str, contents: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    path.push(format!(
        "glagol-numeric-struct-fields-{}-{}.slo",
        name,
        std::process::id()
    ));
    std::fs::write(&path, contents).expect("write fixture");
    path
}

fn assert_success_stdout(output: Output, expected: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        output.status.success(),
        "command failed\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr
    );
    assert_eq!(stdout, expected);
    assert!(stderr.is_empty(), "stderr was not empty:\n{}", stderr);
}
