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

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

const CASES: &[ReservedCase] = &[
    ReservedCase {
        name: "generic-function",
        source: r#"
(module main)

(fn id (type_params T) ((value T)) -> T
  value)

(fn main () -> i32
  0)
"#,
        code: "UnsupportedGenericFunction",
        message: "generic function declarations are reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "generic-type-alias",
        source: r#"
(module main)

(type VecOf (type_params T) (vec T))

(fn main () -> i32
  0)
"#,
        code: "UnsupportedGenericTypeAlias",
        message: "parameterized type aliases are reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "generic-type-parameter",
        source: r#"
(module main)

(fn main () -> i32
  (let xs (vec T) (std.vec.i32.empty))
  0)
"#,
        code: "UnsupportedGenericTypeParameter",
        message: "generic type parameter `T` is reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "generic-vector-spelling",
        source: r#"
(module main)

(fn main () -> (vec)
  (std.vec.i32.empty))
"#,
        code: "UnsupportedGenericTypeParameter",
        message: "generic vector syntax is reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "map-type",
        source: r#"
(module main)

(fn main () -> (map string i32)
  0)
"#,
        code: "UnsupportedMapType",
        message: "`map` types are reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "set-type",
        source: r#"
(module main)

(fn main () -> (set string)
  0)
"#,
        code: "UnsupportedSetType",
        message: "`set` types are reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "std-vec-empty",
        source: r#"
(module main)

(fn main () -> i32
  (std.vec.empty i32)
  0)
"#,
        code: "UnsupportedGenericStandardLibraryCall",
        message:
            "generic standard-library call `std.vec.empty` is reserved but not supported in the current beta",
    },
    ReservedCase {
        name: "std-result-map",
        source: r#"
(module main)

(fn main () -> i32
  (std.result.map (ok string i32 "a") mapper)
  0)
"#,
        code: "UnsupportedGenericStandardLibraryCall",
        message:
            "generic standard-library call `std.result.map` is reserved but not supported in the current beta",
    },
];

#[test]
fn check_fmt_and_project_paths_reject_reserved_generic_collection_surface() {
    for case in CASES {
        let fixture = write_fixture(case);

        let check = run_glagol([OsStr::new("check"), fixture.as_os_str()]);
        assert_rejection(&format!("{} check", case.name), &check, case);

        let fmt = run_glagol([
            OsStr::new("fmt"),
            OsStr::new("--check"),
            fixture.as_os_str(),
        ]);
        assert_rejection(&format!("{} fmt --check", case.name), &fmt, case);

        let project = write_project(case);
        let project_check = run_glagol([OsStr::new("check"), project.as_os_str()]);
        assert_rejection(
            &format!("{} project check", case.name),
            &project_check,
            case,
        );
    }
}

struct ReservedCase {
    name: &'static str,
    source: &'static str,
    code: &'static str,
    message: &'static str,
}

fn assert_rejection(context: &str, output: &Output, case: &ReservedCase) {
    assert_eq!(
        output.status.code(),
        Some(1),
        "{} exit code mismatch\nstdout:\n{}\nstderr:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        output.stdout.is_empty(),
        "{} wrote stdout:\n{}",
        context,
        String::from_utf8_lossy(&output.stdout)
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains(&format!("error[{}]", case.code)),
        "{} human diagnostic did not contain code `{}`:\n{}",
        context,
        case.code,
        stderr
    );
    assert!(
        stderr.contains(&format!("  (code {})", case.code)),
        "{} machine diagnostic did not contain code `{}`:\n{}",
        context,
        case.code,
        stderr
    );
    assert!(
        stderr.contains(case.message),
        "{} stderr did not contain message `{}`:\n{}",
        context,
        case.message,
        stderr
    );
    assert!(
        !stderr.contains("beta.9"),
        "{} stderr still used beta.9 wording:\n{}",
        context,
        stderr
    );
}

fn write_fixture(case: &ReservedCase) -> PathBuf {
    let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
    let path = unique_base_path(&format!("file-{}-{}", id, case.name)).with_extension("slo");
    fs::write(&path, case.source)
        .unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}

fn write_project(case: &ReservedCase) -> PathBuf {
    let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
    let root = unique_base_path(&format!("project-{}-{}", id, case.name));
    let src = root.join("src");
    fs::create_dir_all(&src).unwrap_or_else(|err| panic!("create `{}`: {}", src.display(), err));
    fs::write(
        root.join("slovo.toml"),
        format!(
            "[project]\nname = \"reserved-beta15-{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
            case.name
        ),
    )
    .unwrap_or_else(|err| panic!("write project manifest for `{}`: {}", case.name, err));
    fs::write(src.join("main.slo"), case.source)
        .unwrap_or_else(|err| panic!("write project source for `{}`: {}", case.name, err));
    root
}

fn unique_base_path(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "glagol-reserved-beta15-{}-{}-{}",
        std::process::id(),
        nanos,
        name
    ))
}

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