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

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

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

#[test]
fn beta_project_proves_general_purpose_cli_surface() {
    let project = unique_path("beta-1-0-0-project");
    let std_path = slovo_std_path();

    let scaffold = run_glagol(["new".as_ref(), project.as_os_str()]);
    assert_success("beta scaffold", &scaffold);

    fs::write(
        project.join("src/model.slo"),
        r#"(module model (export Mode Report make_report total_score mode_name second_label report_name report_mode_text))

(import std.vec_i32 (pair sum))

(import std.num (i64_to_string))

(enum Mode
  Dev
  (Code i64))

(struct Report
  (name string)
  (scores (vec i32))
  (ratio f64)
  (active bool)
  (mode Mode)
  (labels (array string 2)))

(fn make_report () -> Report
  (Report (name "beta") (scores (pair 3 4)) (ratio 1.5) (active true) (mode (Mode.Code 7i64)) (labels (array string "slovo" "glagol"))))

(fn total_score ((report Report)) -> i32
  (sum (. report scores)))

(fn mode_name ((mode Mode)) -> string
  (match mode
    ((Mode.Dev)
      "dev")
    ((Mode.Code payload)
      (i64_to_string payload))))

(fn second_label ((report Report)) -> string
  (index (. report labels) 1))

(fn report_name () -> string
  (. (make_report) name))

(fn report_mode_text () -> string
  (mode_name (. (make_report) mode)))
"#,
    )
    .expect("write beta model");

    fs::write(
        project.join("src/helpers.slo"),
        r#"(module helpers (export title_text parse_port_or port_parses big_counter_text parse_ratio parse_enabled))

(import std.string (concat parse_u32_result parse_u32_or parse_f64_or parse_bool_or))

(import std.num (u64_to_string))

(import std.result (is_ok_u32))

(fn title_text ((left string) (right string)) -> string
  (concat left right))

(fn parse_port_or ((text string) (fallback u32)) -> u32
  (parse_u32_or text fallback))

(fn port_parses ((text string)) -> bool
  (is_ok_u32 (parse_u32_result text)))

(fn big_counter_text ((value u64)) -> string
  (u64_to_string value))

(fn parse_ratio ((text string) (fallback f64)) -> f64
  (parse_f64_or text fallback))

(fn parse_enabled ((text string) (fallback bool)) -> bool
  (parse_bool_or text fallback))
"#,
    )
    .expect("write beta helpers");

    fs::write(
        project.join("src/main.slo"),
        r#"(module main)

(import model (make_report total_score second_label report_name report_mode_text))

(import helpers (title_text parse_port_or port_parses big_counter_text parse_ratio parse_enabled))

(fn loop_sum ((limit i32)) -> i32
  (var current i32 0)
  (var total i32 0)
  (while (< current limit)
    (set total (+ total current))
    (set current (+ current 1)))
  total)

(fn main () -> i32
  (std.io.print_string (title_text (report_name) "!"))
  0)

(test "beta project string helpers"
  (= (title_text "slo" "vo") "slovo"))

(test "beta project unsigned helpers"
  (if (= (parse_port_or "8080" 1u32) 8080u32)
    (if (port_parses "8080")
      (= (big_counter_text 42u64) "42")
      false)
    false))

(test "beta project float bool helpers"
  (if (= (parse_ratio "3.5" 0.0) 3.5)
    (parse_enabled "true" false)
    false))

(test "beta project data types"
  (if (= (total_score (make_report)) 7)
    (if (= (report_mode_text) "7")
      (= (second_label (make_report)) "glagol")
      false)
    false))

(test "beta project control flow"
  (= (loop_sum 5) 10))
"#,
    )
    .expect("write beta main");

    let check = run_glagol_with_env(
        ["check".as_ref(), project.as_os_str()],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    assert_success("beta project check", &check);

    let fmt_check = run_glagol_with_env(
        ["fmt".as_ref(), "--check".as_ref(), project.as_os_str()],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    assert_success("beta project fmt --check", &fmt_check);

    let test = run_glagol_with_env(
        ["test".as_ref(), project.as_os_str()],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    assert_success("beta project test", &test);
    assert_eq!(
        String::from_utf8_lossy(&test.stdout),
        "test \"beta project string helpers\" ... ok\n\
test \"beta project unsigned helpers\" ... ok\n\
test \"beta project float bool helpers\" ... ok\n\
test \"beta project data types\" ... ok\n\
test \"beta project control flow\" ... ok\n\
5 test(s) passed\n"
    );

    let docs = unique_path("beta-1-0-0-project-docs");
    let doc = run_glagol_with_env(
        [
            "doc".as_ref(),
            project.as_os_str(),
            "-o".as_ref(),
            docs.as_os_str(),
        ],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    assert_success("beta project doc", &doc);
    let doc_index = fs::read_to_string(docs.join("index.md")).expect("read beta docs");
    assert!(doc_index.contains("# Project "));
    assert!(doc_index.contains("## Module model"));
    assert!(doc_index.contains("## Module helpers"));
    assert!(doc_index.contains("## Module main"));
    assert!(doc_index.contains("beta project control flow"));

    let binary = unique_path("beta-1-0-0-project-bin");
    let build = run_glagol_with_env(
        [
            "build".as_ref(),
            project.as_os_str(),
            "-o".as_ref(),
            binary.as_os_str(),
        ],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    if build.status.success() {
        let run = Command::new(&binary).output().expect("run beta project");
        assert_success("beta project binary", &run);
        assert_eq!(String::from_utf8_lossy(&run.stdout), "beta!\n");
    } else {
        assert_stderr_contains("beta project build", &build, "ToolchainUnavailable");
    }
}

#[test]
fn beta_workspace_proves_local_package_dependency_flow() {
    let workspace = write_workspace(
        "beta-1-0-0-workspace",
        "[workspace]\nmembers = [\"packages/app\", \"packages/libutil\"]\n",
        &[
            WorkspacePackageSpec {
                member: "packages/libutil",
                manifest: "[package]\nname = \"libutil\"\nversion = \"0.1.0\"\n",
                modules: &[(
                    "text",
                    r#"(module text (export Status stamp parse_count render_total))

(import std.string (concat parse_u64_or))

(import std.num (u64_to_string))

(enum Status
  Ready
  Busy)

(fn stamp ((status Status)) -> string
  (match status
    ((Status.Ready)
      "ready")
    ((Status.Busy)
      "busy")))

(fn parse_count ((text string)) -> u64
  (parse_u64_or text 0u64))

(fn render_total ((label string) (value u64)) -> string
  (concat label (u64_to_string value)))
"#,
                )],
            },
            WorkspacePackageSpec {
                member: "packages/app",
                manifest: "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\nlibutil = { path = \"../libutil\" }\n",
                modules: &[(
                    "main",
                    r#"(module main)

(import libutil.text (Status stamp parse_count render_total))

(fn main () -> i32
  (if (= (parse_count "12") 12u64)
    0
    1))

(test "beta workspace package import"
  (if (= (stamp (Status.Ready)) "ready")
    (= (render_total "n=" 12u64) "n=12")
    false))
"#,
                )],
            },
        ],
    );
    let std_path = slovo_std_path();

    let check = run_glagol_with_env(
        ["check".as_ref(), workspace.as_os_str()],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    assert_success_stdout("beta workspace check", check, "");

    let test = run_glagol_with_env(
        ["test".as_ref(), workspace.as_os_str()],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    assert_success_stdout(
        "beta workspace test",
        test,
        "test \"beta workspace package import\" ... ok\n1 test(s) passed\n",
    );

    let binary = unique_path("beta-1-0-0-workspace-bin");
    let build = run_glagol_with_env(
        [
            "build".as_ref(),
            workspace.as_os_str(),
            "-o".as_ref(),
            binary.as_os_str(),
        ],
        &[("SLOVO_STD_PATH", std_path.as_os_str())],
    );
    if build.status.success() {
        let run = Command::new(&binary)
            .output()
            .expect("run beta workspace binary");
        assert_success("beta workspace binary", &run);
        assert!(run.stdout.is_empty(), "beta workspace binary wrote stdout");
    } else {
        assert_stderr_contains("beta workspace build", &build, "ToolchainUnavailable");
    }
}

fn slovo_std_path() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../lib/std")
        .canonicalize()
        .expect("canonicalize slovo std path")
}

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_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-beta-1-0-0-{}-{}-{}-{}",
        std::process::id(),
        nanos,
        id,
        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")
}

fn run_glagol_with_env<I, S>(args: I, envs: &[(&str, &OsStr)]) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let mut command = Command::new(env!("CARGO_BIN_EXE_glagol"));
    command.args(args);
    for (key, value) in envs {
        command.env(key, value);
    }
    command.output().expect("run glagol with env")
}

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_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
    );
}
