//! B0: xfig's export languages through `file.export`, `file.export_data`
//! and `file.export_formats`.

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use rustfig_engine::fig2dev::{language_available, languages::LANGUAGES};
use rustfig_engine::Engine;
use serde_json::json;

fn drawing() -> Engine {
    let mut e = Engine::new();
    e.execute(
        "shape.rectangle",
        &json!({"x": 0, "y": 0, "width": 2400, "height": 1200}),
    )
    .unwrap();
    e
}

#[test]
fn formats_list_every_language_in_xfigs_order() {
    let mut e = Engine::new();
    let v = e.execute("file.export_formats", &json!({})).unwrap();
    let formats = v["formats"].as_array().unwrap();
    let ids: Vec<&str> = formats.iter().map(|f| f["id"].as_str().unwrap()).collect();
    let expected: Vec<&str> = LANGUAGES.iter().map(|l| l.id).collect();
    assert_eq!(ids, expected);
    assert_eq!(ids.first(), Some(&"ps"));
    for f in formats {
        let available = f["available"].as_bool().unwrap();
        assert_eq!(available, f["status"] != "planned", "{f}");
        if f["status"] != "available" {
            assert!(f["task"].as_str().is_some(), "{f}");
        }
    }
}

#[test]
fn every_available_language_exports() {
    let mut e = drawing();
    for l in LANGUAGES.iter().filter(|l| language_available(l)) {
        let r = e.execute("file.export_data", &json!({"format": l.id}));
        if l.id == "shape" {
            // fig2dev's shape driver needs the figure's shape comments.
            assert!(r.unwrap_err().message.contains("No shape found"));
            continue;
        }
        let r = r.unwrap();
        if l.id == "pspdf" {
            let files = r["files"].as_array().unwrap();
            assert_eq!(files.len(), 2);
            assert_eq!(files[0]["extension"], "eps");
            assert!(files[1]["base64"].as_str().is_some());
        } else if let Some(files) = r["files"].as_array() {
            // The combined LaTeX languages: graphics and LaTeX part.
            assert!(files.len() >= 2, "{}", l.id);
        } else {
            assert!(r.get("data").or(r.get("base64")).is_some(), "{}", l.id);
        }
    }
}

#[test]
fn latex_box_has_the_figures_size() {
    let mut e = drawing();
    let r = e.execute("file.export_data", &json!({"format": "box"})).unwrap();
    let text = r["data"].as_str().unwrap();
    assert!(text.starts_with("\\makebox["), "{text}");
    assert!(text.contains("in]{\\rule{0in}{"), "{text}");
}

#[test]
fn planned_and_unknown_languages_are_errors() {
    let mut e = drawing();
    // sld (AutoCAD slide) is a real language since fig2dev 0.2.0.
    let r = e.execute("file.export_data", &json!({"format": "sld"})).unwrap();
    assert!(r["base64"].as_str().is_some_and(|b| !b.is_empty()), "{r}");
    for bad in [
        "",
        "bogus",
        "../../etc/passwd",
        "svg; rm -rf /",
        "ŝvg",
        &"x".repeat(10_000),
    ] {
        assert!(
            e.execute("file.export_data", &json!({"format": bad})).is_err(),
            "{bad:?}"
        );
    }
    assert!(e.execute("file.export_data", &json!({"format": 3})).is_err());
}

#[test]
fn files_pick_the_language_from_their_name() {
    let dir = std::env::temp_dir().join(format!("rustfig-export-languages-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let mut e = drawing();
    let path = |name: &str| dir.join(name).to_string_lossy().into_owned();

    let r = e.execute("file.export", &json!({"path": path("fig.box")})).unwrap();
    assert_eq!(r["format"], "box");
    let r = e
        .execute("file.export", &json!({"path": path("both.eps"), "format": "pspdf"}))
        .unwrap();
    assert_eq!(r["paths"].as_array().unwrap().len(), 2);
    assert!(dir.join("both.eps").is_file() && dir.join("both.pdf").is_file());

    let err = e.execute("file.export", &json!({"path": path("fig.tex")})).unwrap_err();
    assert!(err.message.contains("format"), "{}", err.message);
    let r = e.execute("file.export", &json!({"path": path("fig.sld")})).unwrap();
    assert_eq!(r["format"], "sld");
    assert!(dir.join("fig.sld").is_file());
    std::fs::remove_dir_all(&dir).unwrap();
}

fn eps(e: &mut Engine, options: serde_json::Value) -> String {
    let r = e
        .execute("file.export_data", &json!({"format": "eps", "options": options}))
        .unwrap();
    r["data"].as_str().unwrap().to_string()
}

fn bbox(eps: &str) -> (i64, i64) {
    let line = eps.lines().find(|l| l.starts_with("%%BoundingBox:")).unwrap();
    let v: Vec<i64> = line.split_whitespace().skip(1).map(|n| n.parse().unwrap()).collect();
    (v[2], v[3])
}

#[test]
fn fig2dev_options_change_the_output() {
    let mut e = drawing();
    let plain = eps(&mut e, json!({}));
    let (w, h) = bbox(&plain);
    // -b: a border of 10 points on every side.
    let (bw, bh) = bbox(&eps(&mut e, json!({"border": 10})));
    assert_eq!((bw - w, bh - h), (20, 20));
    // -m 2 doubles the figure; -Z 4w makes it 4 inches wide (padding included).
    let (mw, _) = bbox(&eps(&mut e, json!({"m": 2})));
    assert!((mw - 2 * w).abs() <= 2, "{mw} {w}");
    let (zw, _) = bbox(&eps(&mut e, json!({"max_size": "4w"})));
    assert!((zw - 288).abs() <= 1, "{zw}");
    // -n, -g, -N.
    assert!(eps(&mut e, json!({"title": "Figure 1"})).contains("%%Title: Figure 1"));
    let bg = eps(&mut e, json!({"background": "red"}));
    let gray = eps(&mut e, json!({"background": "red", "N": true}));
    assert_ne!(bg, gray);
    assert_ne!(bg, plain);
    // Options may also be given by letter, with a dash.
    assert_eq!(eps(&mut e, json!({"-b": 10})), eps(&mut e, json!({"border": 10})));
}

#[test]
fn depth_options_select_objects_and_bounds() {
    let mut e = drawing();
    let r = e
        .execute(
            "shape.rectangle",
            &json!({"x": 6000, "y": 0, "width": 1200, "height": 1200}),
        )
        .unwrap();
    let id = r["id"].as_u64().unwrap();
    e.execute("arrange.set_depth", &json!({"ids": [id], "depth": 10}))
        .unwrap();
    let all = eps(&mut e, json!({}));
    let only_50 = eps(&mut e, json!({"depths": "+50"}));
    let only_50_adjusted = eps(&mut e, json!({"depths": "+50", "K": true}));
    // Without -K the page keeps the whole figure; with it, only depth 50 counts.
    assert_ne!(only_50, all);
    assert_eq!(bbox(&all), bbox(&only_50));
    assert!(bbox(&only_50_adjusted).0 < bbox(&all).0 / 2);
    assert_eq!(
        bbox(&eps(&mut e, json!({"depths": "-10", "K": true}))),
        bbox(&only_50_adjusted)
    );
}

#[test]
fn bad_options_are_refused() {
    let mut e = drawing();
    let err = |e: &mut Engine, format: &str, options: serde_json::Value| {
        e.execute("file.export_data", &json!({"format": format, "options": options}))
            .unwrap_err()
            .message
    };
    assert!(err(&mut e, "eps", json!({"nonsense": 1})).contains("no option"));
    // Options not ported yet: fig2dev's driver warns and goes on.
    let r = e
        .execute(
            "file.export_data",
            &json!({"format": "eps", "options": {"composite_fonts": true}}),
        )
        .unwrap();
    assert!(r["warnings"][0].as_str().unwrap().contains("B8"), "{r}");
    assert!(err(&mut e, "svg", json!({"title": "x"})).contains("no option"));
    // fig2dev takes -m and -Z together; -Z sizes the figure.
    assert!(e
        .execute("file.export_data", &json!({"format": "eps", "options": [1]}))
        .is_err());
}

#[test]
fn formats_list_each_languages_options() {
    let mut e = Engine::new();
    let v = e.execute("file.export_formats", &json!({})).unwrap();
    let eps = v["formats"]
        .as_array()
        .unwrap()
        .iter()
        .find(|f| f["id"] == "eps")
        .unwrap();
    let keys: Vec<&str> = eps["options"]
        .as_array()
        .unwrap()
        .iter()
        .map(|o| o["key"].as_str().unwrap())
        .collect();
    assert!(keys.contains(&"border") && keys.contains(&"depths") && keys.contains(&"bbox"));
    let opt = |key: &str| {
        eps["options"]
            .as_array()
            .unwrap()
            .iter()
            .find(|o| o["key"] == key)
            .unwrap()
    };
    // Ported with B4b; composite fonts wait for B8.
    assert_eq!(opt("bbox")["available"], true);
    assert_eq!(opt("composite_fonts")["available"], false);
    assert_eq!(opt("composite_fonts")["task"], "B8");
}
