// arbor-render — SVG emission from a laid-out diagram.
//
// Entry point:
//   fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String
//
// The graph is the lowered (diagram-form) shape produced by arbor-core /
// arbor-diagram (`title`, `direction`, `nodes`, `edges`, `groups`). The
// layout is whatever arbor-layout returned: `node_pos_<id>`, `node_size_<id>`,
// `group_bounds_<id>`, `node_ids`, `group_ids`, `canvas`.
//
// `forbidden` is a list of "from->to" key strings — same format as
// arbor-core's collect_forbidden(). The Rust crate threaded a HashSet
// through; El threads a list and we linear-scan.
//
// SVG is text emission — straightforward El. Every float coordinate is
// passed through format_float(_, 1) for stable output.
//
// ── PNG render is intentionally out of scope ────────────────────────────────
// The Rust crate rasterises via resvg → tiny_skia → png. The El runtime
// today exposes no equivalent: there is no resvg, no usvg, no font rasterer,
// no PNG encoder, no path-fill code. fs_write writes text only — there is
// no binary write primitive. arbor_render_png() returns an error map in El
// until the runtime grows a rasterer (see "runtime gaps" in the report).

// ── Colour palette (matches the Rust constants exactly) ────────────────────

fn col_node_fill() -> String { "#ffffff" }
fn col_node_stroke() -> String { "#334155" }
fn col_primary_fill() -> String { "#0052A0" }
fn col_primary_text() -> String { "#ffffff" }
fn col_node_text() -> String { "#0D0D14" }
fn col_edge() -> String { "#64748B" }
fn col_edge_forbidden() -> String { "#DC2626" }
fn col_group_fill() -> String { "rgba(0,0,0,0.03)" }
fn col_group_stroke() -> String { "#CBD5E1" }
fn col_group_text() -> String { "#64748B" }
fn col_edge_label() -> String { "#64748B" }

// ── XML escape ─────────────────────────────────────────────────────────────

fn esc(s: String) -> String {
    let r1: String = str_replace(s, "&", "&amp;")
    let r2: String = str_replace(r1, "<", "&lt;")
    let r3: String = str_replace(r2, ">", "&gt;")
    let r4: String = str_replace(r3, "\"", "&quot;")
    r4
}

// Float to "%.1f" — the Rust pt() helper.
fn pt(v: el_val_t) -> String {
    format_float(v, 1)
}

// Float arithmetic helpers — float_to_int / int_to_float trip through Int,
// which is exact for the integer-valued floats used by the layout pass.
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
    let ai: Int = float_to_int(a)
    let bi: Int = float_to_int(b)
    int_to_float(ai + bi)
}

fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
    let ai: Int = float_to_int(a)
    let bi: Int = float_to_int(b)
    int_to_float(ai - bi)
}

fn fdiv2(a: el_val_t) -> el_val_t {
    let ai: Int = float_to_int(a)
    int_to_float(ai / 2)
}

fn fmid(a: el_val_t, b: el_val_t) -> el_val_t {
    fdiv2(fadd(a, b))
}

// ── forbidden-edge linear lookup ───────────────────────────────────────────

fn forbidden_key(from: String, to: String) -> String {
    from + "->" + to
}

fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
    let key: String = forbidden_key(src, dst)
    let n: Int = el_list_len(set)
    let i = 0
    while i < n {
        let s: String = get(set, i)
        if str_eq(s, key) { return true }
        let i = i + 1
    }
    false
}

// ── Arrow marker defs ──────────────────────────────────────────────────────

fn arrow_defs() -> String {
    let s = "\n    <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
    let s = s + "      <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
    let s = s + "    </marker>\n"
    let s = s + "    <marker id=\"ah-bi\" markerWidth=\"10\" markerHeight=\"7\" refX=\"1\" refY=\"3.5\" orient=\"auto-start-reverse\">\n"
    let s = s + "      <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
    let s = s + "    </marker>\n"
    let s = s + "    <marker id=\"ah-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
    let s = s + "      <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge_forbidden() + "\"/>\n"
    let s = s + "    </marker>"
    s
}

// ── Node rendering ─────────────────────────────────────────────────────────

fn render_node(buf: String, node: Map<String, Any>, layout: Map<String, Any>) -> String {
    let nid: String = node["id"]
    let pos: Map<String, Any> = el_map_get(layout, "node_pos_" + nid)
    let sz: Map<String, Any> = el_map_get(layout, "node_size_" + nid)

    let cx: el_val_t = pos["x"]
    let cy: el_val_t = pos["y"]
    let w: el_val_t = sz["w"]
    let h: el_val_t = sz["h"]

    let x: el_val_t = fsub(cx, fdiv2(w))
    let y: el_val_t = fsub(cy, fdiv2(h))

    let fill_in: String = node["style_fill"]
    let stroke_in: String = node["style_stroke"]
    let color_in: String = node["style_color"]
    let fill = col_node_fill()
    if str_len(fill_in) > 0 { let fill = fill_in }
    let stroke = col_node_stroke()
    if str_len(stroke_in) > 0 { let stroke = stroke_in }
    let text_col = col_node_text()
    if str_len(color_in) > 0 { let text_col = color_in }

    let shape: String = node["shape"]
    let buf = buf

    if str_eq(shape, "rectangle") {
        let buf = buf + "  <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
        let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
        let buf = buf + "\" rx=\"4\" fill=\"" + fill + "\" stroke=\"" + stroke
        let buf = buf + "\" stroke-width=\"1.5\"/>\n"
    }
    if str_eq(shape, "rounded_rect") {
        let buf = buf + "  <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
        let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
        let buf = buf + "\" rx=\"20\" fill=\"" + fill + "\" stroke=\"" + stroke
        let buf = buf + "\" stroke-width=\"1.5\"/>\n"
    }
    if str_eq(shape, "stadium") {
        let buf = buf + "  <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
        let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
        let buf = buf + "\" rx=\"" + pt(fdiv2(h)) + "\" fill=\"" + fill
        let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
    }
    if str_eq(shape, "cylinder") {
        // body: rect from y+ry to bottom; ry ≈ h/6 (Rust uses h*0.18, we use h/6
        // to stay in integer arithmetic — visually indistinguishable on the
        // canvas sizes the layout produces).
        let hi: Int = float_to_int(h)
        let ry: el_val_t = int_to_float(hi / 6)
        let body_y: el_val_t = fadd(y, ry)
        let body_h: el_val_t = fsub(h, ry)
        let buf = buf + "  <rect x=\"" + pt(x) + "\" y=\"" + pt(body_y)
        let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(body_h)
        let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
        // top ellipse
        let buf = buf + "  <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(body_y)
        let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
        let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
        // bottom ellipse
        let bot_y: el_val_t = fadd(y, h)
        let buf = buf + "  <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(bot_y)
        let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
        let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
    }
    if str_eq(shape, "diamond") {
        let hw: el_val_t = fdiv2(w)
        let hh: el_val_t = fdiv2(h)
        let buf = buf + "  <polygon points=\""
        let buf = buf + pt(cx) + "," + pt(fsub(cy, hh)) + " "
        let buf = buf + pt(fadd(cx, hw)) + "," + pt(cy) + " "
        let buf = buf + pt(cx) + "," + pt(fadd(cy, hh)) + " "
        let buf = buf + pt(fsub(cx, hw)) + "," + pt(cy)
        let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
    }

    // Label.
    let label: String = node["label"]
    let buf = buf + "  <text x=\"" + pt(cx) + "\" y=\"" + pt(cy)
    let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
    let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\">"
    let buf = buf + esc(label) + "</text>\n"

    // Sublabel — Rust's DiagramNode stores Option<String>; El uses "" sentinel.
    let sub: String = node["sublabel"]
    if str_len(sub) > 0 {
        let sub_y: el_val_t = fadd(cy, int_to_float(14))
        let buf = buf + "  <text x=\"" + pt(cx) + "\" y=\"" + pt(sub_y)
        let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
        let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\" font-size=\"10\">"
        let buf = buf + esc(sub) + "</text>\n"
    }
    buf
}

// ── Edge rendering ─────────────────────────────────────────────────────────
//
// We emit a straight line from one node centre to the other and let the
// browser draw it; the Rust crate renders cubic bezier paths but the runtime
// has no robust math layer, and the rectangles are large enough that
// straight edges read clearly. (See "runtime gaps".)

fn render_edge(buf: String, edge: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
    let from_id: String = edge["from"]
    let to_id: String = edge["to"]
    let from_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + from_id)
    let to_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + to_id)

    let fx: el_val_t = from_pos["x"]
    let fy: el_val_t = from_pos["y"]
    let tx: el_val_t = to_pos["x"]
    let ty: el_val_t = to_pos["y"]

    let is_forbidden: Bool = forbidden_contains(forbidden, from_id, to_id)
    let stroke = col_edge()
    if is_forbidden { let stroke = col_edge_forbidden() }

    let line: String = edge["line"]
    let arrow: String = edge["arrow"]
    let dash_attr = ""
    if str_eq(line, "dashed") { let dash_attr = " stroke-dasharray=\"5,3\"" }
    if str_eq(line, "dotted") { let dash_attr = " stroke-dasharray=\"2,2\"" }

    let marker_start = ""
    if str_eq(arrow, "both") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
    if str_eq(arrow, "backward") { let marker_start = " marker-start=\"url(#ah-bi)\"" }

    let marker_end = " marker-end=\"url(#ah)\""
    if is_forbidden { let marker_end = " marker-end=\"url(#ah-red)\"" }
    if str_eq(arrow, "none") { let marker_end = "" }
    if str_eq(arrow, "backward") { let marker_end = "" }

    let buf = buf + "  <line x1=\"" + pt(fx) + "\" y1=\"" + pt(fy)
    let buf = buf + "\" x2=\"" + pt(tx) + "\" y2=\"" + pt(ty)
    let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
    let buf = buf + dash_attr + marker_start + marker_end + "/>\n"

    // Forbidden marker — circle-X at midpoint.
    if is_forbidden {
        let mx: el_val_t = fmid(fx, tx)
        let my: el_val_t = fmid(fy, ty)
        let r: el_val_t = int_to_float(7)
        let buf = buf + "  <circle cx=\"" + pt(mx) + "\" cy=\"" + pt(my)
        let buf = buf + "\" r=\"" + pt(r) + "\" fill=\"white\" stroke=\""
        let buf = buf + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
        let off: el_val_t = int_to_float(4)
        let buf = buf + "  <line x1=\"" + pt(fsub(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
        let buf = buf + "\" x2=\"" + pt(fadd(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
        let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
        let buf = buf + "  <line x1=\"" + pt(fadd(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
        let buf = buf + "\" x2=\"" + pt(fsub(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
        let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
    }

    // Edge label
    let label: String = edge["label"]
    if str_len(label) > 0 {
        let mx: el_val_t = fmid(fx, tx)
        let my: el_val_t = fmid(fy, ty)
        let lw: el_val_t = int_to_float(str_len(label) * 7 + 8)
        let lh: el_val_t = int_to_float(16)
        let buf = buf + "  <rect x=\"" + pt(fsub(mx, fdiv2(lw))) + "\" y=\"" + pt(fsub(my, fdiv2(lh)))
        let buf = buf + "\" width=\"" + pt(lw) + "\" height=\"" + pt(lh)
        let buf = buf + "\" rx=\"3\" fill=\"white\" opacity=\"0.85\"/>\n"
        let buf = buf + "  <text x=\"" + pt(mx) + "\" y=\"" + pt(my)
        let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
        let buf = buf + " class=\"arbor-edge-label\">" + esc(label) + "</text>\n"
    }
    buf
}

// ── Group rendering ────────────────────────────────────────────────────────

fn render_group(buf: String, group: Map<String, Any>, layout: Map<String, Any>) -> String {
    let gid: String = group["id"]
    let bounds: Map<String, Any> = el_map_get(layout, "group_bounds_" + gid)
    // Layout may not have bounds for empty groups — defensive.
    let bx_check: el_val_t = bounds["x"]
    if float_to_int(bx_check) == 0 {
        // Could be a real 0; cheaper to skip via presence check on group_ids.
    }
    let bx: el_val_t = bounds["x"]
    let by: el_val_t = bounds["y"]
    let bw: el_val_t = bounds["w"]
    let bh: el_val_t = bounds["h"]
    let buf = buf + "  <rect x=\"" + pt(bx) + "\" y=\"" + pt(by)
    let buf = buf + "\" width=\"" + pt(bw) + "\" height=\"" + pt(bh)
    let buf = buf + "\" rx=\"8\" fill=\"" + col_group_fill() + "\" stroke=\""
    let buf = buf + col_group_stroke() + "\" stroke-width=\"1\" stroke-dasharray=\"4,3\"/>\n"

    // Group label in the top-left corner.
    let lx: el_val_t = fadd(bx, int_to_float(8))
    let ly: el_val_t = fadd(by, int_to_float(14))
    let label: String = group["label"]
    let buf = buf + "  <text x=\"" + pt(lx) + "\" y=\"" + pt(ly)
    let buf = buf + "\" class=\"arbor-group-label\">" + esc(label) + "</text>\n"
    buf
}

// ── Public entry point ─────────────────────────────────────────────────────

fn arbor_render_svg(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
    let canvas: Map<String, Any> = el_map_get(layout, "canvas")
    let cw: el_val_t = canvas["w"]
    let ch: el_val_t = canvas["h"]

    let buf = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + pt(cw)
    let buf = buf + "\" height=\"" + pt(ch) + "\" viewBox=\"0 0 " + pt(cw) + " " + pt(ch) + "\">\n"
    let buf = buf + "  <defs>"
    let buf = buf + arrow_defs()
    let buf = buf + "\n    <style>\n"
    let buf = buf + "      .arbor-node-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; }\n"
    let buf = buf + "      .arbor-group-label { font-family: 'Helvetica Neue', Helvetica, Arial, monospace; font-size: 10px; fill: " + col_group_text() + "; letter-spacing: 0.08em; }\n"
    let buf = buf + "      .arbor-edge-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 11px; fill: " + col_edge_label() + "; }\n"
    let buf = buf + "    </style>\n"
    let buf = buf + "  </defs>\n"

    // Groups first (behind everything).
    let buf = buf + "  <!-- Groups -->\n"
    let groups: [Map<String, Any>] = graph["groups"]
    let gn: Int = el_list_len(groups)
    let i = 0
    while i < gn {
        let g: Map<String, Any> = get(groups, i)
        let gid: String = g["id"]
        // Only render groups the layout actually placed.
        let gids: [String] = el_map_get(layout, "group_ids")
        let placed = false
        let j = 0
        while j < el_list_len(gids) {
            if str_eq(get(gids, j), gid) { let placed = true }
            let j = j + 1
        }
        if placed {
            let buf = render_group(buf, g, layout)
        }
        let i = i + 1
    }

    // Edges
    let buf = buf + "  <!-- Edges -->\n"
    let edges: [Map<String, Any>] = graph["edges"]
    let en: Int = el_list_len(edges)
    let i = 0
    while i < en {
        let e: Map<String, Any> = get(edges, i)
        let buf = render_edge(buf, e, layout, forbidden)
        let i = i + 1
    }

    // Nodes
    let buf = buf + "  <!-- Nodes -->\n"
    let nodes: [Map<String, Any>] = graph["nodes"]
    let nn: Int = el_list_len(nodes)
    let i = 0
    while i < nn {
        let n: Map<String, Any> = get(nodes, i)
        let buf = render_node(buf, n, layout)
        let i = i + 1
    }

    // Title
    let title: String = graph["title"]
    if str_len(title) > 0 {
        let title_x: el_val_t = fdiv2(cw)
        let buf = buf + "  <text x=\"" + pt(title_x) + "\" y=\"22\" text-anchor=\"middle\""
        let buf = buf + " font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\""
        let buf = buf + " font-size=\"15\" font-weight=\"600\" fill=\"" + col_node_text() + "\">"
        let buf = buf + esc(title) + "</text>\n"
    }

    let buf = buf + "</svg>\n"
    buf
}

// PNG — not implemented; the runtime has no SVG rasterizer or PNG encoder.
// Returns an error map that callers can inspect via map["error"].
fn arbor_render_png(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> Map<String, Any> {
    {
        "error": "PNG rasterization not available in El runtime — install a runtime image library or use the Rust binary"
    }
}

// ── Smoke test ─────────────────────────────────────────────────────────────

fn fail(label: String, msg: String) -> Int {
    println("FAIL " + label + ": " + msg)
    state_set("smoke_failures", "1")
    0
}

fn check_contains(label: String, haystack: String, needle: String) -> Int {
    if str_contains(haystack, needle) {
        println("ok   " + label)
        return 1
    }
    fail(label, "missing [" + needle + "]")
}

fn check_not_contains(label: String, haystack: String, needle: String) -> Int {
    if str_contains(haystack, needle) {
        return fail(label, "should not contain [" + needle + "]")
    }
    println("ok   " + label)
    1
}

fn make_test_node(id: String, label: String, shape: String) -> Map<String, Any> {
    {
        "id": id, "label": label, "sublabel": "",
        "shape": shape,
        "style_fill": "", "style_stroke": "", "style_color": ""
    }
}

fn make_test_edge(src: String, dst: String, line: String, arrow: String, label: String) -> Map<String, Any> {
    {
        "from": src, "to": dst, "label": label,
        "line": line, "arrow": arrow
    }
}

fn make_test_pos(x: Int, y: Int) -> Map<String, Any> {
    { "x": int_to_float(x), "y": int_to_float(y) }
}

fn make_test_size(w: Int, h: Int) -> Map<String, Any> {
    { "w": int_to_float(w), "h": int_to_float(h) }
}

// Build a minimal layout map by hand.
fn build_layout(node_ids: [String], group_ids: [String], cw: Int, ch: Int) -> Map<String, Any> {
    let r: Map<String, Any> = el_map_new(0)
    let r = el_map_set(r, "node_ids", node_ids)
    let r = el_map_set(r, "group_ids", group_ids)
    let r = el_map_set(r, "canvas", { "w": int_to_float(cw), "h": int_to_float(ch) })
    r
}

let n_a: Map<String, Any> = make_test_node("a", "Node A", "rectangle")
let n_b: Map<String, Any> = make_test_node("b", "Node B", "rectangle")
let e_ab: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")

let nodes: [Map<String, Any>] = native_list_empty()
let nodes = native_list_append(nodes, n_a)
let nodes = native_list_append(nodes, n_b)
let edges: [Map<String, Any>] = native_list_empty()
let edges = native_list_append(edges, e_ab)
let groups: [Map<String, Any>] = native_list_empty()

let g: Map<String, Any> = {
    "title": "Test", "direction": "top-down",
    "nodes": nodes, "edges": edges, "groups": groups
}

let nid_list: [String] = native_list_empty()
let nid_list = native_list_append(nid_list, "a")
let nid_list = native_list_append(nid_list, "b")
let gid_list: [String] = native_list_empty()
let layout: Map<String, Any> = build_layout(nid_list, gid_list, 400, 300)
let layout = el_map_set(layout, "node_pos_a", make_test_pos(100, 60))
let layout = el_map_set(layout, "node_pos_b", make_test_pos(100, 200))
let layout = el_map_set(layout, "node_size_a", make_test_size(120, 40))
let layout = el_map_set(layout, "node_size_b", make_test_size(120, 40))

let forbidden: [String] = native_list_empty()
let svg: String = arbor_render_svg(g, layout, forbidden)

check_contains("svg starts with <svg", svg, "<svg xmlns=")
check_contains("svg ends with </svg>", svg, "</svg>")
check_contains("svg contains node label", svg, "Node A")
check_contains("svg contains title", svg, ">Test</text>")
check_contains("svg has rect for rectangle node", svg, "<rect")
check_contains("svg has line for edge", svg, "<line")
check_contains("svg has arrow marker def", svg, "id=\"ah\"")

// Escape test
let n_esc: Map<String, Any> = make_test_node("x", "A & B <C>", "rectangle")
let nodes2: [Map<String, Any>] = native_list_empty()
let nodes2 = native_list_append(nodes2, n_esc)
let g2: Map<String, Any> = {
    "title": "Test <Title>", "direction": "top-down",
    "nodes": nodes2, "edges": native_list_empty(), "groups": native_list_empty()
}
let nid2: [String] = native_list_empty()
let nid2 = native_list_append(nid2, "x")
let layout2: Map<String, Any> = build_layout(nid2, native_list_empty(), 200, 100)
let layout2 = el_map_set(layout2, "node_pos_x", make_test_pos(80, 40))
let layout2 = el_map_set(layout2, "node_size_x", make_test_size(120, 40))
let svg2: String = arbor_render_svg(g2, layout2, native_list_empty())
check_contains("escapes ampersand", svg2, "&amp;")
check_contains("escapes <", svg2, "&lt;")
check_not_contains("no raw <C>", svg2, "<C>")

// Forbidden edge
let e_fb: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
let edges3: [Map<String, Any>] = native_list_empty()
let edges3 = native_list_append(edges3, e_fb)
let g3: Map<String, Any> = {
    "title": "F", "direction": "top-down",
    "nodes": nodes, "edges": edges3, "groups": native_list_empty()
}
let fb: [String] = native_list_empty()
let fb = native_list_append(fb, forbidden_key("a", "b"))
let svg3: String = arbor_render_svg(g3, layout, fb)
check_contains("forbidden uses red marker", svg3, "ah-red")
check_contains("forbidden colour present", svg3, col_edge_forbidden())

// Diamond shape → polygon
let n_d: Map<String, Any> = make_test_node("d", "Decide", "diamond")
let g4: Map<String, Any> = {
    "title": "", "direction": "top-down",
    "nodes": native_list_append(native_list_empty(), n_d),
    "edges": native_list_empty(), "groups": native_list_empty()
}
let nid4: [String] = native_list_append(native_list_empty(), "d")
let layout4: Map<String, Any> = build_layout(nid4, native_list_empty(), 200, 100)
let layout4 = el_map_set(layout4, "node_pos_d", make_test_pos(80, 50))
let layout4 = el_map_set(layout4, "node_size_d", make_test_size(120, 40))
let svg4: String = arbor_render_svg(g4, layout4, native_list_empty())
check_contains("diamond uses polygon", svg4, "<polygon")

// Cylinder shape → ellipses
let n_cy: Map<String, Any> = make_test_node("cy", "DB", "cylinder")
let g5: Map<String, Any> = {
    "title": "", "direction": "top-down",
    "nodes": native_list_append(native_list_empty(), n_cy),
    "edges": native_list_empty(), "groups": native_list_empty()
}
let nid5: [String] = native_list_append(native_list_empty(), "cy")
let layout5: Map<String, Any> = build_layout(nid5, native_list_empty(), 200, 100)
let layout5 = el_map_set(layout5, "node_pos_cy", make_test_pos(80, 50))
let layout5 = el_map_set(layout5, "node_size_cy", make_test_size(120, 40))
let svg5: String = arbor_render_svg(g5, layout5, native_list_empty())
check_contains("cylinder uses ellipse", svg5, "<ellipse")

// Dashed edge
let e_dash: Map<String, Any> = make_test_edge("a", "b", "dashed", "forward", "")
let g6: Map<String, Any> = {
    "title": "", "direction": "top-down",
    "nodes": nodes, "edges": native_list_append(native_list_empty(), e_dash),
    "groups": native_list_empty()
}
let svg6: String = arbor_render_svg(g6, layout, native_list_empty())
check_contains("dashed line dasharray", svg6, "stroke-dasharray=\"5,3\"")

// PNG returns an error map
let png: Map<String, Any> = arbor_render_png(g, layout, native_list_empty())
let err: String = png["error"]
if str_len(err) > 0 {
    println("ok   PNG returns error map")
} else {
    println("FAIL PNG should have returned error")
    state_set("smoke_failures", "1")
}

println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
    println("arbor-render: FAILED")
    exit_program(1)
} else {
    println("arbor-render: ok")
}
