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

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

#[test]
fn symbols_single_file_reports_editor_facing_source_metadata() {
    let source = r#"(module docs (export main))

(type Count i32)

(struct Point
  (x i32)
  (y i32))

(enum Status Ready (Blocked i32))

(fn main () -> i32
  0)

(test "main returns zero"
  (= (main) 0))
"#;
    let file = write_file("symbols-file", source);

    let output = run_glagol(["symbols".as_ref(), file.as_os_str()]);

    assert_success("symbols file", &output);
    assert!(output.stderr.is_empty(), "symbols wrote stderr");
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.starts_with("(symbols\n"));
    assert!(stdout.contains("  (schema slovo.symbols)\n"));
    assert!(stdout.contains("  (version \"1.0.0-beta.10\")\n"));
    assert!(stdout.contains(&format!("      (path \"{}\")\n", file.display())));
    assert!(stdout.contains("      (name \"docs\")\n"));
    assert!(stdout.contains("      (module_span (span "));
    assert!(stdout.contains("(range (start (line 1) (column 1))"));
    assert!(stdout.contains("          (name \"main\")\n"));
    assert!(stdout.contains("          (name \"Count\")\n"));
    assert!(stdout.contains("          (name \"Point\")\n"));
    assert!(stdout.contains("          (name \"Status\")\n"));
    assert!(stdout.contains("          (name \"Blocked\")\n"));
    assert!(stdout.contains("          (name \"main returns zero\")\n"));
}

#[test]
fn symbols_project_writes_output_and_manifest_without_stdout() {
    let project = write_project(
        "symbols-project",
        &[(
            "math",
            "(module math (export one))\n\n(fn one () -> i32\n  1)\n",
        )],
        "(module main)\n\n(import math (one))\n\n(fn main () -> i32\n  (one))\n",
    );
    let output_path = unique_path("symbols-project-out").with_extension("sexpr");
    let manifest_path = unique_path("symbols-project-manifest").with_extension("slo");

    let output = run_glagol([
        "symbols".as_ref(),
        project.as_os_str(),
        "-o".as_ref(),
        output_path.as_os_str(),
        "--manifest".as_ref(),
        manifest_path.as_os_str(),
    ]);

    assert_success("symbols project", &output);
    assert!(output.stdout.is_empty(), "symbols -o wrote stdout");
    assert!(output.stderr.is_empty(), "symbols project wrote stderr");

    let symbols = fs::read_to_string(&output_path).expect("read symbols output");
    assert!(symbols.contains("      (name \"main\")\n"));
    assert!(symbols.contains("      (name \"math\")\n"));
    assert!(symbols.contains("          (module \"math\")\n"));
    assert!(symbols.contains("          (name \"one\")\n"));
    assert!(symbols.contains("      (imports\n"));
    assert!(symbols.contains("      (functions\n"));

    let manifest = fs::read_to_string(&manifest_path).expect("read symbols manifest");
    assert!(manifest.contains("  (mode symbols)\n"));
    assert!(manifest.contains("    (kind symbols)\n"));
    assert!(manifest.contains(&format!("    (path \"{}\")\n", output_path.display())));
}

#[test]
fn symbols_workspace_includes_package_names_deterministically() {
    let workspace = unique_path("symbols-workspace");
    let scaffold = run_glagol([
        "new".as_ref(),
        workspace.as_os_str(),
        "--template".as_ref(),
        "workspace".as_ref(),
    ]);
    assert_success("workspace scaffold", &scaffold);

    let first = run_glagol(["symbols".as_ref(), workspace.as_os_str()]);
    let second = run_glagol(["symbols".as_ref(), workspace.as_os_str()]);

    assert_success("symbols workspace first", &first);
    assert_success("symbols workspace second", &second);
    assert_eq!(
        first.stdout, second.stdout,
        "workspace symbols output was not deterministic"
    );
    let stdout = String::from_utf8_lossy(&first.stdout);
    assert!(stdout.contains("      (package \"app\")\n"));
    assert!(stdout.contains("      (package \"libutil\")\n"));
    assert!(stdout.contains("          (module \"libutil.libutil\")\n"));
}

#[test]
fn symbols_usage_is_part_of_the_public_cli_surface() {
    let output = run_glagol([std::ffi::OsStr::new("--help")]);

    assert_success("glagol help", &output);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("glagol symbols <file.slo|project|workspace>"));
}

fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
    let project = unique_path(name);
    fs::create_dir_all(project.join("src")).expect("create project src");
    fs::write(
        project.join("slovo.toml"),
        format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            name
        ),
    )
    .expect("write manifest");
    for (module, source) in modules {
        fs::write(project.join("src").join(format!("{}.slo", module)), source)
            .expect("write module");
    }
    fs::write(project.join("src/main.slo"), main).expect("write main");
    project
}

fn write_file(name: &str, source: &str) -> PathBuf {
    let path = unique_path(name).with_extension("slo");
    fs::write(&path, source).expect("write fixture");
    path
}

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-symbols-beta10-{}-{}-{}-{}",
        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)
    );
}
