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

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

#[test]
fn exp14_project_exercises_released_standard_runtime_surface() {
    let project = write_conformance_project();

    let fmt_check = run_glagol(["fmt".as_ref(), "--check".as_ref(), project.as_os_str()]);
    assert_success("exp-14 project fmt --check", &fmt_check);

    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success("exp-14 project check", &check);

    let test = run_glagol(["test".as_ref(), project.as_os_str()]);
    assert_success("exp-14 project test", &test);
    assert_eq!(
        String::from_utf8_lossy(&test.stdout),
        concat!(
            "test \"provider imported base\" ... ok\n",
            "test \"concat parse result match\" ... ok\n",
            "test \"vec i32 imported module value\" ... ok\n",
            "test \"vec i64 imported runtime value\" ... ok\n",
            "test \"payloadless enum match\" ... ok\n",
            "test \"time sleep zero preserves score\" ... ok\n",
            "6 test(s) passed\n",
        )
    );

    let docs = unique_path("docs");
    let doc = run_glagol([
        "doc".as_ref(),
        project.as_os_str(),
        "-o".as_ref(),
        docs.as_os_str(),
    ]);
    assert_success("exp-14 project docs", &doc);
    let doc_index = fs::read_to_string(docs.join("index.md")).expect("read exp-14 docs");
    assert!(doc_index.contains("## Module math"));
    assert!(doc_index.contains("## Module main"));
    assert!(doc_index.contains("- `main"));
    assert!(doc_index.contains("- `concat parse result match`"));

    let binary = unique_path("bin");
    let build = run_glagol([
        "build".as_ref(),
        project.as_os_str(),
        "-o".as_ref(),
        binary.as_os_str(),
    ]);
    if build.status.success() {
        let run = Command::new(&binary).output().expect("run exp-14 binary");
        assert_success("exp-14 project binary", &run);
        assert_eq!(String::from_utf8_lossy(&run.stdout), "85\n");
        assert!(
            run.stderr.is_empty(),
            "exp-14 project binary wrote stderr:\n{}",
            String::from_utf8_lossy(&run.stderr)
        );
    } else {
        assert!(
            build.stdout.is_empty(),
            "failed exp-14 build wrote stdout:\n{}",
            String::from_utf8_lossy(&build.stdout)
        );
        assert_stderr_contains("exp-14 project build", &build, "ToolchainUnavailable");
    }
}

#[test]
fn conformance_gate_script_exists_and_names_required_commands() {
    let script = Path::new("../scripts/conformance-gate.sh");
    let text = fs::read_to_string(script).expect("read conformance gate script");

    assert!(text.starts_with("#!/usr/bin/env bash\nset -euo pipefail\n"));
    assert!(text.contains("scripts/release-gate.sh remains the full"));
    assert!(text.contains("git -C \"${repo_root}\" diff --check"));
    assert!(text.contains("cargo fmt --check"));
    assert!(text.contains("cargo test --test standard_runtime_conformance_alignment"));
    assert!(text.contains("cargo test --test standard_time"));
    assert!(text.contains("cargo test --test promotion_gate promotion_gate_artifacts_are_aligned"));
}

fn write_conformance_project() -> PathBuf {
    let project = unique_path("project");
    fs::create_dir_all(project.join("src")).expect("create exp-14 project src");
    fs::write(
        project.join("slovo.toml"),
        format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            project.file_name().unwrap().to_string_lossy()
        ),
    )
    .expect("write exp-14 manifest");
    fs::write(
        project.join("src/math.slo"),
        concat!(
            "(module math (export imported_base))\n",
            "\n",
            "(fn imported_base () -> i32\n",
            "  20)\n",
            "\n",
            "(test \"provider imported base\"\n",
            "  (= (imported_base) 20))\n",
        ),
    )
    .expect("write exp-14 math module");
    fs::write(
        project.join("src/main.slo"),
        concat!(
            "(module main)\n",
            "\n",
            "(import math (imported_base))\n",
            "\n",
            "(enum Signal Red Green)\n",
            "\n",
            "(fn concat_digits () -> string\n",
            "  (std.string.concat \"4\" \"2\"))\n",
            "\n",
            "(fn parse_digits () -> (result i32 i32)\n",
            "  (std.string.parse_i32_result (concat_digits)))\n",
            "\n",
            "(fn parsed_or_code () -> i32\n",
            "  (match (parse_digits)\n",
            "    ((ok value)\n",
            "      value)\n",
            "    ((err code)\n",
            "      code)))\n",
            "\n",
            "(fn pair () -> (vec i32)\n",
            "  (let values (vec i32) (std.vec.i32.empty))\n",
            "  (let first (vec i32) (std.vec.i32.append values (imported_base)))\n",
            "  (std.vec.i32.append first (parsed_or_code)))\n",
            "\n",
            "(fn pair_i64 () -> (vec i64)\n",
            "  (let values (vec i64) (std.vec.i64.empty))\n",
            "  (let first (vec i64) (std.vec.i64.append values 40i64))\n",
            "  (std.vec.i64.append first 41i64))\n",
            "\n",
            "(fn vec_i64_bonus () -> i32\n",
            "  (match (std.num.i64_to_i32_result (std.vec.i64.index (pair_i64) 1))\n",
            "    ((ok value)\n",
            "      value)\n",
            "    ((err code)\n",
            "      code)))\n",
            "\n",
            "(fn signal_code ((signal Signal)) -> i32\n",
            "  (match signal\n",
            "    ((Signal.Red)\n",
            "      1)\n",
            "    ((Signal.Green)\n",
            "      2)))\n",
            "\n",
            "(fn score () -> i32\n",
            "  (+ (+ (std.vec.i32.index (pair) 1) (signal_code (Signal.Green))) (vec_i64_bonus)))\n",
            "\n",
            "(fn sleep_zero_then_score () -> i32\n",
            "  (std.time.sleep_ms 0)\n",
            "  (score))\n",
            "\n",
            "(test \"concat parse result match\"\n",
            "  (= (parsed_or_code) 42))\n",
            "\n",
            "(test \"vec i32 imported module value\"\n",
            "  (= (std.vec.i32.index (pair) 0) 20))\n",
            "\n",
            "(test \"vec i64 imported runtime value\"\n",
            "  (= (vec_i64_bonus) 41))\n",
            "\n",
            "(test \"payloadless enum match\"\n",
            "  (= (signal_code (Signal.Green)) 2))\n",
            "\n",
            "(test \"time sleep zero preserves score\"\n",
            "  (= (sleep_zero_then_score) 85))\n",
            "\n",
            "(fn main () -> i32\n",
            "  (std.io.print_i32 (sleep_zero_then_score))\n",
            "  0)\n",
        ),
    )
    .expect("write exp-14 main module");

    project
}

fn unique_path(name: &str) -> PathBuf {
    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("system clock before UNIX_EPOCH")
        .as_nanos();
    std::env::temp_dir().join(format!(
        "glagol-exp14-{}-{}-{}-{}",
        std::process::id(),
        nanos,
        id,
        name
    ))
}

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)
        .output()
        .expect("run glagol")
}

fn assert_success(context: &str, output: &Output) {
    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn assert_stderr_contains(context: &str, output: &Output, needle: &str) {
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains(needle),
        "{} stderr did not contain `{}`:\n{}",
        context,
        needle,
        stderr
    );
}
