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

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

const UNSAFE_HEADS: &[&str] = &[
    "alloc",
    "dealloc",
    "load",
    "store",
    "ptr_add",
    "unchecked_index",
    "reinterpret",
    "ffi_call",
];

#[test]
fn check_accepts_basic_project_root_and_manifest_path() {
    let project = write_project(
        "basic",
        &[(
            "math",
            "(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n",
        )],
        "(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n  (add_one 41))\n",
    );

    let root = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success_stdout("project root check", root, "");

    let manifest = project.join("slovo.toml");
    let manifest_path = run_glagol(["check".as_ref(), manifest.as_os_str()]);
    assert_success_stdout("manifest path check", manifest_path, "");
}

#[test]
fn project_check_preserves_enum_payload_layout_metadata() {
    let project = write_project(
        "project-enum-payload",
        &[],
        "(module main)\n\n\
(enum Reading\n  Missing\n  (Value string))\n\n\
(fn same ((left Reading) (right Reading)) -> bool\n  (= left right))\n\n\
(fn main () -> i32\n  (if (same (Reading.Value \"hello\") (Reading.Value \"hello\")) 0 1))\n",
    );

    let output = run_glagol(["check".as_ref(), project.as_os_str()]);

    assert_success_stdout("project enum payload check", output, "");
}

#[test]
fn project_imports_exported_direct_string_enum_values_across_modules() {
    let project = write_project(
        "project-direct-string-enum-imports",
        &[(
            "readings",
            "(module readings (export LabelReading make text same))\n\n\
(enum LabelReading\n  Missing\n  (Value string))\n\n\
(fn make ((value string)) -> LabelReading\n  (LabelReading.Value value))\n\n\
(fn text ((reading LabelReading)) -> string\n  (match reading\n    ((LabelReading.Missing)\n      \"\")\n    ((LabelReading.Value payload)\n      payload)))\n\n\
(fn same ((left LabelReading) (right LabelReading)) -> bool\n  (= left right))\n",
        )],
        "(module main)\n\n\
(import readings (LabelReading make text same))\n\n\
(fn pass ((reading LabelReading)) -> LabelReading\n  reading)\n\n\
(fn main () -> i32\n  (if (same (pass (make \"hello\")) (LabelReading.Value \"hello\")) 0 1))\n\n\
(test \"project string enum import match\"\n  (= (text (pass (make \"hello\"))) \"hello\"))\n\n\
(test \"project string enum import equality\"\n  (same (pass (LabelReading.Value \"hello\")) (make \"hello\")))\n",
    );

    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success_stdout("project string enum import check", check, "");

    let test = run_glagol(["test".as_ref(), project.as_os_str()]);
    assert_success_stdout(
        "project string enum import test",
        test,
        "test \"project string enum import match\" ... ok\n\
test \"project string enum import equality\" ... ok\n\
2 test(s) passed\n",
    );
}

#[test]
fn project_imports_exported_enum_values_across_modules() {
    let project = write_project(
        "project-enum-imports",
        &[(
            "readings",
            "(module readings (export Reading make score same))\n\n\
(enum Reading\n  Missing\n  (Value i32))\n\n\
(fn make ((value i32)) -> Reading\n  (Reading.Value value))\n\n\
(fn score ((reading Reading)) -> i32\n  (match reading\n    ((Reading.Missing)\n      0)\n    ((Reading.Value payload)\n      payload)))\n\n\
(fn same ((left Reading) (right Reading)) -> bool\n  (= left right))\n",
        )],
        "(module main)\n\n\
(import readings (Reading make score same))\n\n\
(fn pass ((reading Reading)) -> Reading\n  reading)\n\n\
(fn main () -> i32\n  (let reading Reading (pass (make 41)))\n  (score reading))\n\n\
(test \"project enum import match\"\n  (let reading Reading (pass (make 41)))\n  (= (score reading) 41))\n\n\
(test \"project enum import equality\"\n  (same (pass (Reading.Value 41)) (make 41)))\n",
    );

    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success_stdout("project enum import check", check, "");

    let test = run_glagol(["test".as_ref(), project.as_os_str()]);
    assert_success_stdout(
        "project enum import test",
        test,
        "test \"project enum import match\" ... ok\n\
test \"project enum import equality\" ... ok\n\
2 test(s) passed\n",
    );
}

#[test]
fn project_imports_direct_scalar_and_string_enum_payloads_across_modules() {
    let project = write_project(
        "project-enum-direct-scalars",
        &[(
            "readings",
            "(module readings (export WideReading LabelReading make_wide make_label wide_code label_text same_wide same_label))\n\n\
(enum WideReading\n  Missing\n  (Value i64))\n\n\
(enum LabelReading\n  Missing\n  (Value string))\n\n\
(fn make_wide ((value i64)) -> WideReading\n  (WideReading.Value value))\n\n\
(fn make_label ((value string)) -> LabelReading\n  (LabelReading.Value value))\n\n\
(fn wide_code ((reading WideReading)) -> i64\n  (match reading\n    ((WideReading.Missing)\n      0i64)\n    ((WideReading.Value payload)\n      payload)))\n\n\
(fn label_text ((reading LabelReading)) -> string\n  (match reading\n    ((LabelReading.Missing)\n      \"\")\n    ((LabelReading.Value payload)\n      payload)))\n\n\
(fn same_wide ((left WideReading) (right WideReading)) -> bool\n  (= left right))\n\n\
(fn same_label ((left LabelReading) (right LabelReading)) -> bool\n  (= left right))\n",
        )],
        "(module main)\n\n\
(import readings (WideReading LabelReading make_wide make_label wide_code label_text same_wide same_label))\n\n\
(fn pass_wide ((reading WideReading)) -> WideReading\n  reading)\n\n\
(fn pass_label ((reading LabelReading)) -> LabelReading\n  reading)\n\n\
(fn main () -> i32\n  (if (= (wide_code (pass_wide (make_wide 41i64))) 41i64)\n    41\n    0))\n\n\
(test \"project direct scalar enum import wide match\"\n  (= (wide_code (pass_wide (make_wide 41i64))) 41i64))\n\n\
(test \"project direct scalar enum import wide equality\"\n  (same_wide (pass_wide (WideReading.Value 41i64)) (make_wide 41i64)))\n\n\
(test \"project direct scalar enum import string match\"\n  (= (label_text (pass_label (make_label \"hi\"))) \"hi\"))\n\n\
(test \"project direct scalar enum import string equality\"\n  (same_label (pass_label (LabelReading.Value \"hi\")) (make_label \"hi\")))\n",
    );

    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success_stdout("project direct scalar enum import check", check, "");

    let test = run_glagol(["test".as_ref(), project.as_os_str()]);
    assert_success_stdout(
        "project direct scalar enum import test",
        test,
        "test \"project direct scalar enum import wide match\" ... ok\n\
test \"project direct scalar enum import wide equality\" ... ok\n\
test \"project direct scalar enum import string match\" ... ok\n\
test \"project direct scalar enum import string equality\" ... ok\n\
4 test(s) passed\n",
    );
}

#[test]
fn std_layout_local_math_project_uses_only_explicit_local_imports() {
    let project =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-math");

    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success_stdout("std layout local math check", check, "");

    let test = run_glagol(["test".as_ref(), project.as_os_str()]);
    assert_success_stdout(
        "std layout local math test",
        test,
        "test \"local math i32 helpers\" ... ok\n\
test \"local math i64 helpers\" ... ok\n\
test \"local math f64 helpers\" ... ok\n\
test \"explicit local math import i32 helpers\" ... ok\n\
test \"explicit local math import i64 helpers\" ... ok\n\
test \"explicit local math import f64 helpers\" ... ok\n\
test \"explicit local math import all helpers\" ... ok\n\
7 test(s) passed\n",
    );
}

#[test]
fn std_layout_local_result_project_uses_only_explicit_local_imports() {
    let project =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-result");

    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_success_stdout("std layout local result check", check, "");

    let test = run_glagol(["test".as_ref(), project.as_os_str()]);
    assert_success_stdout(
        "std layout local result test",
        test,
        "test \"explicit local result i32 wrappers\" ... ok\n\
test \"explicit local result err wrappers\" ... ok\n\
test \"explicit local result unwrap_or i32\" ... ok\n\
test \"explicit local result ok_or_none i32 ok\" ... ok\n\
test \"explicit local result ok_or_none i32 none\" ... ok\n\
test \"explicit local result u32 wrappers\" ... ok\n\
test \"explicit local result unwrap_or u32\" ... ok\n\
test \"explicit local result ok_or_none u32 ok\" ... ok\n\
test \"explicit local result ok_or_none u32 none\" ... ok\n\
test \"explicit local result unwrap_or i64\" ... ok\n\
test \"explicit local result ok_or_none i64 ok\" ... ok\n\
test \"explicit local result ok_or_none i64 none\" ... ok\n\
test \"explicit local result u64 wrappers\" ... ok\n\
test \"explicit local result unwrap_or u64\" ... ok\n\
test \"explicit local result ok_or_none u64 ok\" ... ok\n\
test \"explicit local result ok_or_none u64 none\" ... ok\n\
test \"explicit local result unwrap_or string\" ... ok\n\
test \"explicit local result ok_or_none string ok\" ... ok\n\
test \"explicit local result ok_or_none string none\" ... ok\n\
test \"explicit local result unwrap_or f64\" ... ok\n\
test \"explicit local result ok_or_none f64 ok\" ... ok\n\
test \"explicit local result ok_or_none f64 none\" ... ok\n\
test \"explicit local result bool helpers\" ... ok\n\
test \"explicit local result ok_or_none bool ok\" ... ok\n\
test \"explicit local result ok_or_none bool none\" ... ok\n\
test \"explicit local result helpers all\" ... ok\n\
26 test(s) passed\n",
    );
}

#[test]
fn test_project_runs_tests_in_topological_module_order() {
    let project = write_project(
        "test-order",
        &[("a", "(module a (export value))\n\n(fn value () -> i32\n  1)\n\n(test \"provider first\"\n  (= (value) 1))\n")],
        "(module main)\n\n(import a (value))\n\n(fn main () -> i32\n  (value))\n\n(test \"consumer second\"\n  (= (value) 1))\n",
    );

    let output = run_glagol(["test".as_ref(), project.as_os_str()]);

    assert_success_stdout(
        "project test",
        output,
        "test \"provider first\" ... ok\ntest \"consumer second\" ... ok\n2 test(s) passed\n",
    );
}

#[test]
fn test_project_filter_preserves_order_and_counts_skips() {
    let project = write_project(
        "test-filter",
        &[("a", "(module a (export value))\n\n(fn value () -> i32\n  1)\n\n(test \"provider first\"\n  (= (value) 1))\n")],
        "(module main)\n\n(import a (value))\n\n(fn main () -> i32\n  (value))\n\n(test \"consumer second\"\n  (= (value) 1))\n",
    );

    let output = run_glagol([
        "test".as_ref(),
        project.as_os_str(),
        "--filter".as_ref(),
        "consumer".as_ref(),
    ]);

    assert_success_stdout(
        "project filtered test",
        output,
        "test \"provider first\" ... skipped\n\
test \"consumer second\" ... ok\n\
1 test(s) passed (total_discovered 2, selected 1, passed 1, failed 0, skipped 1, filter \"consumer\")\n",
    );
}

#[test]
fn manifest_records_project_fields_modules_and_import_edges() {
    let project = write_project(
        "manifest",
        &[(
            "math",
            "(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n",
        )],
        "(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n  (add_one 41))\n",
    );
    let manifest = unique_path("artifact-manifest");

    let output = run_glagol([
        "check".as_ref(),
        "--manifest".as_ref(),
        manifest.as_os_str(),
        project.as_os_str(),
    ]);

    assert_success("project manifest check", &output);
    let artifact = fs::read_to_string(&manifest).expect("read artifact manifest");
    assert_project_block_snapshot(
        "success check project manifest",
        &artifact,
        &project,
        r#"  (project
    (project_manifest "$PROJECT/slovo.toml")
    (project_root "$PROJECT")
    (source_root "src")
    (project_name "manifest")
    (entry_module "main")
    (modules
      (module
        (name "math")
        (path "$PROJECT/src/math.slo")
        (imports)
      )
      (module
        (name "main")
        (path "$PROJECT/src/main.slo")
        (imports
          (import "math")
        )
      )
    )
    (import_edges
      (import_edge
        (from "main")
        (to "math")
      )
    )
    (diagnostics_count 0)
    (diagnostic_artifacts)
    (build_outputs)
  )"#,
    );
}

#[test]
fn workspace_local_packages_fixture_checks_tests_and_records_graph_manifest() {
    let workspace =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/workspaces/exp-5-local");
    let manifest = unique_path("workspace-artifact-manifest");

    let check = run_glagol([
        "check".as_ref(),
        "--manifest".as_ref(),
        manifest.as_os_str(),
        workspace.as_os_str(),
    ]);

    assert_success("workspace check", &check);
    let artifact = fs::read_to_string(&manifest).expect("read workspace artifact manifest");
    assert_project_block_snapshot(
        "workspace check manifest",
        &artifact,
        &workspace,
        r#"  (project
    (project_manifest "$PROJECT/slovo.toml")
    (project_root "$PROJECT")
    (source_root "")
    (project_name "exp-5-local")
    (entry_module "main")
    (workspace
      (workspace_root "$PROJECT")
      (workspace_manifest "$PROJECT/slovo.toml")
      (members
        (member "packages/app")
        (member "packages/mathlib")
      )
      (packages
        (package
          (name "mathlib")
          (version "0.1.0")
          (root "$PROJECT/packages/mathlib")
          (manifest "$PROJECT/packages/mathlib/slovo.toml")
          (source_root "src")
          (entry "main")
          (test_count 1)
          (modules
            (module
              (name "math")
              (path "$PROJECT/packages/mathlib/src/math.slo")
              (imports)
            )
          )
        )
        (package
          (name "app")
          (version "0.1.0")
          (root "$PROJECT/packages/app")
          (manifest "$PROJECT/packages/app/slovo.toml")
          (source_root "src")
          (entry "main")
          (test_count 1)
          (modules
            (module
              (name "main")
              (path "$PROJECT/packages/app/src/main.slo")
              (imports
                (import "mathlib.math")
              )
            )
          )
        )
      )
      (package_dependency_edges
        (package_dependency
          (from "app")
          (to "mathlib")
          (kind "local-path")
          (path "packages/mathlib")
        )
      )
      (selected_build_entry_package)
    )
    (modules
      (module
        (name "mathlib.math")
        (path "$PROJECT/packages/mathlib/src/math.slo")
        (imports)
      )
      (module
        (name "app.main")
        (path "$PROJECT/packages/app/src/main.slo")
        (imports
          (import "mathlib.math")
        )
      )
    )
    (import_edges
      (import_edge
        (from "app.main")
        (to "mathlib.math")
      )
    )
    (diagnostics_count 0)
    (diagnostic_artifacts)
    (build_outputs)
  )"#,
    );

    let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
    assert_success_stdout(
        "workspace test",
        test,
        "test \"mathlib add one\" ... ok\ntest \"package import add one\" ... ok\n2 test(s) passed\n",
    );
}

#[test]
fn workspace_imports_exported_enum_values_across_packages() {
    let workspace = write_workspace(
        "workspace-enum-imports",
        "[workspace]\nmembers = [\"packages/app\", \"packages/readings\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/readings",
                manifest: "[package]\nname = \"readings\"\nversion = \"0.1.0\"\n",
                modules: &[(
                    "readings",
                    "(module readings (export Reading make score))\n\n\
(enum Reading\n  Missing\n  (Value i32))\n\n\
(fn make ((value i32)) -> Reading\n  (Reading.Value value))\n\n\
(fn score ((reading Reading)) -> i32\n  (match reading\n    ((Reading.Missing)\n      0)\n    ((Reading.Value payload)\n      payload)))\n",
                )],
            },
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nreadings = { path = \"../readings\" }\n",
                modules: &[(
                    "main",
                    "(module main)\n\n\
(import readings.readings (Reading make score))\n\n\
(fn main () -> i32\n  (let reading Reading (make 41))\n  (score reading))\n\n\
(test \"workspace enum import\"\n  (= (Reading.Value 41) (make 41)))\n",
                )],
            },
        ],
    );

    let check = run_glagol(["check".as_ref(), workspace.as_os_str()]);
    assert_success_stdout("workspace enum import check", check, "");

    let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
    assert_success_stdout(
        "workspace enum import test",
        test,
        "test \"workspace enum import\" ... ok\n1 test(s) passed\n",
    );
}

#[test]
fn workspace_imports_exported_direct_i64_enum_values_across_packages() {
    let workspace = write_workspace(
        "workspace-direct-i64-enum-imports",
        "[workspace]\nmembers = [\"packages/app\", \"packages/readings\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/readings",
                manifest: "[package]\nname = \"readings\"\nversion = \"0.1.0\"\n",
                modules: &[(
                    "readings",
                    "(module readings (export WideReading make score))\n\n\
(enum WideReading\n  Missing\n  (Value i64))\n\n\
(fn make ((value i64)) -> WideReading\n  (WideReading.Value value))\n\n\
(fn score ((reading WideReading)) -> i64\n  (match reading\n    ((WideReading.Missing)\n      0i64)\n    ((WideReading.Value payload)\n      payload)))\n",
                )],
            },
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nreadings = { path = \"../readings\" }\n",
                modules: &[(
                    "main",
                    "(module main)\n\n\
(import readings.readings (WideReading make score))\n\n\
(fn main () -> i32\n  (if (= (score (make 4294967296i64)) 4294967296i64) 0 1))\n\n\
(test \"workspace i64 enum import\"\n  (= (WideReading.Value 4294967296i64) (make 4294967296i64)))\n",
                )],
            },
        ],
    );

    let check = run_glagol(["check".as_ref(), workspace.as_os_str()]);
    assert_success_stdout("workspace i64 enum import check", check, "");

    let test = run_glagol(["test".as_ref(), workspace.as_os_str()]);
    assert_success_stdout(
        "workspace i64 enum import test",
        test,
        "test \"workspace i64 enum import\" ... ok\n1 test(s) passed\n",
    );
}

#[test]
fn workspace_build_smoke_uses_single_entry_package_when_host_toolchain_is_available() {
    let workspace =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/workspaces/exp-5-local");
    let binary = unique_path("workspace-build-bin");

    let output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        binary.as_os_str(),
        workspace.as_os_str(),
    ]);

    if output.status.code() == Some(3) {
        assert_stderr_contains("workspace build toolchain", &output, "ToolchainUnavailable");
        return;
    }

    assert_success("workspace build", &output);
    let run = Command::new(&binary)
        .output()
        .expect("run workspace build output");
    assert_success("workspace build binary", &run);
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        "42\n",
        "workspace build binary stdout mismatch"
    );
}

#[test]
fn workspace_build_requires_one_entry_package() {
    let workspace = write_workspace(
        "workspace-ambiguous-build-entry",
        "[workspace]\nmembers = [\"packages/app\", \"packages/tool\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
            WorkspacePackageSpec {
                member: "packages/tool",
                manifest: "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
        ],
    );
    let binary = unique_path("workspace-ambiguous-build-bin");

    let output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        binary.as_os_str(),
        workspace.as_os_str(),
    ]);

    assert_exit_code("workspace ambiguous entry", &output, 1);
    assert_stderr_contains(
        "workspace ambiguous entry",
        &output,
        "WorkspaceBuildAmbiguousEntryPackage",
    );
}

#[test]
fn workspace_default_package_selects_build_entry() {
    let workspace = write_workspace(
        "workspace-default-build-entry",
        "[workspace]\nmembers = [\"packages/app\", \"packages/tool\"]\ndefault_package = \"app\"\n",
        &[
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
            WorkspacePackageSpec {
                member: "packages/tool",
                manifest: "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  7)\n")],
            },
        ],
    );
    let binary = unique_path("workspace-default-build-bin");

    let output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        binary.as_os_str(),
        workspace.as_os_str(),
    ]);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("WorkspaceBuildAmbiguousEntryPackage"),
        "default package should avoid ambiguous build entry diagnostic:\n{}",
        stderr
    );

    if output.status.code() == Some(3) {
        assert_stderr_contains(
            "workspace default package build toolchain",
            &output,
            "ToolchainUnavailable",
        );
        return;
    }

    assert_success("workspace default package build", &output);
    let run = Command::new(&binary)
        .output()
        .expect("run workspace default package build output");
    assert_success("workspace default package build binary", &run);
}

#[test]
fn workspace_build_reports_entry_main_contract() {
    let missing_main = write_workspace(
        "workspace-missing-entry-main",
        "[workspace]\nmembers = [\"packages/app\"]\n",
        &[WorkspacePackageSpec {
            member: "packages/app",
            manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
            modules: &[("main", "(module main)\n\n(test \"ok\"\n  true)\n")],
        }],
    );
    let missing_binary = unique_path("workspace-missing-entry-main-bin");
    let missing_output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        missing_binary.as_os_str(),
        missing_main.as_os_str(),
    ]);
    assert_exit_code("workspace missing entry main", &missing_output, 1);
    assert_stderr_contains(
        "workspace missing entry main",
        &missing_output,
        "WorkspaceEntryMainMissing",
    );
    assert_stderr_contains(
        "workspace missing entry main",
        &missing_output,
        "build/run require `(fn main () -> i32 ...)`",
    );

    let bad_signature = write_workspace(
        "workspace-bad-entry-main",
        "[workspace]\nmembers = [\"packages/app\"]\n",
        &[WorkspacePackageSpec {
            member: "packages/app",
            manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
            modules: &[(
                "main",
                "(module main)\n\n(fn main () -> string\n  \"bad\")\n",
            )],
        }],
    );
    let bad_binary = unique_path("workspace-bad-entry-main-bin");
    let bad_output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        bad_binary.as_os_str(),
        bad_signature.as_os_str(),
    ]);
    assert_exit_code("workspace bad entry main", &bad_output, 1);
    assert_stderr_contains(
        "workspace bad entry main",
        &bad_output,
        "WorkspaceEntryMainInvalidSignature",
    );
    assert_stderr_contains(
        "workspace bad entry main",
        &bad_output,
        "found 0 parameter(s) and return `string`",
    );
}

#[test]
fn workspace_package_boundaries_are_diagnostics() {
    let missing = write_workspace(
        "workspace-missing-dependency",
        "[workspace]\nmembers = [\"packages/app\"]\n",
        &[WorkspacePackageSpec {
            member: "packages/app",
            manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmathlib = { path = \"../mathlib\" }\n",
            modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
        }],
    );
    let missing_output = run_glagol(["check".as_ref(), missing.as_os_str()]);
    assert_exit_code("missing package dependency", &missing_output, 1);
    assert_stderr_contains(
        "missing package dependency",
        &missing_output,
        "MissingPackageDependency",
    );

    let duplicate = write_workspace(
        "workspace-duplicate-package",
        "[workspace]\nmembers = [\"packages/app\", \"packages/dup\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
            WorkspacePackageSpec {
                member: "packages/dup",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
        ],
    );
    let duplicate_output = run_glagol(["check".as_ref(), duplicate.as_os_str()]);
    assert_exit_code("duplicate package name", &duplicate_output, 1);
    assert_stderr_contains(
        "duplicate package name",
        &duplicate_output,
        "DuplicatePackageName",
    );

    let cycle = write_workspace(
        "workspace-package-cycle",
        "[workspace]\nmembers = [\"packages/app\", \"packages/mathlib\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmathlib = { path = \"../mathlib\" }\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
            WorkspacePackageSpec {
                member: "packages/mathlib",
                manifest: "[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\n\n[dependencies]\napp = { path = \"../app\" }\n",
                modules: &[("math", "(module math)\n\n(fn value () -> i32\n  1)\n")],
            },
        ],
    );
    let cycle_output = run_glagol(["check".as_ref(), cycle.as_os_str()]);
    assert_exit_code("package dependency cycle", &cycle_output, 1);
    assert_stderr_contains(
        "package dependency cycle",
        &cycle_output,
        "PackageDependencyCycle",
    );

    let private = write_workspace(
        "workspace-private-visibility",
        "[workspace]\nmembers = [\"packages/app\", \"packages/mathlib\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nmathlib = { path = \"../mathlib\" }\n",
                modules: &[(
                    "main",
                    "(module main)\n\n(import mathlib.math (add_one))\n\n(fn main () -> i32\n  (add_one 1))\n",
                )],
            },
            WorkspacePackageSpec {
                member: "packages/mathlib",
                manifest: "[package]\nname = \"mathlib\"\nversion = \"0.1.0\"\n",
                modules: &[(
                    "math",
                    "(module math)\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n",
                )],
            },
        ],
    );
    let private_output = run_glagol(["check".as_ref(), private.as_os_str()]);
    assert_exit_code("private package visibility", &private_output, 1);
    assert_stderr_contains("private package visibility", &private_output, "Visibility");

    let invalid = write_workspace(
        "workspace-invalid-package",
        "[workspace]\nmembers = [\"packages/app\"]\n",
        &[WorkspacePackageSpec {
            member: "packages/app",
            manifest: "[package]\nname = \"App\"\nversion = \"0.1\"\n",
            modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
        }],
    );
    let invalid_output = run_glagol(["check".as_ref(), invalid.as_os_str()]);
    assert_exit_code("invalid package metadata", &invalid_output, 1);
    assert_stderr_contains(
        "invalid package name",
        &invalid_output,
        "InvalidPackageName",
    );
    assert_stderr_contains(
        "invalid package version",
        &invalid_output,
        "InvalidPackageVersion",
    );

    let escape = write_workspace(
        "workspace-path-escape",
        "[workspace]\nmembers = [\"../outside\"]\n",
        &[],
    );
    let escape_output = run_glagol(["check".as_ref(), escape.as_os_str()]);
    assert_exit_code("workspace member path escape", &escape_output, 1);
    assert_stderr_contains(
        "workspace member path escape",
        &escape_output,
        "WorkspaceMemberPathEscape",
    );

    let duplicate_member = write_workspace(
        "workspace-duplicate-member",
        "[workspace]\nmembers = [\"packages/app\", \"packages/./app\"]\n",
        &[WorkspacePackageSpec {
            member: "packages/app",
            manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
            modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
        }],
    );
    let duplicate_member_output = run_glagol(["check".as_ref(), duplicate_member.as_os_str()]);
    assert_exit_code("duplicate workspace member", &duplicate_member_output, 1);
    assert_stderr_contains(
        "duplicate workspace member",
        &duplicate_member_output,
        "DuplicateWorkspaceMember",
    );

    let missing_default = write_workspace(
        "workspace-missing-default-package",
        "[workspace]\nmembers = [\"packages/app\"]\ndefault_package = \"tool\"\n",
        &[WorkspacePackageSpec {
            member: "packages/app",
            manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
            modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
        }],
    );
    let missing_default_output = run_glagol(["check".as_ref(), missing_default.as_os_str()]);
    assert_exit_code("missing default package", &missing_default_output, 1);
    assert_stderr_contains(
        "missing default package",
        &missing_default_output,
        "WorkspaceDefaultPackageMissing",
    );

    let missing_default_entry = write_workspace(
        "workspace-missing-default-entry",
        "[workspace]\nmembers = [\"packages/app\", \"packages/tool\"]\ndefault_package = \"app\"\n",
        &[
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\nentry = \"app\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
            WorkspacePackageSpec {
                member: "packages/tool",
                manifest: "[package]\nname = \"tool\"\nversion = \"0.1.0\"\n",
                modules: &[("main", "(module main)\n\n(fn main () -> i32\n  0)\n")],
            },
        ],
    );
    let missing_default_entry_binary = unique_path("workspace-missing-default-entry-bin");
    let missing_default_entry_output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        missing_default_entry_binary.as_os_str(),
        missing_default_entry.as_os_str(),
    ]);
    assert_exit_code(
        "missing default package entry",
        &missing_default_entry_output,
        1,
    );
    assert_stderr_contains(
        "missing default package entry",
        &missing_default_entry_output,
        "WorkspaceDefaultPackageEntryMissing",
    );
}

#[test]
fn single_file_input_keeps_v1_2_behavior_even_next_to_manifest() {
    let project = write_project(
        "single-file",
        &[(
            "math",
            "(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n",
        )],
        "(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n  (add_one 41))\n",
    );
    let single = project.join("src/main.slo");

    let output = run_glagol(["check".as_ref(), single.as_os_str()]);

    assert_exit_code("single file project source", &output, 1);
    assert_stderr_contains("single file remains v1.2", &output, "UnknownTopLevelForm");
}

#[test]
fn legacy_run_tests_flag_does_not_enter_project_mode() {
    let project = write_project(
        "legacy-run-tests",
        &[],
        "(module main)\n\n(test \"ok\"\n  true)\n",
    );

    let output = run_glagol(["--run-tests".as_ref(), project.as_os_str()]);

    assert_exit_code("legacy --run-tests project", &output, 1);
    assert_stderr_contains("legacy --run-tests project", &output, "InputReadFailed");
}

#[test]
fn project_check_and_test_do_not_require_entry_main_function() {
    let project = write_project(
        "check-test-no-main",
        &[],
        "(module main)\n\n(test \"ok\"\n  true)\n",
    );
    let check = run_glagol(["check".as_ref(), project.as_os_str()]);
    let test = run_glagol(["test".as_ref(), project.as_os_str()]);

    assert_success_stdout("check without entry main", check, "");
    assert_success_stdout(
        "test without entry main",
        test,
        "test \"ok\" ... ok\n1 test(s) passed\n",
    );
}

#[test]
fn diagnostic_uses_source_file_for_failed_project_test() {
    let project = write_project(
        "source-span",
        &[],
        "(module main)\n\n(fn main () -> i32\n  0)\n\n(test \"fails\"\n  false)\n",
    );
    let output = run_glagol(["test".as_ref(), project.as_os_str()]);
    let main_file = project.join("src/main.slo").display().to_string();

    assert_exit_code("failed project test", &output, 1);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("TestFailed") && stderr.contains(&main_file),
        "diagnostic did not reference source file `{}`:\n{}",
        main_file,
        stderr
    );
}

#[test]
fn failure_manifest_records_project_block_and_diagnostics_artifact() {
    let project = write_project(
        "failure-manifest",
        &[],
        "(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n  (add_one 41))\n",
    );
    let manifest = unique_path("failure-artifact-manifest");

    let output = run_glagol([
        "check".as_ref(),
        "--manifest".as_ref(),
        manifest.as_os_str(),
        project.as_os_str(),
    ]);

    assert_exit_code("failure project manifest check", &output, 1);
    let artifact = fs::read_to_string(&manifest).expect("read failure artifact manifest");
    assert_project_block_snapshot(
        "failure check project manifest",
        &artifact,
        &project,
        r#"  (project
    (project_manifest "$PROJECT/slovo.toml")
    (project_root "$PROJECT")
    (source_root "src")
    (project_name "failure-manifest")
    (entry_module "main")
    (modules
      (module
        (name "main")
        (path "$PROJECT/src/main.slo")
        (imports
          (import "math")
        )
      )
    )
    (import_edges
      (import_edge
        (from "main")
        (to "math")
      )
    )
    (diagnostics_count 1)
    (diagnostic_artifacts
      (artifact
        (kind diagnostics)
        (stream stderr)
      )
    )
    (build_outputs)
  )"#,
    );
}

#[test]
fn missing_import_module_source_root_and_manifest_are_diagnostics() {
    let project = write_project(
        "missing-import",
        &[],
        "(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n  (add_one 41))\n",
    );
    let missing_import = run_glagol(["check".as_ref(), project.as_os_str()]);
    assert_exit_code("missing import", &missing_import, 1);
    assert_stderr_contains("missing import", &missing_import, "MissingImport");

    let missing_root = write_project_with_manifest(
        "missing-root",
        "[project]\nname = \"missing-root\"\nsource_root = \"missing\"\nentry = \"main\"\n",
        &[],
        "",
    );
    let missing_root_output = run_glagol(["check".as_ref(), missing_root.as_os_str()]);
    assert_exit_code("missing source root", &missing_root_output, 1);
    assert_stderr_contains(
        "missing source root",
        &missing_root_output,
        "ProjectSourceRootMissing",
    );

    let bad_manifest = unique_path("bad-manifest");
    fs::create_dir_all(&bad_manifest).expect("create bad manifest project");
    fs::write(
        bad_manifest.join("slovo.toml"),
        "[project]\nentry = \"main\"\n",
    )
    .expect("write bad manifest");
    let bad_manifest_output = run_glagol(["check".as_ref(), bad_manifest.as_os_str()]);
    assert_exit_code("bad manifest", &bad_manifest_output, 1);
    assert_stderr_contains(
        "bad manifest",
        &bad_manifest_output,
        "ProjectManifestInvalid",
    );
}

#[test]
fn invalid_manifest_boundaries_are_diagnostics() {
    let cases = [
        (
            "missing-project-section",
            "name = \"missing-section\"\n",
            "project manifest is missing `[project]` section",
        ),
        (
            "invalid-name",
            "[project]\nname = \"Bad_Name\"\n",
            "project name must be lowercase ASCII",
        ),
        (
            "invalid-entry",
            "[project]\nname = \"invalid-entry\"\nentry = \"../main\"\n",
            "project entry must be a flat module identifier",
        ),
        (
            "source-root-parent",
            "[project]\nname = \"source-root-parent\"\nsource_root = \"../src\"\n",
            "project source_root must be a relative path",
        ),
        (
            "source-root-absolute",
            "[project]\nname = \"source-root-absolute\"\nsource_root = \"/tmp\"\n",
            "project source_root must be a relative path",
        ),
        (
            "generated-source",
            "[project]\nname = \"generated-source\"\nsource_root = \"generated-source\"\n",
            "project source_root must be a relative path",
        ),
    ];

    for (name, manifest, expected) in cases {
        let project = write_project_with_manifest(name, manifest, &[], "");
        let output = run_glagol(["check".as_ref(), project.as_os_str()]);
        assert_exit_code(name, &output, 1);
        assert_stderr_contains(name, &output, "ProjectManifestInvalid");
        assert_stderr_contains(name, &output, expected);
    }
}

#[cfg(unix)]
#[test]
fn symlink_source_root_and_module_file_escapes_are_diagnostics() {
    use std::os::unix::fs::symlink;

    let root_escape = unique_path("symlink-root-escape");
    let outside_root = unique_path("symlink-root-outside");
    fs::create_dir_all(&root_escape).expect("create symlink project");
    fs::create_dir_all(&outside_root).expect("create symlink outside root");
    fs::write(
        root_escape.join("slovo.toml"),
        "[project]\nname = \"symlink-root-escape\"\nsource_root = \"src\"\nentry = \"main\"\n",
    )
    .expect("write symlink manifest");
    symlink(&outside_root, root_escape.join("src")).expect("symlink source root");
    let root_output = run_glagol(["check".as_ref(), root_escape.as_os_str()]);
    assert_exit_code("symlink source root escape", &root_output, 1);
    assert_stderr_contains(
        "symlink source root escape",
        &root_output,
        "ProjectManifestInvalid",
    );
    assert_stderr_contains(
        "symlink source root escape",
        &root_output,
        "source_root must not escape",
    );

    let module_escape = unique_path("symlink-module-escape");
    let outside_module = unique_path("symlink-module-outside");
    fs::create_dir_all(module_escape.join("src")).expect("create module escape src");
    fs::create_dir_all(&outside_module).expect("create module outside dir");
    fs::write(
        module_escape.join("slovo.toml"),
        "[project]\nname = \"symlink-module-escape\"\nsource_root = \"src\"\nentry = \"main\"\n",
    )
    .expect("write module escape manifest");
    let outside_main = outside_module.join("main.slo");
    fs::write(&outside_main, "(module main)\n\n(fn main () -> i32\n  0)\n")
        .expect("write outside module");
    symlink(outside_main, module_escape.join("src/main.slo")).expect("symlink module file");
    let module_output = run_glagol(["check".as_ref(), module_escape.as_os_str()]);
    assert_exit_code("symlink module escape", &module_output, 1);
    assert_stderr_contains(
        "symlink module escape",
        &module_output,
        "ProjectManifestInvalid",
    );
    assert_stderr_contains(
        "symlink module escape",
        &module_output,
        "escapes the source root",
    );
}

#[test]
fn duplicate_visibility_ambiguous_and_import_cycle_are_diagnostics() {
    let duplicate = write_project(
        "duplicate",
        &[("math", "(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n")],
        "(module main)\n\n(import math (add_one))\n\n(fn add_one ((value i32)) -> i32\n  value)\n\n(fn main () -> i32\n  (add_one 41))\n",
    );
    let duplicate_output = run_glagol(["check".as_ref(), duplicate.as_os_str()]);
    assert_exit_code("duplicate", &duplicate_output, 1);
    assert_stderr_contains("duplicate", &duplicate_output, "DuplicateName");

    let visibility = write_project(
        "visibility",
        &[("math", "(module math)\n\n(fn hidden () -> i32\n  1)\n")],
        "(module main)\n\n(import math (hidden))\n\n(fn main () -> i32\n  (hidden))\n",
    );
    let visibility_output = run_glagol(["check".as_ref(), visibility.as_os_str()]);
    assert_exit_code("visibility", &visibility_output, 1);
    assert_stderr_contains("visibility", &visibility_output, "Visibility");

    let ambiguous = write_project(
        "ambiguous",
        &[
            ("left", "(module left (export value))\n\n(fn value () -> i32\n  1)\n"),
            ("right", "(module right (export value))\n\n(fn value () -> i32\n  2)\n"),
        ],
        "(module main)\n\n(import left (value))\n(import right (value))\n\n(fn main () -> i32\n  (value))\n",
    );
    let ambiguous_output = run_glagol(["check".as_ref(), ambiguous.as_os_str()]);
    assert_exit_code("ambiguous", &ambiguous_output, 1);
    assert_stderr_contains("ambiguous", &ambiguous_output, "AmbiguousName");

    let cycle = write_project(
        "cycle",
        &[
            (
                "a",
                "(module a (export fa))\n\n(import b (fb))\n\n(fn fa () -> i32\n  (fb))\n",
            ),
            (
                "b",
                "(module b (export fb))\n\n(import a (fa))\n\n(fn fb () -> i32\n  (fa))\n",
            ),
        ],
        "(module main)\n\n(import a (fa))\n\n(fn main () -> i32\n  (fa))\n",
    );
    let cycle_output = run_glagol(["check".as_ref(), cycle.as_os_str()]);
    assert_exit_code("cycle", &cycle_output, 1);
    assert_stderr_contains("cycle", &cycle_output, "ImportCycle");
    assert_stderr_contains(
        "cycle related edge",
        &cycle_output,
        "module `a` imports `b`",
    );
}

#[test]
fn enum_import_visibility_and_duplicate_cases_are_diagnostics() {
    let private = write_project(
        "enum-private-import",
        &[(
            "readings",
            "(module readings)\n\n(enum Reading Missing (Value i32))\n",
        )],
        "(module main)\n\n(import readings (Reading))\n",
    );
    let private_output = run_glagol(["check".as_ref(), private.as_os_str()]);
    assert_exit_code("private enum import", &private_output, 1);
    assert_stderr_contains("private enum import", &private_output, "Visibility");
    assert_stderr_contains(
        "private enum related span",
        &private_output,
        "private declaration",
    );

    let missing = write_project(
        "enum-missing-import",
        &[(
            "readings",
            "(module readings)\n\n(fn main () -> i32\n  0)\n",
        )],
        "(module main)\n\n(import readings (Reading))\n",
    );
    let missing_output = run_glagol(["check".as_ref(), missing.as_os_str()]);
    assert_exit_code("missing enum import", &missing_output, 1);
    assert_stderr_contains("missing enum import", &missing_output, "MissingImport");
    assert_stderr_contains(
        "missing enum import message",
        &missing_output,
        "function, struct, or enum",
    );

    let duplicate = write_project(
        "enum-duplicate-import",
        &[(
            "readings",
            "(module readings (export Reading))\n\n(enum Reading Missing)\n",
        )],
        "(module main)\n\n(import readings (Reading))\n\n(enum Reading Local)\n",
    );
    let duplicate_output = run_glagol(["check".as_ref(), duplicate.as_os_str()]);
    assert_exit_code("duplicate enum import", &duplicate_output, 1);
    assert_stderr_contains("duplicate enum import", &duplicate_output, "DuplicateName");

    let invalid_export = write_project(
        "enum-invalid-export",
        &[("readings", "(module readings (export Reading))\n")],
        "(module main)\n",
    );
    let invalid_export_output = run_glagol(["check".as_ref(), invalid_export.as_os_str()]);
    assert_exit_code("invalid enum export", &invalid_export_output, 1);
    assert_stderr_contains("invalid enum export", &invalid_export_output, "Visibility");
    assert_stderr_contains(
        "invalid enum export message",
        &invalid_export_output,
        "local function, struct, or enum",
    );
}

#[test]
fn type_aliases_are_local_across_project_visibility() {
    let erased_signature = write_project(
        "alias-erased-signature",
        &[(
            "types",
            "(module types (export make_count))\n\n(type Count i32)\n\n(fn make_count ((value Count)) -> Count\n  value)\n",
        )],
        "(module main)\n\n(import types (make_count))\n\n(fn main () -> i32\n  (make_count 42))\n",
    );
    let erased_output = run_glagol(["check".as_ref(), erased_signature.as_os_str()]);
    assert_success_stdout("alias erased project signature", erased_output, "");

    let export_alias = write_project(
        "alias-export",
        &[(
            "types",
            "(module types (export Count))\n\n(type Count i32)\n\n(fn make_count ((value Count)) -> Count\n  value)\n",
        )],
        "(module main)\n",
    );
    let export_output = run_glagol(["check".as_ref(), export_alias.as_os_str()]);
    assert_exit_code("alias export", &export_output, 1);
    assert_stderr_contains("alias export", &export_output, "Visibility");
    assert_stderr_contains(
        "alias export message",
        &export_output,
        "type alias `Count` is module-local and cannot be exported",
    );

    let import_alias = write_project(
        "alias-import",
        &[("types", "(module types)\n\n(type Count i32)\n")],
        "(module main)\n\n(import types (Count))\n",
    );
    let import_output = run_glagol(["check".as_ref(), import_alias.as_os_str()]);
    assert_exit_code("alias import", &import_output, 1);
    assert_stderr_contains("alias import", &import_output, "Visibility");
    assert_stderr_contains(
        "alias import message",
        &import_output,
        "type alias `Count` is module-local and cannot be imported",
    );

    let duplicate_local = write_project(
        "alias-import-duplicate",
        &[(
            "types",
            "(module types (export value))\n\n(fn value () -> i32\n  1)\n",
        )],
        "(module main)\n\n(import types (Count))\n\n(type Count i32)\n",
    );
    let duplicate_output = run_glagol(["check".as_ref(), duplicate_local.as_os_str()]);
    assert_exit_code("alias import duplicate", &duplicate_output, 1);
    assert_stderr_contains("alias import duplicate", &duplicate_output, "DuplicateName");
}

#[test]
fn project_rejects_exported_generic_alias_and_function_before_visibility_leakage() {
    let generic_function = write_project(
        "generic-function-export",
        &[(
            "ids",
            "(module ids (export id))\n\n(fn id (type_params T) ((value T)) -> T\n  value)\n",
        )],
        "(module main)\n",
    );
    let function_output = run_glagol(["check".as_ref(), generic_function.as_os_str()]);
    assert_exit_code("generic function export", &function_output, 1);
    assert_stderr_contains(
        "generic function export",
        &function_output,
        "UnsupportedGenericFunction",
    );

    let generic_alias = write_project(
        "generic-alias-export",
        &[(
            "types",
            "(module types (export VecOf))\n\n(type VecOf (type_params T) (vec T))\n",
        )],
        "(module main)\n",
    );
    let alias_output = run_glagol(["check".as_ref(), generic_alias.as_os_str()]);
    assert_exit_code("generic alias export", &alias_output, 1);
    assert_stderr_contains(
        "generic alias export",
        &alias_output,
        "UnsupportedGenericTypeAlias",
    );
}

#[test]
fn project_diagnostic_families_have_json_golden_coverage() {
    let duplicate = write_project(
        "json-duplicate",
        &[(
            "math",
            "(module math (export add_one))\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n",
        )],
        "(module main)\n\n(import math (add_one))\n\n(fn add_one ((value i32)) -> i32\n  value)\n",
    );
    assert_json_diagnostic_code("duplicate json", &duplicate, "DuplicateName");

    let missing = write_project(
        "json-missing",
        &[],
        "(module main)\n\n(import math (add_one))\n",
    );
    assert_json_diagnostic_code("missing json", &missing, "MissingImport");

    let visibility = write_project(
        "json-visibility",
        &[("math", "(module math)\n\n(fn hidden () -> i32\n  1)\n")],
        "(module main)\n\n(import math (hidden))\n",
    );
    assert_json_diagnostic_code("visibility json", &visibility, "Visibility");

    let ambiguous = write_project(
        "json-ambiguous",
        &[
            (
                "left",
                "(module left (export value))\n\n(fn value () -> i32\n  1)\n",
            ),
            (
                "right",
                "(module right (export value))\n\n(fn value () -> i32\n  2)\n",
            ),
        ],
        "(module main)\n\n(import left (value))\n(import right (value))\n",
    );
    assert_json_diagnostic_code("ambiguous json", &ambiguous, "AmbiguousName");

    let cycle = write_project(
        "json-cycle",
        &[
            (
                "a",
                "(module a (export fa))\n\n(import b (fb))\n\n(fn fa () -> i32\n  (fb))\n",
            ),
            (
                "b",
                "(module b (export fb))\n\n(import a (fa))\n\n(fn fb () -> i32\n  (fa))\n",
            ),
        ],
        "(module main)\n\n(import a (fa))\n",
    );
    assert_json_diagnostic_code("cycle json", &cycle, "ImportCycle");
}

#[test]
fn cross_file_related_spans_render_file_identity_in_text_sexpr_and_json() {
    let visibility = write_project(
        "related-visibility",
        &[("math", "(module math)\n\n(fn hidden () -> i32\n  1)\n")],
        "(module main)\n\n(import math (hidden))\n",
    );
    let text = run_glagol(["check".as_ref(), visibility.as_os_str()]);
    assert_exit_code("visibility related text", &text, 1);
    let stderr = String::from_utf8_lossy(&text.stderr);
    let math_file = visibility.join("src/math.slo").display().to_string();
    let main_file = visibility.join("src/main.slo").display().to_string();
    assert!(
        stderr.contains(&format!("(file {})", sexpr_string(&main_file)))
            && stderr.contains(&format!("(file {})", sexpr_string(&math_file)))
            && stderr.contains("private declaration"),
        "visibility diagnostic did not carry cross-file related span:\n{}",
        stderr
    );

    let json = run_glagol([
        "check".as_ref(),
        "--json-diagnostics".as_ref(),
        visibility.as_os_str(),
    ]);
    assert_exit_code("visibility related json", &json, 1);
    let stderr = String::from_utf8_lossy(&json.stderr);
    assert!(
        stderr.contains(&format!("\"file\":{}", json_string(&main_file)))
            && stderr.contains(&format!("\"file\":{}", json_string(&math_file)))
            && stderr.contains("\"related\"")
            && stderr.contains("private declaration"),
        "visibility JSON diagnostic did not carry cross-file related span:\n{}",
        stderr
    );
}

#[test]
fn project_build_smoke_uses_entry_main_when_host_toolchain_is_available() {
    let project = write_project(
        "build",
        &[("math", "(module math (export main add_one))\n\n(fn main () -> i32\n  9)\n\n(fn add_one ((value i32)) -> i32\n  (+ value 1))\n")],
        "(module main)\n\n(import math (add_one))\n\n(fn main () -> i32\n  (print_i32 (add_one 41))\n  0)\n",
    );
    let binary = unique_path("project-build-bin");

    let output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        binary.as_os_str(),
        project.as_os_str(),
    ]);

    if output.status.code() == Some(3) {
        assert_stderr_contains("project build toolchain", &output, "ToolchainUnavailable");
        return;
    }

    assert_success("project build", &output);
    let run = Command::new(&binary)
        .output()
        .expect("run project build output");
    assert_success("project build binary", &run);
    assert_eq!(
        String::from_utf8_lossy(&run.stdout),
        "42\n",
        "project build binary stdout mismatch"
    );
}

#[test]
fn project_build_requires_entry_main_contract() {
    let missing_main = write_project(
        "missing-main",
        &[],
        "(module main)\n\n(test \"ok\"\n  true)\n",
    );
    let missing_binary = unique_path("missing-main-bin");
    let missing_output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        missing_binary.as_os_str(),
        missing_main.as_os_str(),
    ]);
    assert_exit_code("missing entry main", &missing_output, 1);
    assert_stderr_contains(
        "missing entry main",
        &missing_output,
        "ProjectEntryMainMissing",
    );
    assert_stderr_contains(
        "missing entry main",
        &missing_output,
        "build/run require `(fn main () -> i32 ...)`",
    );

    let bad_signature = write_project(
        "bad-main-signature",
        &[],
        "(module main)\n\n(fn main ((value i32)) -> i32\n  value)\n",
    );
    let bad_binary = unique_path("bad-main-signature-bin");
    let bad_output = run_glagol([
        "build".as_ref(),
        "-o".as_ref(),
        bad_binary.as_os_str(),
        bad_signature.as_os_str(),
    ]);
    assert_exit_code("bad entry main signature", &bad_output, 1);
    assert_stderr_contains(
        "bad entry main signature",
        &bad_output,
        "ProjectEntryMainInvalidSignature",
    );
    assert_stderr_contains(
        "bad entry main signature",
        &bad_output,
        "found 1 parameter(s) and return `i32`",
    );
}

#[test]
fn module_mismatch_and_reserved_intrinsics_are_diagnostics() {
    let mismatch = write_project_with_manifest(
        "module-mismatch",
        "[project]\nname = \"module-mismatch\"\nsource_root = \"src\"\nentry = \"main\"\n",
        &[("main", "(module wrong)\n\n(fn main () -> i32\n  0)\n")],
        "",
    );
    let mismatch_output = run_glagol(["check".as_ref(), mismatch.as_os_str()]);
    assert_exit_code("module mismatch", &mismatch_output, 1);
    assert_stderr_contains("module mismatch", &mismatch_output, "MissingImport");

    let local_reserved = write_project(
        "local-reserved",
        &[],
        "(module main)\n\n(fn print_i32 () -> i32\n  0)\n",
    );
    let local_output = run_glagol(["check".as_ref(), local_reserved.as_os_str()]);
    assert_exit_code("local reserved", &local_output, 1);
    assert_stderr_contains("local reserved", &local_output, "DuplicateName");

    let export_reserved = write_project(
        "export-reserved",
        &[],
        "(module main (export string_len))\n\n(fn main () -> i32\n  0)\n",
    );
    let export_output = run_glagol(["check".as_ref(), export_reserved.as_os_str()]);
    assert_exit_code("export reserved", &export_output, 1);
    assert_stderr_contains("export reserved", &export_output, "Visibility");

    let import_reserved = write_project(
        "import-reserved",
        &[("math", "(module math)\n\n(fn main () -> i32\n  0)\n")],
        "(module main)\n\n(import math (print_i32))\n",
    );
    let import_output = run_glagol(["check".as_ref(), import_reserved.as_os_str()]);
    assert_exit_code("import reserved", &import_output, 1);
    assert_stderr_contains("import reserved", &import_output, "Visibility");

    let local_promoted_reserved = write_project(
        "local-promoted-reserved",
        &[],
        "(module main)\n\n(fn std.io.print_i32 () -> i32\n  0)\n",
    );
    let local_promoted_output = run_glagol(["check".as_ref(), local_promoted_reserved.as_os_str()]);
    assert_exit_code("local promoted reserved", &local_promoted_output, 1);
    assert_stderr_contains(
        "local promoted reserved",
        &local_promoted_output,
        "DuplicateName",
    );

    let local_private_runtime_reserved = write_project(
        "local-private-runtime-reserved",
        &[],
        "(module main)\n\n(fn __glagol_string_concat ((left string) (right string)) -> string\n  \"intercepted\")\n",
    );
    let local_private_runtime_output =
        run_glagol(["check".as_ref(), local_private_runtime_reserved.as_os_str()]);
    assert_exit_code(
        "local private runtime reserved",
        &local_private_runtime_output,
        1,
    );
    assert_stderr_contains(
        "local private runtime reserved",
        &local_private_runtime_output,
        "DuplicateName",
    );

    let struct_promoted_reserved = write_project(
        "struct-promoted-reserved",
        &[],
        "(module main)\n\n(struct std.string.len (value i32))\n\n(fn main () -> i32\n  0)\n",
    );
    let struct_promoted_output =
        run_glagol(["check".as_ref(), struct_promoted_reserved.as_os_str()]);
    assert_exit_code("struct promoted reserved", &struct_promoted_output, 1);
    assert_stderr_contains(
        "struct promoted reserved",
        &struct_promoted_output,
        "DuplicateName",
    );

    let export_promoted_reserved = write_project(
        "export-promoted-reserved",
        &[],
        "(module main (export std.string.len))\n\n(fn main () -> i32\n  0)\n",
    );
    let export_promoted_output =
        run_glagol(["check".as_ref(), export_promoted_reserved.as_os_str()]);
    assert_exit_code("export promoted reserved", &export_promoted_output, 1);
    assert_stderr_contains(
        "export promoted reserved",
        &export_promoted_output,
        "Visibility",
    );

    let import_promoted_reserved = write_project(
        "import-promoted-reserved",
        &[("math", "(module math)\n\n(fn main () -> i32\n  0)\n")],
        "(module main)\n\n(import math (std.io.print_i32))\n",
    );
    let import_promoted_output =
        run_glagol(["check".as_ref(), import_promoted_reserved.as_os_str()]);
    assert_exit_code("import promoted reserved", &import_promoted_output, 1);
    assert_stderr_contains(
        "import promoted reserved",
        &import_promoted_output,
        "Visibility",
    );
}

#[test]
fn unsafe_operation_heads_are_reserved_in_project_visibility() {
    for head in UNSAFE_HEADS {
        let project_head = head.replace('_', "-");
        let local_reserved = write_project(
            &format!("unsafe-local-reserved-{}", project_head),
            &[],
            &format!("(module main)\n\n(fn {} () -> i32\n  0)\n", head),
        );
        let local_output = run_glagol(["check".as_ref(), local_reserved.as_os_str()]);
        assert_exit_code("unsafe local reserved", &local_output, 1);
        assert_stderr_contains("unsafe local reserved", &local_output, "DuplicateName");

        let export_reserved = write_project(
            &format!("unsafe-export-reserved-{}", project_head),
            &[],
            &format!(
                "(module main (export {}))\n\n(fn main () -> i32\n  0)\n",
                head
            ),
        );
        let export_output = run_glagol(["check".as_ref(), export_reserved.as_os_str()]);
        assert_exit_code("unsafe export reserved", &export_output, 1);
        assert_stderr_contains("unsafe export reserved", &export_output, "Visibility");

        let import_reserved = write_project(
            &format!("unsafe-import-reserved-{}", project_head),
            &[("math", "(module math)\n\n(fn main () -> i32\n  0)\n")],
            &format!("(module main)\n\n(import math ({}))\n", head),
        );
        let import_output = run_glagol(["check".as_ref(), import_reserved.as_os_str()]);
        assert_exit_code("unsafe import reserved", &import_output, 1);
        assert_stderr_contains("unsafe import reserved", &import_output, "Visibility");
    }
}

fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
    write_project_with_manifest(
        name,
        &format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            name
        ),
        modules,
        main,
    )
}

fn write_project_with_manifest(
    name: &str,
    manifest: &str,
    modules: &[(&str, &str)],
    main: &str,
) -> PathBuf {
    let root = unique_path(name);
    let src = root.join("src");
    fs::create_dir_all(&src).expect("create project src");
    fs::write(root.join("slovo.toml"), manifest).expect("write manifest");
    if !main.is_empty() {
        fs::write(src.join("main.slo"), main).expect("write main module");
    }
    for (module, source) in modules {
        fs::write(src.join(format!("{}.slo", module)), source).expect("write module");
    }
    root
}

struct WorkspacePackageSpec<'a> {
    member: &'a str,
    manifest: &'a str,
    modules: &'a [(&'a str, &'a str)],
}

fn write_workspace(
    name: &str,
    workspace_manifest: &str,
    packages: &[WorkspacePackageSpec<'_>],
) -> PathBuf {
    let root = unique_path(name);
    fs::create_dir_all(&root).expect("create workspace root");
    fs::write(root.join("slovo.toml"), workspace_manifest).expect("write workspace manifest");
    for package in packages {
        let package_root = root.join(package.member);
        let src = package_root.join("src");
        fs::create_dir_all(&src).expect("create package src");
        fs::write(package_root.join("slovo.toml"), package.manifest)
            .expect("write package manifest");
        for (module, source) in package.modules {
            fs::write(src.join(format!("{}.slo", module)), source).expect("write package module");
        }
    }
    root
}

fn unique_path(name: &str) -> PathBuf {
    let id = NEXT_PROJECT_ID.fetch_add(1, Ordering::SeqCst);
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "glagol-project-mode-{}-{}-{}-{}",
        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_success_stdout(context: &str, output: Output, expected: &str) {
    assert_success(context, &output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        expected,
        "{} stdout mismatch",
        context
    );
    assert!(
        output.stderr.is_empty(),
        "{} wrote stderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stderr)
    );
}

fn assert_exit_code(context: &str, output: &Output, expected: i32) {
    assert_eq!(
        output.status.code(),
        Some(expected),
        "{} exit code mismatch\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
    );
}

fn assert_json_diagnostic_code(context: &str, project: &Path, code: &str) {
    let output = run_glagol([
        "check".as_ref(),
        "--json-diagnostics".as_ref(),
        project.as_os_str(),
    ]);
    assert_exit_code(context, &output, 1);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr
            .lines()
            .any(|line| line.contains(&format!("\"code\":{}", json_string(code)))),
        "{} JSON diagnostic did not contain code `{}`:\n{}",
        context,
        code,
        stderr
    );
}

fn assert_project_block_snapshot(context: &str, manifest: &str, project: &Path, expected: &str) {
    let block = project_block(manifest);
    let normalized = block.replace(&project.display().to_string(), "$PROJECT");
    assert_eq!(
        normalized, expected,
        "{} project manifest block mismatch\nfull manifest:\n{}",
        context, manifest
    );
}

fn project_block(manifest: &str) -> &str {
    let start = manifest
        .find("  (project\n")
        .expect("manifest did not contain project block");
    manifest[start..]
        .strip_suffix("\n)\n")
        .expect("manifest did not end with artifact-manifest close")
}

fn sexpr_string(value: &str) -> String {
    let mut out = String::from("\"");
    for ch in value.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            ch => out.push(ch),
        }
    }
    out.push('"');
    out
}

fn json_string(value: &str) -> String {
    let mut out = String::from("\"");
    for ch in value.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            '\u{08}' => out.push_str("\\b"),
            '\u{0c}' => out.push_str("\\f"),
            ch if ch <= '\u{1f}' => out.push_str(&format!("\\u{:04x}", ch as u32)),
            ch => out.push(ch),
        }
    }
    out.push('"');
    out
}

#[allow(dead_code)]
fn _assert_path_exists(path: &Path) {
    assert!(path.exists(), "path did not exist: {}", path.display());
}
