use assert_cmd::Command;

fn bin() -> Command {
    Command::cargo_bin("fulgur-chart").unwrap()
}

#[test]
fn renders_bar_to_svg_stdout() {
    let spec = r#"{"type":"bar","data":{"labels":["a","b"],"datasets":[{"data":[1,2]}]}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-"])
        .write_stdin(spec)
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"));
    assert!(s.trim_end().ends_with("</svg>"));
}

#[test]
fn renders_bar_to_file() {
    let dir = tempfile_dir();
    let spec_path = dir.join("spec.json");
    let out_path = dir.join("out.svg");
    std::fs::write(
        &spec_path,
        r#"{"type":"bar","data":{"labels":["a"],"datasets":[{"data":[1]}]}}"#,
    )
    .unwrap();
    bin()
        .args([
            "render",
            spec_path.to_str().unwrap(),
            "-o",
            out_path.to_str().unwrap(),
        ])
        .assert()
        .success();
    let s = std::fs::read_to_string(&out_path).unwrap();
    assert!(s.starts_with("<svg"));
}

#[test]
fn invalid_json_exits_1() {
    bin()
        .args(["render", "-", "-o", "-"])
        .write_stdin("{ not json")
        .assert()
        .failure()
        .code(1);
}

#[test]
fn strict_unknown_key_exits_2() {
    let spec = r#"{"type":"bar","data":{"labels":[],"datasets":[]},"wat":1}"#;
    bin()
        .args(["render", "-", "-o", "-", "--strict"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(2);
    // 非strictは成功
    bin()
        .args(["render", "-", "-o", "-"])
        .write_stdin(spec)
        .assert()
        .success();
}

#[test]
fn renders_bar_to_png_file() {
    let dir = tempfile_dir();
    let out = dir.join("out.png");
    let spec = r#"{"type":"bar","data":{"labels":["a","b"],"datasets":[{"data":[1,2]}]}}"#;
    bin()
        .args([
            "render",
            "-",
            "-o",
            out.to_str().unwrap(),
            "--format",
            "png",
        ])
        .write_stdin(spec)
        .assert()
        .success();
    let bytes = std::fs::read(&out).unwrap();
    assert_eq!(&bytes[0..4], &[0x89, b'P', b'N', b'G']);
}

#[test]
fn renders_to_png_stdout() {
    // -o - かつ --format png のとき、stdout 先頭 4 バイトが PNG シグネチャ。
    let spec = r#"{"type":"bar","data":{"labels":["a","b"],"datasets":[{"data":[1,2]}]}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-", "--format", "png"])
        .write_stdin(spec)
        .assert()
        .success();
    let bytes = &out.get_output().stdout;
    assert_eq!(&bytes[0..4], &[0x89, b'P', b'N', b'G']);
}

#[test]
fn missing_input_file_exits_1() {
    bin()
        .args(["render", "/nonexistent/xyz.json", "-o", "-"])
        .assert()
        .failure()
        .code(1);
}

/// 同梱フォントの絶対パス(CLI クレートから見た相対で解決)。
const BUNDLED_FONT: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../fulgur-chart/assets/fonts/NotoSansJP-Regular.otf"
);

#[test]
fn font_flag_with_bundled_font_succeeds() {
    let spec = r#"{"type":"bar","data":{"labels":["a","b"],"datasets":[{"data":[1,2]}]}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-", "--font", BUNDLED_FONT])
        .write_stdin(spec)
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"));
}

#[test]
fn font_flag_missing_file_fails() {
    let spec = r#"{"type":"bar","data":{"labels":["a"],"datasets":[{"data":[1]}]}}"#;
    bin()
        .args(["render", "-", "-o", "-", "--font", "/no/such.ttf"])
        .write_stdin(spec)
        .assert()
        .failure();
}

/// 最小の Vega-Lite bar spec。
const MINIMAL_VEGALITE_BAR: &str = r#"{"mark":"bar","data":{"values":[{"c":"A","v":3},{"c":"B","v":5}]},"encoding":{"x":{"field":"c","type":"nominal"},"y":{"field":"v","type":"quantitative"}}}"#;

#[test]
fn renders_vegalite_spec() {
    let out = bin()
        .args(["render", "-", "-o", "-", "--dsl", "vegalite"])
        .write_stdin(MINIMAL_VEGALITE_BAR)
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"));
}

#[test]
fn vegalite_strict_unknown_key_exits_2() {
    let spec = r#"{"wat":1,"mark":"bar","data":{"values":[{"c":"A","v":3},{"c":"B","v":5}]},"encoding":{"x":{"field":"c","type":"nominal"},"y":{"field":"v","type":"quantitative"}}}"#;
    bin()
        .args(["render", "-", "-o", "-", "--dsl", "vegalite", "--strict"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(2);
}

#[test]
fn unknown_dsl_exits_1() {
    bin()
        .args(["render", "-", "-o", "-", "--dsl", "bogus"])
        .write_stdin(MINIMAL_BAR_A)
        .assert()
        .failure()
        .code(1);
}

// tempdir ヘルパ: tempfile クレートを使わず std::env::temp_dir に一意ディレクトリを作る
fn tempfile_dir() -> std::path::PathBuf {
    let base = std::env::temp_dir().join(format!("fulgur_chart_cli_test_{}", std::process::id()));
    std::fs::create_dir_all(&base).unwrap();
    base
}

// テスト名ごとに固定・隔離されたディレクトリを作る（並列実行時の競合を防ぐ）
fn tempfile_dir_for(name: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!("fulgur_chart_test_{name}"));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

// バッチ用 tempdir: テスト名ごとに固定ディレクトリを使い、開始時に消してクリーンスレートにする。
fn batch_dir(name: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!("fulgur_batch_{name}"));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

const MINIMAL_BAR_A: &str =
    r#"{"type":"bar","data":{"labels":["a","b"],"datasets":[{"data":[1,2]}]}}"#;
const MINIMAL_BAR_B: &str =
    r#"{"type":"bar","data":{"labels":["x","y","z"],"datasets":[{"data":[3,4,5]}]}}"#;

#[test]
fn batch_renders_multiple_svgs() {
    let dir = batch_dir("batch_renders_multiple_svgs");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    let a = in_dir.join("a.json");
    let b = in_dir.join("b.json");
    std::fs::write(&a, MINIMAL_BAR_A).unwrap();
    std::fs::write(&b, MINIMAL_BAR_B).unwrap();

    bin()
        .args([
            "render",
            a.to_str().unwrap(),
            b.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .success();

    let sa = std::fs::read_to_string(out_dir.join("a.svg")).unwrap();
    let sb = std::fs::read_to_string(out_dir.join("b.svg")).unwrap();
    assert!(sa.starts_with("<svg"));
    assert!(sb.starts_with("<svg"));
}

#[test]
fn batch_renders_png() {
    let dir = batch_dir("batch_renders_png");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    let a = in_dir.join("a.json");
    let b = in_dir.join("b.json");
    std::fs::write(&a, MINIMAL_BAR_A).unwrap();
    std::fs::write(&b, MINIMAL_BAR_B).unwrap();

    bin()
        .args([
            "render",
            a.to_str().unwrap(),
            b.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
            "--format",
            "png",
        ])
        .assert()
        .success();

    let bytes = std::fs::read(out_dir.join("a.png")).unwrap();
    assert_eq!(&bytes[0..4], &[0x89, b'P', b'N', b'G']);
}

#[test]
fn batch_matches_single() {
    let dir = batch_dir("batch_matches_single");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    let a = in_dir.join("a.json");
    std::fs::write(&a, MINIMAL_BAR_A).unwrap();

    // バッチ出力。
    bin()
        .args([
            "render",
            a.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .success();
    let batch_bytes = std::fs::read(out_dir.join("a.svg")).unwrap();

    // 単一モード(stdout)出力。
    let single = bin()
        .args(["render", a.to_str().unwrap(), "-o", "-"])
        .assert()
        .success();
    let single_bytes = single.get_output().stdout.clone();

    // バッチと単一はバイト一致。
    assert_eq!(batch_bytes, single_bytes);
}

#[test]
fn batch_requires_out_dir_for_multiple() {
    let dir = batch_dir("batch_requires_out_dir_for_multiple");
    let a = dir.join("a.json");
    let b = dir.join("b.json");
    std::fs::write(&a, MINIMAL_BAR_A).unwrap();
    std::fs::write(&b, MINIMAL_BAR_B).unwrap();

    bin()
        .args(["render", a.to_str().unwrap(), b.to_str().unwrap()])
        .assert()
        .failure();
}

#[test]
fn out_dir_conflicts_with_output() {
    let dir = batch_dir("out_dir_conflicts_with_output");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    let a = in_dir.join("a.json");
    std::fs::write(&a, MINIMAL_BAR_A).unwrap();

    bin()
        .args([
            "render",
            a.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
            "-o",
            "x.svg",
        ])
        .assert()
        .failure();
}

#[test]
fn batch_rejects_stem_collision() {
    let dir = batch_dir("batch_rejects_stem_collision");
    let d1 = dir.join("d1");
    let d2 = dir.join("d2");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&d1).unwrap();
    std::fs::create_dir_all(&d2).unwrap();
    // 別ディレクトリの同名 stem (a.json) → 出力 a.<ext> が衝突 → 書き出し前に fail-fast。
    let a1 = d1.join("a.json");
    let a2 = d2.join("a.json");
    std::fs::write(&a1, MINIMAL_BAR_A).unwrap();
    std::fs::write(&a2, MINIMAL_BAR_A).unwrap();

    bin()
        .args([
            "render",
            a1.to_str().unwrap(),
            a2.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .code(1);
    // 衝突検出は create_dir_all より前なので、成果物も out_dir も作らない。
    assert!(!out_dir.join("a.svg").exists(), "部分出力を残さない");
}

#[test]
fn batch_rejects_stdin_before_any_output() {
    let dir = batch_dir("batch_rejects_stdin_before_any_output");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    let valid = in_dir.join("valid.json");
    std::fs::write(&valid, MINIMAL_BAR_A).unwrap();

    // `valid.json -` の順でも、検証は出力前に行われ `-` で失敗し、部分出力を残さない。
    bin()
        .args([
            "render",
            valid.to_str().unwrap(),
            "-",
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .code(1);
    assert!(
        !out_dir.join("valid.svg").exists(),
        "失敗時に部分成果物を残さない"
    );
}

#[test]
fn batch_render_error_leaves_no_partial_output() {
    let dir = batch_dir("batch_render_error_leaves_no_partial_output");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    let good = in_dir.join("good.json");
    let bad = in_dir.join("bad.json");
    std::fs::write(&good, MINIMAL_BAR_A).unwrap();
    std::fs::write(&bad, "{ not valid json").unwrap();

    // good→bad の順でも、二相処理(全件レンダリング後に一括書き出し)により
    // 2 件目の JSON エラーで失敗し、good の成果物も書き出されない。
    bin()
        .args([
            "render",
            good.to_str().unwrap(),
            bad.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure();
    assert!(
        !out_dir.join("good.svg").exists(),
        "レンダリング失敗時に先行成果物を残さない"
    );
}

#[test]
fn batch_preflight_blocks_when_output_is_directory() {
    let dir = batch_dir("batch_preflight_blocks_when_output_is_directory");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    std::fs::create_dir_all(&out_dir).unwrap();
    // b.svg を「ディレクトリ」として作り、2 件目の出力先を塞ぐ。
    std::fs::create_dir_all(out_dir.join("b.svg")).unwrap();
    let a = in_dir.join("a.json");
    let b = in_dir.join("b.json");
    std::fs::write(&a, MINIMAL_BAR_A).unwrap();
    std::fs::write(&b, MINIMAL_BAR_A).unwrap();

    bin()
        .args([
            "render",
            a.to_str().unwrap(),
            b.to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .code(3);
    // preflight で書き込み前に中止するため、先行する a.svg も書かれない。
    assert!(
        !out_dir.join("a.svg").exists(),
        "preflight 失敗時に部分出力を残さない"
    );
}

#[test]
fn schema_chartjs_is_valid_json() {
    let output = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["schema"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let text = String::from_utf8(output.stdout).unwrap();
    let v: serde_json::Value = serde_json::from_str(&text).expect("not valid JSON");
    // $schema フィールドが存在する
    assert!(v.get("$schema").is_some(), "missing $schema");
    // oneOf / anyOf のいずれかが存在する（discriminated union）
    let has_union = v.get("oneOf").is_some() || v.get("anyOf").is_some();
    assert!(has_union, "expected union schema (oneOf or anyOf)");
}

#[test]
fn schema_chartjs_includes_progress() {
    let output = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["schema"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let text = String::from_utf8(output.stdout).unwrap();
    assert!(
        text.contains("progress"),
        "schema should mention progress variant"
    );
    // QuickChart の正式名 progressBar も schema に現れること(serde alias)。
    // パーサが受理する型を schema 検証側も受理できるよう整合させる。
    assert!(
        text.contains("progressBar"),
        "schema should include the progressBar alias"
    );
}

#[test]
fn schema_chartjs_includes_gauge() {
    let out = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["schema"])
        .output()
        .unwrap();
    assert!(out.status.success(), "schema command should exit 0");
    let s = String::from_utf8(out.stdout).unwrap();
    assert!(s.contains("gauge"), "schema should mention gauge");
    assert!(
        s.contains("radialGauge"),
        "schema should mention radialGauge"
    );
}

#[test]
fn schema_vegalite_is_valid_json() {
    let output = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["schema", "--dsl", "vegalite"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let text = String::from_utf8(output.stdout).unwrap();
    let _: serde_json::Value = serde_json::from_str(&text).expect("not valid JSON");
}

#[test]
fn schema_unknown_dsl_exits_1() {
    let output = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["schema", "--dsl", "unknown"])
        .output()
        .unwrap();
    assert_eq!(output.status.code(), Some(1));
}

#[test]
fn auto_detect_chartjs_without_dsl_flag() {
    let spec = r#"{"type":"bar","data":{"labels":["A"],"datasets":[{"data":[1]}]}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-"])
        .write_stdin(spec)
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"));
}

#[test]
fn auto_detect_vegalite_without_dsl_flag() {
    let spec = r#"{"mark":"bar","data":{"values":[{"x":"A","y":1}]},"encoding":{"x":{"field":"x"},"y":{"field":"y"}}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-"])
        .write_stdin(spec)
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"));
}

#[test]
fn auto_detect_unknown_spec_exits_1() {
    let spec = r#"{"labels":["A"],"values":[1]}"#;
    let out = bin()
        .args(["render", "-", "-o", "-"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(1);
    let stderr = String::from_utf8(out.get_output().stderr.clone()).unwrap();
    assert!(
        stderr.contains("auto-detect"),
        "stderr should contain 'auto-detect', got: {stderr}"
    );
}

// --- 入力上限（guard モジュールが CLI 経由で正しく動作することを確認）---

#[test]
fn oversized_width_exits_1() {
    // width が MAX_DIMENSION_PX (32768) を超えると exit 1。
    let spec = r#"{"type":"bar","data":{"labels":["a"],"datasets":[{"data":[1]}]}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-", "--width", "32769"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(1);
    let stderr = String::from_utf8(out.get_output().stderr.clone()).unwrap();
    assert!(
        stderr.contains("width"),
        "stderr should mention 'width', got: {stderr}"
    );
}

#[test]
fn oversized_height_exits_1() {
    // height が MAX_DIMENSION_PX (32768) を超えると exit 1。
    let spec = r#"{"type":"bar","data":{"labels":["a"],"datasets":[{"data":[1]}]}}"#;
    let out = bin()
        .args(["render", "-", "-o", "-", "--height", "32769"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(1);
    let stderr = String::from_utf8(out.get_output().stderr.clone()).unwrap();
    assert!(
        stderr.contains("height"),
        "stderr should mention 'height', got: {stderr}"
    );
}

#[test]
fn zero_width_exits_1() {
    // width=0 (MIN_DIMENSION_PX 未満) も exit 1。
    let spec = r#"{"type":"bar","data":{"labels":["a"],"datasets":[{"data":[1]}]}}"#;
    bin()
        .args(["render", "-", "-o", "-", "--width", "0"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(1);
}

// --- Jsonnet サポート ---

const MINIMAL_JSONNET_STDIN: &str = r#"
// コメント付き bar チャート
{
  type: "bar",
  data: {
    labels: ["A", "B"],
    datasets: [{ data: [1, 2] }],
  },
}
"#;

#[test]
fn jsonnet_stdin_renders_svg() {
    let out = bin()
        .args(["render", "-", "-o", "-", "--jsonnet"])
        .write_stdin(MINIMAL_JSONNET_STDIN)
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"), "expected SVG, got: {s}");
}

#[test]
fn jsonnet_stdin_import_exits_1() {
    let dir = tempfile_dir_for("jsonnet_stdin_import_exits_1");
    std::fs::write(dir.join("secret.txt"), "SENTINEL_LOCAL_FILE_DISCLOSURE").unwrap();

    bin()
        .current_dir(&dir)
        .args(["render", "-", "-o", "-", "--jsonnet"])
        .write_stdin(
            r#"{
  type: "bar",
  data: {
    labels: [importstr "secret.txt"],
    datasets: [{ data: [1] }],
  },
}
"#,
        )
        .assert()
        .failure()
        .code(1);
}

#[test]
fn jsonnet_flag_with_file_path_exits_1() {
    // ファイルと --jsonnet の組み合わせは不正（拡張子を使うべき）
    let dir = tempfile_dir_for("jsonnet_flag_with_file_path_exits_1");
    let spec = dir.join("spec.json");
    std::fs::write(&spec, MINIMAL_BAR_A).unwrap();
    bin()
        .args(["render", spec.to_str().unwrap(), "-o", "-", "--jsonnet"])
        .assert()
        .failure()
        .code(1);
}

// --- .jsonnet ファイルの自動検出 ---

const MINIMAL_JSONNET_FILE: &str = r#"
// Jsonnet で書いた bar チャート
{
  type: "bar",
  data: {
    labels: ["X", "Y"],
    datasets: [{ data: [3, 7] }],
  },
}
"#;

#[test]
fn jsonnet_file_renders_svg() {
    let dir = tempfile_dir_for("jsonnet_file_renders_svg");
    let spec = dir.join("spec.jsonnet");
    std::fs::write(&spec, MINIMAL_JSONNET_FILE).unwrap();
    let out = bin()
        .args(["render", spec.to_str().unwrap(), "-o", "-"])
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"), "expected SVG, got: {s}");
}

#[test]
fn jsonnet_file_path_traversal_exits_1() {
    let dir = tempfile_dir_for("jsonnet_file_path_traversal_exits_1");
    let sub = dir.join("sub");
    std::fs::create_dir_all(&sub).unwrap();
    std::fs::write(dir.join("secret.txt"), "SENTINEL_PARENT_FILE").unwrap();

    std::fs::write(
        sub.join("chart.jsonnet"),
        r#"{
  type: "bar",
  data: {
    labels: [importstr "../secret.txt"],
    datasets: [{ data: [1] }],
  },
}
"#,
    )
    .unwrap();

    bin()
        .args([
            "render",
            sub.join("chart.jsonnet").to_str().unwrap(),
            "-o",
            "-",
        ])
        .assert()
        .failure()
        .code(1);
}

// --- inspect サブコマンド（意味モデルを JSON で出力）---

#[test]
fn inspect_bar_emits_model_json() {
    // bar spec を inspect して JSON モデルを stdout に得る。
    let spec = r#"{"type":"bar","data":{"labels":["1月","2月","3月"],"datasets":[{"label":"売上","data":[120,200,150]}]}}"#;
    let out = bin()
        .args(["inspect", "-", "-o", "-"])
        .write_stdin(spec)
        .assert()
        .success();
    let bytes = out.get_output().stdout.clone();
    let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid JSON");
    assert_eq!(v["meta"]["type"], "bar");
    assert!(!v["series"].as_array().unwrap().is_empty());
    assert!(v["axes"]["y"]["ticks"].is_array());
}

#[test]
fn jsonnet_file_with_import_renders_svg() {
    let dir = tempfile_dir_for("jsonnet_file_with_import_renders_svg");

    // ライブラリファイル
    std::fs::write(
        dir.join("colors.libsonnet"),
        r#"{ red: "rgb(255,0,0)", blue: "rgb(0,0,255)" }"#,
    )
    .unwrap();

    // メインスペック（import あり）
    let spec = dir.join("spec.jsonnet");
    std::fs::write(
        &spec,
        r#"
local colors = import 'colors.libsonnet';
{
  type: "bar",
  data: {
    labels: ["A"],
    datasets: [{ backgroundColor: colors.red, data: [1] }],
  },
}
"#,
    )
    .unwrap();

    let out = bin()
        .args(["render", spec.to_str().unwrap(), "-o", "-"])
        .assert()
        .success();
    let s = String::from_utf8(out.get_output().stdout.clone()).unwrap();
    assert!(s.starts_with("<svg"), "expected SVG, got: {s}");
}

#[test]
fn libsonnet_direct_input_exits_1() {
    // .libsonnet は直接入力に使えない（DSL 検出失敗として扱う）
    let dir = tempfile_dir_for("libsonnet_direct_input_exits_1");
    let lib = dir.join("lib.libsonnet");
    std::fs::write(&lib, r#"{ x: 1 }"#).unwrap();
    let out = bin()
        .args(["render", lib.to_str().unwrap(), "-o", "-"])
        .assert()
        .failure()
        .code(1);
    let stderr = String::from_utf8(out.get_output().stderr.clone()).unwrap();
    // .libsonnet は JSON として解析されるため、JSON パースエラー or DSL 検出エラーのいずれかが出る。
    assert!(
        stderr.contains("auto-detect") || stderr.contains("DSL") || stderr.contains("JSON"),
        "stderr should mention rejection reason (DSL detection or JSON parse error), got: {stderr}"
    );
}

#[test]
fn jsonnet_inspect_emits_model() {
    let dir = tempfile_dir_for("jsonnet_inspect_emits_model");
    let spec = dir.join("spec.jsonnet");
    std::fs::write(&spec, MINIMAL_JSONNET_FILE).unwrap();
    let out = bin()
        .args(["inspect", spec.to_str().unwrap(), "-o", "-"])
        .assert()
        .success();
    let bytes = out.get_output().stdout.clone();
    let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid JSON");
    assert_eq!(v["meta"]["type"], "bar");
}

#[test]
fn jsonnet_stdin_inspect_emits_model() {
    let out = bin()
        .args(["inspect", "-", "-o", "-", "--jsonnet"])
        .write_stdin(MINIMAL_JSONNET_STDIN)
        .assert()
        .success();
    let bytes = out.get_output().stdout.clone();
    let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid JSON");
    assert_eq!(v["meta"]["type"], "bar");
}

#[test]
fn inspect_jsonnet_flag_with_file_path_exits_1() {
    let dir = tempfile_dir_for("inspect_jsonnet_flag_with_file_path_exits_1");
    let spec = dir.join("spec.json");
    std::fs::write(&spec, MINIMAL_BAR_A).unwrap();
    bin()
        .args(["inspect", spec.to_str().unwrap(), "-o", "-", "--jsonnet"])
        .assert()
        .failure()
        .code(1);
}

#[test]
fn jsonnet_syntax_error_exits_1() {
    bin()
        .args(["render", "-", "-o", "-", "--jsonnet"])
        .write_stdin("{ invalid jsonnet ::::")
        .assert()
        .failure()
        .code(1);
}

#[test]
fn renders_to_webp_stdout() {
    let out = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["render", "-", "-o", "-", "--format", "webp"])
        .write_stdin(r#"{"type":"bar","data":{"labels":["A","B"],"datasets":[{"data":[1,2]}]}}"#)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "exit: {:?}\n{}",
        out.status,
        String::from_utf8_lossy(&out.stderr)
    );
    assert_eq!(&out.stdout[0..4], b"RIFF");
    assert_eq!(&out.stdout[8..12], b"WEBP");
}

#[test]
fn renders_bar_to_webp_file() {
    let dir = tempfile_dir_for("renders_bar_to_webp_file");
    let out_path = dir.join("out.webp");
    let out = Command::cargo_bin("fulgur-chart")
        .unwrap()
        .args(["render", "-", "-o", out_path.to_str().unwrap()])
        .write_stdin(r#"{"type":"bar","data":{"labels":["A","B"],"datasets":[{"data":[1,2]}]}}"#)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "exit: {:?}\n{}",
        out.status,
        String::from_utf8_lossy(&out.stderr)
    );
    let bytes = std::fs::read(&out_path).unwrap();
    assert_eq!(&bytes[0..4], b"RIFF");
    assert_eq!(&bytes[8..12], b"WEBP");
}

#[test]
fn jsonnet_file_syntax_error_exits_1() {
    let dir = tempfile_dir_for("jsonnet_file_syntax_error_exits_1");
    let spec = dir.join("bad.jsonnet");
    std::fs::write(&spec, "{ not valid jsonnet ::::").unwrap();
    bin()
        .args(["render", spec.to_str().unwrap(), "-o", "-"])
        .assert()
        .failure()
        .code(1);
}

#[test]
fn batch_jsonnet_importstr_exits_1_without_output() {
    let dir = batch_dir("batch_jsonnet_importstr_exits_1_without_output");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();
    std::fs::write(dir.join("secret.txt"), "FULGUR_SECRET_SHOULD_NOT_RENDER").unwrap();

    std::fs::write(
        in_dir.join("leak.jsonnet"),
        r#"{
  type: "bar",
  data: {
    labels: [importstr "../secret.txt"],
    datasets: [{ data: [1] }],
  },
}
"#,
    )
    .unwrap();

    bin()
        .args([
            "render",
            in_dir.join("leak.jsonnet").to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .code(1);

    assert!(
        !out_dir.join("leak.svg").exists(),
        "rejected Jsonnet must not produce output"
    );
}

#[test]
fn batch_jsonnet_import_via_textblock_exits_1() {
    // ||| テキストブロック内に " があってもテキストスキャナを騙せないことを確認するテスト。
    // import resolver を持たないため jrsonnet 自体が評価時にエラーを返す。
    let dir = batch_dir("batch_jsonnet_import_via_textblock_exits_1");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();

    std::fs::write(
        in_dir.join("bypass.jsonnet"),
        r#"local x = |||
  he said "hello
|||;
local y = importstr "/etc/hostname";
{ type: "bar", data: { labels: [y], datasets: [{ data: [1] }] } }
"#,
    )
    .unwrap();

    bin()
        .args([
            "render",
            in_dir.join("bypass.jsonnet").to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .code(1);

    assert!(
        !out_dir.join("bypass.svg").exists(),
        "import bypass must not produce output"
    );
}

// ---- std.parseYaml は無効化されているため評価エラーになる ----

#[test]
fn jsonnet_stdin_parse_yaml_exits_1() {
    // stdin モードで std.parseYaml を呼ぶと "no such field" エラー
    let spec = r#"{ type: "bar", data: { labels: [std.parseYaml("a: 1").a], datasets: [{ data: [1] }] } }"#;
    bin()
        .args(["render", "-", "-o", "-", "--jsonnet"])
        .write_stdin(spec)
        .assert()
        .failure()
        .code(1);
}

#[test]
fn jsonnet_file_parse_yaml_exits_1() {
    let dir = tempfile_dir_for("jsonnet_file_parse_yaml_exits_1");
    let spec_path = dir.join("spec.jsonnet");
    std::fs::write(
        &spec_path,
        r#"{ type: "bar", data: { labels: [std.parseYaml("a: 1").a], datasets: [{ data: [1] }] } }"#,
    )
    .unwrap();
    bin()
        .args(["render", spec_path.to_str().unwrap(), "-o", "-"])
        .assert()
        .failure()
        .code(1);
}

#[test]
fn batch_jsonnet_parse_yaml_exits_1() {
    let dir = batch_dir("batch_jsonnet_parse_yaml_exits_1");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();

    std::fs::write(
        in_dir.join("spec.jsonnet"),
        r#"{ type: "bar", data: { labels: [std.parseYaml("a: 1").a], datasets: [{ data: [1] }] } }"#,
    )
    .unwrap();

    bin()
        .args([
            "render",
            in_dir.join("spec.jsonnet").to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .failure()
        .code(1);

    assert!(
        !out_dir.join("spec.svg").exists(),
        "parseYaml error must not produce output"
    );
}

// ---- 入力サイズ上限（1 MiB 超）はエラー ----

#[test]
fn jsonnet_stdin_size_limit_exits_1() {
    // 1 MiB + 1 byte の入力はエラーになる
    let over_limit = "x".repeat(1024 * 1024 + 1);
    bin()
        .args(["render", "-", "-o", "-", "--jsonnet"])
        .write_stdin(over_limit)
        .assert()
        .failure()
        .code(1);
}

#[test]
fn jsonnet_file_size_limit_exits_1() {
    let dir = tempfile_dir_for("jsonnet_file_size_limit_exits_1");
    let spec_path = dir.join("big.jsonnet");
    // 1 MiB + 1 byte の .jsonnet ファイルはサイズチェックで弾かれる
    std::fs::write(&spec_path, "x".repeat(1024 * 1024 + 1)).unwrap();
    bin()
        .args(["render", spec_path.to_str().unwrap(), "-o", "-"])
        .assert()
        .failure()
        .code(1);
}

#[test]
fn batch_renders_jsonnet_files() {
    let dir = batch_dir("batch_renders_jsonnet_files");
    let in_dir = dir.join("in");
    let out_dir = dir.join("out");
    std::fs::create_dir_all(&in_dir).unwrap();

    std::fs::write(in_dir.join("a.jsonnet"), MINIMAL_JSONNET_FILE).unwrap();
    std::fs::write(in_dir.join("b.jsonnet"), MINIMAL_JSONNET_FILE).unwrap();

    bin()
        .args([
            "render",
            in_dir.join("a.jsonnet").to_str().unwrap(),
            in_dir.join("b.jsonnet").to_str().unwrap(),
            "--out-dir",
            out_dir.to_str().unwrap(),
        ])
        .assert()
        .success();

    let sa = std::fs::read_to_string(out_dir.join("a.svg")).unwrap();
    assert!(sa.starts_with("<svg"));
}
