Documentation
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

fn lang_and_codegen(folder: &str) -> Option<&'static str> {
    match folder {
        "rust" => Some("rs"),
        "ts" => Some("ts"),
        "golang" => Some("go"),
        "golang-http" => Some("go-http"),
        "python" => Some("python"),
        "python-http" => Some("python-http"),
        "axum" => Some("axum"),
        "openapi" => Some("openapi"),
        "openrpc" => Some("openrpc"),
        _ => None,
    }
}

fn collect_idl_cases(root: &Path) -> Vec<(String, PathBuf)> {
    let mut cases = Vec::new();
    let entries = fs::read_dir(root).expect("read xidlc/tests");
    for entry in entries {
        let entry = entry.expect("dir entry");
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let lang = entry.file_name().to_string_lossy().to_string();
        if lang_and_codegen(&lang).is_none() {
            continue;
        }
        let files = fs::read_dir(&path).expect("read lang folder");
        for file in files {
            let file = file.expect("idl file");
            let case_path = file.path();
            if case_path.extension().and_then(|ext| ext.to_str()) != Some("idl") {
                continue;
            }
            cases.push((lang.clone(), case_path));
        }
    }
    cases.sort_by(|a, b| a.1.cmp(&b.1));
    cases
}

fn render_output(files: Vec<xidlc::driver::File>) -> String {
    let mut files = files;
    files.sort_by(|a, b| a.path().cmp(b.path()));

    let mut out = String::new();
    for file in files {
        out.push_str("===============\n");
        out.push_str(file.path());
        out.push_str("\n===============\n");
        out.push_str(file.content());
        if !file.content().ends_with('\n') {
            out.push('\n');
        }
    }
    out
}

fn render_single_output(path: &str, content: &str) -> String {
    let mut out = String::new();
    out.push_str("===============\n");
    out.push_str(path);
    out.push_str("\n===============\n");
    out.push_str(content);
    if !content.ends_with('\n') {
        out.push('\n');
    }
    out
}

fn case_props(folder: &str, case_name: &str) -> HashMap<String, serde_json::Value> {
    let mut props: HashMap<String, serde_json::Value> =
        HashMap::from([(String::from("enable_metadata"), false.into())]);
    if folder == "rust" && matches!(case_name, "simple_union" | "option" | "enum_serialize") {
        props.insert("enable_serialize".to_string(), false.into());
        props.insert("enable_deserialize".to_string(), false.into());
        props.insert("enable_render_header".to_string(), false.into());
    }
    props
}

async fn generate_case_output(
    folder: &str,
    case_name: &str,
    props: HashMap<String, serde_json::Value>,
) -> String {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
    let case_path = root.join(folder).join(format!("{case_name}.idl"));
    let lang = lang_and_codegen(folder).expect("supported folder");
    let source = fs::read_to_string(&case_path).expect("read idl");
    let mut generator = xidlc::driver::Generator::new(lang.to_string());
    let files = generator
        .generate_from_idl(
            &source,
            case_path
                .strip_prefix(env!("CARGO_MANIFEST_DIR"))
                .unwrap_or(&case_path),
            props,
        )
        .await
        .expect("generate");
    render_output(files)
}

#[tokio::test(flavor = "current_thread")]
async fn codegen_snapshots_from_idl_folders() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
    let cases = collect_idl_cases(&root);
    assert!(!cases.is_empty(), "no idl cases found under xidlc/tests/*");

    for (folder, case_path) in cases {
        let lang = lang_and_codegen(&folder).expect("supported folder");
        let source = fs::read_to_string(&case_path).expect("read idl");
        let case_name = case_path
            .file_stem()
            .and_then(|value| value.to_str())
            .expect("case stem");
        let props = case_props(&folder, case_name);
        let mut generator = xidlc::driver::Generator::new(lang.to_string());
        let files = generator
            .generate_from_idl(
                &source,
                case_path
                    .strip_prefix(env!("CARGO_MANIFEST_DIR"))
                    .unwrap_or(&case_path),
                props,
            )
            .await
            .expect("generate");
        let output = render_output(files);
        let snapshot_name = format!("{folder}__{case_name}");
        insta::assert_snapshot!(snapshot_name, output);
    }
}

#[cfg(feature = "cli")]
#[tokio::test(flavor = "current_thread")]
async fn skip_cdr_codec_matches_disabling_serialize_and_deserialize() {
    use clap::Parser;

    let args = xidlc::driver::ArgsGenerate {
        lang: "rust".to_string(),
        out_dir: ".".to_string(),
        client: false,
        server: true,
        dry_run: false,
        files: Vec::new(),
    };
    let mut expected_props = args.generator_props();
    expected_props.insert(String::from("enable_serialize"), false.into());
    expected_props.insert(String::from("enable_deserialize"), false.into());
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
    let case_path = root.join("rust").join("struct_simple.idl");
    let out_dir = std::env::temp_dir().join(format!(
        "xidlc-skip-cdr-codec-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("unix epoch")
            .as_nanos()
    ));
    fs::create_dir_all(&out_dir).expect("create output dir");
    let cli = xidlc::cli::Cli::parse_from([
        "xidlc",
        "--lang",
        "rust",
        "--out-dir",
        out_dir.to_str().expect("utf8 path"),
        "--skip-cdr-codec",
        case_path.to_str().expect("utf8 path"),
    ]);
    cli.run().await.expect("run cli");
    let actual = fs::read_to_string(out_dir.join("struct_simple.rs")).expect("read generated file");
    let expected = generate_case_output("rust", "struct_simple", expected_props).await;
    assert_eq!(render_single_output("struct_simple.rs", &actual), expected);
    fs::remove_dir_all(out_dir).expect("cleanup output dir");
}