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

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

#[test]
fn file_list_preserves_order_and_does_not_execute_bodies() {
    let fixture = write_fixture(
        "file-list",
        r#"
(module main)

(test "alpha first"
  true)

(test "beta would fail"
  false)
"#,
    );

    let output = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]);

    assert_success_stdout(
        "file test list",
        output,
        "test \"alpha first\" ... selected\n\
test \"beta would fail\" ... selected\n\
2 test(s) selected (total_discovered 2, selected 2, passed 0, failed 0, skipped 0, filter none)\n",
    );
}

#[test]
fn file_list_filter_marks_selected_and_skipped_in_order() {
    let fixture = write_fixture(
        "file-list-filter",
        r#"
(module main)

(test "alpha first"
  false)

(test "beta second"
  false)
"#,
    );

    let output = run_glagol([
        "test".as_ref(),
        "--list".as_ref(),
        fixture.as_os_str(),
        "--filter".as_ref(),
        "beta".as_ref(),
    ]);

    assert_success_stdout(
        "file filtered test list",
        output,
        "test \"alpha first\" ... skipped\n\
test \"beta second\" ... selected\n\
1 test(s) selected (total_discovered 2, selected 1, passed 0, failed 0, skipped 1, filter \"beta\")\n",
    );
}

#[test]
fn legacy_run_tests_list_matches_test_subcommand_list() {
    let fixture = write_fixture(
        "legacy-list",
        r#"
(module main)

(test "legacy first"
  false)

(test "legacy second"
  false)
"#,
    );

    let subcommand = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]);
    let legacy = run_glagol([
        "--run-tests".as_ref(),
        "--list".as_ref(),
        fixture.as_os_str(),
    ]);

    assert_success("test --list", &subcommand);
    assert_success("legacy --run-tests --list", &legacy);
    assert_eq!(
        legacy.stdout, subcommand.stdout,
        "legacy list output differed from test subcommand"
    );
    assert!(legacy.stderr.is_empty(), "legacy list wrote stderr");
}

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

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

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

#[test]
fn workspace_list_preserves_package_order_without_running_tests() {
    let workspace = write_workspace(
        "workspace-list",
        "[workspace]\nmembers = [\"packages/app\", \"packages/util\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/util",
                manifest: "[package]\nname = \"util\"\nversion = \"0.1.0\"\n",
                modules: &[(
                    "util",
                    "(module util (export answer))\n\n\
(fn answer () -> i32\n  41)\n\n\
(test \"util provider first\"\n  false)\n",
                )],
            },
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nutil = { path = \"../util\" }\n",
                modules: &[(
                    "main",
                    "(module main)\n\n\
(import util.util (answer))\n\n\
(fn main () -> i32\n  (answer))\n\n\
(test \"app consumer second\"\n  false)\n",
                )],
            },
        ],
    );

    let output = run_glagol(["test".as_ref(), "--list".as_ref(), workspace.as_os_str()]);

    assert_success_stdout(
        "workspace list",
        output,
        "test \"util provider first\" ... selected\n\
test \"app consumer second\" ... selected\n\
2 test(s) selected (total_discovered 2, selected 2, passed 0, failed 0, skipped 0, filter none)\n",
    );
}

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

(test "alpha first"
  true)

(test "beta second"
  true)
"#,
    );

    let subcommand = run_glagol(["test".as_ref(), fixture.as_os_str()]);
    assert_success_stdout(
        "ordinary test",
        subcommand,
        "test \"alpha first\" ... ok\n\
test \"beta second\" ... ok\n\
2 test(s) passed\n",
    );

    let legacy = run_glagol(["--run-tests".as_ref(), fixture.as_os_str()]);
    assert_success_stdout(
        "ordinary legacy run-tests",
        legacy,
        "test \"alpha first\" ... ok\n\
test \"beta second\" ... ok\n\
2 test(s) passed\n",
    );
}

#[test]
fn list_mode_reuses_checked_diagnostics_for_invalid_tests() {
    let fixture = write_fixture(
        "invalid-list",
        r#"
(module main)

(test "not bool"
  1)
"#,
    );

    let output = run_glagol(["test".as_ref(), "--list".as_ref(), fixture.as_os_str()]);

    assert_failure_stderr_contains(
        "invalid list",
        &output,
        &[
            "TestExpressionNotBool",
            "Expected:",
            "bool",
            "Found:",
            "i32",
        ],
    );
    assert!(
        output.stdout.is_empty(),
        "invalid list wrote stdout:\n{}",
        String::from_utf8_lossy(&output.stdout)
    );
}

#[test]
fn list_flag_is_rejected_outside_test_mode() {
    let fixture = write_fixture(
        "list-outside-test",
        r#"
(module main)

(fn main () -> i32
  0)
"#,
    );

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

    assert_failure_stderr_contains(
        "check --list",
        &output,
        &["`--list` is only supported with `test` and `--run-tests`"],
    );
}

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

fn write_project(name: &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"),
        format!(
            "[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            name
        ),
    )
    .expect("write project manifest");
    fs::write(src.join("main.slo"), main).expect("write project main");
    for (module, source) in modules {
        fs::write(src.join(format!("{}.slo", module)), source).expect("write project 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 workspace package src");
        fs::write(package_root.join("slovo.toml"), package.manifest)
            .expect("write workspace package manifest");
        for (module, source) in package.modules {
            fs::write(src.join(format!("{}.slo", module)), source)
                .expect("write workspace package module");
        }
    }
    root
}

fn unique_path(name: &str) -> PathBuf {
    let id = NEXT_FIXTURE_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-test-discovery-beta19-{}-{}-{}-{}",
        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_failure_stderr_contains(context: &str, output: &Output, expected: &[&str]) {
    assert!(
        !output.status.success(),
        "{} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    for needle in expected {
        assert!(
            stderr.contains(needle),
            "{} stderr missing `{}`:\n{}",
            context,
            needle,
            stderr
        );
    }
}
