use acadrust::entities::Tolerance;

use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::SnapHint;
use crate::scene::text::lff;
use crate::scene::view::transform;

// ── GDT text parser ───────────────────────────────────────────────────────────

/// The font a compartment's symbols come from.
///
/// The source names it in its own escapes, and it is registered under that name
/// like any other font — so nothing here special-cases it. A letter in this font
/// IS a symbol; it holds no Latin text.
const SYMBOL_FONT: &str = "gdt";
/// The font a compartment's ordinary text comes from.
const TEXT_FONT: &str = "txt";

/// One stretch of a compartment drawn in a single font.
#[derive(Debug, Clone, PartialEq)]
struct Run {
    text: String,
    font: &'static str,
}

/// A compartment: the runs it is built from, in order.
type Cell = Vec<Run>;

/// The characters of `cell`, whichever font each came from. For messages and
/// tests — never for measuring, since the runs are in different fonts.
#[cfg(test)]
fn cell_text(cell: &Cell) -> String {
    cell.iter().map(|r| r.text.as_str()).collect()
}

/// Parse a tolerance text string into rows of compartments.
///
/// Example: `{\Fgdt;p}%%v0.5%%vA%%vB%%v%%v` + newline + `{\Fgdt;j}%%v0.1%%vA`
///   - row separator → see below; `%%v` → compartment separator within a row
///   - `{\Fgdt;X}` → character X, drawn from the font that escape names
///
/// The row break reaches us in four encodings and all four are live:
/// a DWG carries a raw newline; the text-DXF reader rewrites the on-disk `^J`
/// to a newline before we ever see it; the binary-DXF reader does not, so `^J`
/// can still arrive literally; and our own DXF writer re-emits embedded
/// newlines as `\P`, because a line-based format cannot carry a raw newline
/// inside a string value. Splitting on `^J` alone — as this did — therefore
/// never fired on the two mainstream paths, and every row collapsed into one.
fn parse_gdt_rows(raw: &str) -> Vec<Vec<Cell>> {
    let norm = raw
        .replace("^J", "\n")
        .replace("\\P", "\n")
        .replace("\r\n", "\n")
        .replace('\r', "\n");

    norm.split('\n')
        .filter(|row| !row.trim().is_empty())
        .map(|row| {
            let mut cells: Vec<Cell> = row.split("%%v").map(|c| parse_cell(c.trim())).collect();
            // Trailing empties are unused compartments, not blank boxes: a row
            // written with spare datum slots ends in `%%v%%v` yet draws only the
            // compartments it filled. Interior empties stay — there the position
            // itself carries meaning (datum A, _, C).
            while cells.last().is_some_and(|c| c.is_empty()) {
                cells.pop();
            }
            cells
        })
        .filter(|cells: &Vec<Cell>| !cells.is_empty())
        .collect()
}

/// Split a compartment into font runs, resolving `{\Fgdt;X}` switches and
/// dropping every other inline format code.
///
/// The escape names its font and carries the character to draw in it; both are
/// kept as-is, so the symbol is looked up by the ordinary font machinery rather
/// than translated into some other character first.
fn parse_cell(s: &str) -> Cell {
    let mut runs: Cell = Vec::new();
    let push = |ch: char, font: &'static str, runs: &mut Cell| match runs.last_mut() {
        Some(r) if r.font == font => r.text.push(ch),
        _ => runs.push(Run {
            text: ch.to_string(),
            font,
        }),
    };

    let mut chars = s.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch != '{' {
            push(ch, TEXT_FONT, &mut runs);
            continue;
        }
        // Collect to the closing brace.
        let mut inner = String::new();
        let mut depth = 1usize;
        for c in chars.by_ref() {
            match c {
                '{' => {
                    depth += 1;
                    inner.push(c);
                }
                '}' => {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                    inner.push(c);
                }
                _ => inner.push(c),
            }
        }
        if let Some(sym) = symbol_font_switch(&inner) {
            push(sym, SYMBOL_FONT, &mut runs);
        }
        // Any other format code contributes nothing.
    }
    runs
}

/// The character selected by a `\F<symbol font>;X` switch, or `None` when the
/// braces hold some other format code.
///
/// The font name may carry an extension and a run of `|`-separated parameters
/// (`\Fgdt.shx|b0|i0|c134|p6;j`), so match the name and skip to the `;` rather
/// than demanding the bare `\Fgdt;` form — the parameterised spelling is legal
/// and would otherwise drop the symbol silently.
fn symbol_font_switch(inner: &str) -> Option<char> {
    let rest = inner
        .strip_prefix("\\F")
        .or_else(|| inner.strip_prefix("\\f"))?;
    let (name, tail) = rest.split_once(';')?;
    let name = name.split('|').next()?.trim();
    let stem = name.strip_suffix(".shx").unwrap_or(name);
    if !stem.eq_ignore_ascii_case("gdt") {
        return None;
    }
    tail.chars().next()
}

/// Per-entity overrides of individual dimension-style variables, carried as
/// extended data under the `DSTYLE` application.
///
/// An entity may keep its style yet override single variables on itself, as
/// `(variable group code, value)` pairs. Reading the style but ignoring these
/// draws the frame at the style's size rather than its own — which is how this
/// one, overriding its text height to a fraction of the style's, came out far
/// too large.
///
/// Only the character height is read — it is the frame's single geometric
/// input (see `tessellate_tolerance`); every other variable is left to the style.
fn dstyle_overrides(tol: &Tolerance) -> Option<f64> {
    // The only variable the frame is built from.
    const DIMTXT: i16 = 140;

    use acadrust::xdata::XDataValue as V;

    // The record belongs to the shared "ACAD" application and names itself in
    // its FIRST STRING VALUE. There is no record called "DSTYLE" — asking for
    // one finds nothing and leaves every override silently unread.
    let rec = tol.common.extended_data.get_record("ACAD")?;
    let mut vals = rec.values.iter();
    match vals.next() {
        Some(V::String(s)) if s == "DSTYLE" => {}
        _ => return None,
    }

    let mut txt = None;
    let mut pending: Option<i16> = None;
    for v in vals {
        match v {
            V::Integer16(code) => pending = Some(*code),
            V::Real(value) | V::Distance(value) => {
                if pending.take() == Some(DIMTXT) {
                    txt = Some(*value);
                }
            }
            // Braces and anything else just delimit; a non-numeric value also
            // ends the pair we were waiting on.
            _ => pending = None,
        }
    }
    txt
}

/// The tolerance's dimension style — by handle first, then by name.
///
/// The order matters and is not interchangeable: a DWG records the style's
/// handle and leaves the name at its default, while a DXF records the name and
/// leaves the handle empty. Matching on the name alone — the shape used
/// elsewhere for entities that only ever carry one — would silently resolve
/// every DWG-read tolerance to "Standard" and pick the wrong metrics.
fn resolve_dim_style<'a>(
    tol: &Tolerance,
    doc: &'a acadrust::CadDocument,
) -> Option<&'a acadrust::tables::DimStyle> {
    if let Some(h) = tol.dimension_style_handle {
        if !h.is_null() {
            if let Some(s) = doc.dim_styles.iter().find(|s| s.handle == h) {
                return Some(s);
            }
        }
    }
    let name = tol.dimension_style_name.trim();
    if name.is_empty() {
        return None;
    }
    doc.dim_styles
        .iter()
        .find(|s| s.name.eq_ignore_ascii_case(name))
}

// ── Feature-control frame builder ─────────────────────────────────────────────

/// The pen advance of `text` in `font` at `height`.
///
/// This is where the pen LANDS, so it carries the font's letter spacing past
/// the final glyph — which is what puts the gap between one run and the next.
fn run_advance(text: &str, font: &str, height: f32) -> f32 {
    crate::entities::text_support::text_local_bounds(font, text, height, 1.0, 0.0)
        .map(|b| b.advance)
        .unwrap_or(0.0)
}

/// The font's letter spacing at `height`, in world units.
///
/// Glyph geometry is authored against a 9-unit cap height, so a font's spacing
/// scales with the character height like everything else.
fn letter_spacing(font: &str, height: f32) -> f32 {
    crate::scene::text::font_face::Face::resolve(font).letter_spacing() * height / 9.0
}

/// How wide a compartment's content actually draws.
///
/// Not the pen advance: that stops one letter-space PAST the last glyph, since
/// its job is to place whatever comes next. Summing run advances therefore
/// over-measures by exactly one spacing — the gaps between runs are real, the
/// one hanging off the end is not — and every compartment came out that much
/// too wide.
fn content_width(cell: &Cell, height: f32) -> f32 {
    let Some(last) = cell.last() else {
        return 0.0;
    };
    let pen: f32 = cell
        .iter()
        .map(|r| run_advance(&r.text, r.font, height))
        .sum();
    (pen - letter_spacing(last.font, height)).max(0.0)
}

/// One text run of a feature-control frame, ready to become a `TextStroke`
/// with a `GlyphRun` so the run can render as SDF glyph quads (or, when
/// SDF is off, from `strokes`). `origin` is relative to the tolerance insertion
/// point (already rotated); `strokes` are the glyph polylines rotated about the
/// origin (no origin translation — the wire-builder adds the origin).
struct TolCell {
    text: String,
    font: &'static str,
    origin: [f32; 2],
    strokes: Vec<Vec<[f32; 2]>>,
    height: f32,
    rotation: f32,
}

/// Tessellate a Tolerance entity's feature-control frame.
///
/// Returns (`box_strokes`, `cells`):
///   - `box_strokes` — the outer border, row separators and column dividers as
///     2-D polylines (rotated; run-less, so they always render as strokes)
///   - `cells` — one [`TolCell`] per non-empty cell, carrying its text + a
///     `GlyphRun` so the cell renders as SDF text (frame stays geometry).
fn tessellate_tolerance(
    tol: &Tolerance,
    doc: &acadrust::CadDocument,
) -> (Vec<Vec<[f32; 2]>>, Vec<TolCell>) {
    if tol.text.is_empty() {
        return (vec![], vec![]);
    }

    let rows = parse_gdt_rows(&tol.text);
    if rows.is_empty() {
        return (vec![], vec![]);
    }

    // ── Metrics ──────────────────────────────────────────────────────────
    // Text height and gap come from the dimension style whenever one resolves.
    // The entity's own fields are not trustworthy: a DWG carries only the
    // style's handle and leaves these at their constructed defaults (0.18 /
    // 0.09), which are small but non-zero — so a "> 0" test happily uses them
    // and the frame draws about ten times too small. Falling back to them only
    // when no style resolves keeps DXF-read entities working.
    let style = resolve_dim_style(tol, doc);
    let scale = style
        .map(|s| if s.dimscale > 1e-6 { s.dimscale } else { 1.0 })
        .unwrap_or(1.0);
    // The entity's own overrides win over its style; the style wins over the
    // entity's constructed defaults.
    let h = dstyle_overrides(tol)
        .or(style.map(|s| s.dimtxt))
        .map(|v| v * scale)
        .unwrap_or(tol.text_height) as f32;
    let h = if h > 1e-6 { h } else { 2.5_f32 };

    // A compartment is a PROPORTION of the character height: it spans -h..+h
    // about the row's centreline, so it is exactly 2h tall and the h/2 margin
    // falls out of that rather than being an input.
    //
    // The gap variable is deliberately NOT read. Its scope is the dimension
    // line's break and the rectangle around a basic dimension — a different
    // entity's geometry, where it genuinely does apply twice per axis. Carrying
    // that shape over to this frame drew it at ~4x the character height. Even
    // fed a self-consistent style it can only ever produce 1.5h, never 2h, so
    // the formula was wrong in kind, not merely mis-fed.
    let cell_h = 2.0 * h;
    let pad = h * 0.5;
    let min_cell_w = 2.0 * h;

    // Cell widths per row, not a shared column grid: a row holding one
    // compartment is only as wide as that compartment, so a lone datum below a
    // four-compartment row draws a small box rather than inheriting the row
    // above it. Width comes from the real pen advance — a character count times
    // an average width mis-measures every symbol, and counting UTF-8 bytes
    // (`len()`) would make each one two or three cells wide.
    // A compartment's runs come from different fonts, so each is measured in its
    // own before they are summed.
    let cell_width = |cell: &Cell| -> f32 { (content_width(cell, h) + 2.0 * pad).max(min_cell_w) };
    let row_widths: Vec<Vec<f32>> = rows
        .iter()
        .map(|row| row.iter().map(|c| cell_width(c)).collect())
        .collect();

    // ── Transform helpers (local space — translation applied in tessellate.rs) ──
    let angle = (tol.direction.y as f32).atan2(tol.direction.x as f32);
    let (sa, ca) = angle.sin_cos();

    // Rotate only; origin is kept as f64 and applied later with full precision.
    let rot = |x: f32, y: f32| -> [f32; 2] { [x * ca - y * sa, x * sa + y * ca] };

    let mut box_out: Vec<Vec<[f32; 2]>> = Vec::new();
    let mut cells: Vec<TolCell> = Vec::new();

    // The insertion point sits on the CENTRELINE OF THE FIRST ROW, and rows
    // stack downward from there: row `i` spans `-h - 2h*i` .. `+h - 2h*i`. So
    // row 0 straddles y = 0 and the frame grows downward only.
    //
    // Anchoring the frame's bottom-left at the insertion point instead — as
    // this did — offsets the whole frame upward by its own height, and by an
    // amount that changes with the number of rows, so a two-row frame and a
    // three-row one would sit at different heights from the same point.
    let row_bottom = |ri: usize| -h - cell_h * ri as f32;

    // ── Per-row border ────────────────────────────────────────────────────
    // One box per row, each left-aligned at x = 0 and only as wide as its own
    // compartments. Adjacent rows share the edge between them, so this draws
    // the row separators too.
    for (ri, widths) in row_widths.iter().enumerate() {
        let rw: f32 = widths.iter().sum();
        let y0 = row_bottom(ri);
        let y1 = y0 + cell_h;
        box_out.push(vec![
            rot(0.0, y0),
            rot(rw, y0),
            rot(rw, y1),
            rot(0.0, y1),
            rot(0.0, y0),
        ]);
    }

    // ── Compartment dividers, within each row ─────────────────────────────
    for (ri, widths) in row_widths.iter().enumerate() {
        let y0 = row_bottom(ri);
        let y1 = y0 + cell_h;
        let mut x_cursor = 0.0_f32;
        for w in widths.iter().take(widths.len().saturating_sub(1)) {
            x_cursor += w;
            box_out.push(vec![rot(x_cursor, y0), rot(x_cursor, y1)]);
        }
    }

    // ── Text content per cell ─────────────────────────────────────────────
    // Each cell becomes a TolCell: its origin is the (rotated) cell position
    // relative to the insertion point; its `strokes` are the glyph polylines
    // rotated about that origin (used only when SDF is off). The GlyphRun the
    // caller attaches lets the cell render as SDF text.
    for (ri, row) in rows.iter().enumerate() {
        // The compartment is 2h tall and the character h, so an h/2 margin
        // centres the text on the row's centreline.
        let row_y = row_bottom(ri) + pad;
        let mut cell_x = 0.0_f32;
        for (ci, cell) in row.iter().enumerate() {
            let cw = row_widths[ri][ci];
            if !cell.is_empty() {
                // Centre the compartment's whole content, then lay its runs out
                // left to right — each in its own font, each advancing the pen
                // by what that font actually measures.
                let mut run_x = cell_x + (cw - content_width(cell, h)) * 0.5;
                for run in cell {
                    let (text, font) = (run.text.clone(), run.font);
                    let (local_strokes, _) =
                        lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, font, &text);
                    // Glyph polylines rotated about the run's origin (no origin
                    // translation — the wire-builder adds `origin`).
                    let strokes: Vec<Vec<[f32; 2]>> = local_strokes
                        .into_iter()
                        .map(|pl| pl.into_iter().map(|[px, py]| rot(px, py)).collect())
                        .filter(|pl: &Vec<[f32; 2]>| !pl.is_empty())
                        .collect();
                    let advance = run_advance(&text, font, h);
                    cells.push(TolCell {
                        text,
                        font,
                        origin: rot(run_x, row_y),
                        strokes,
                        height: h,
                        rotation: angle,
                    });
                    run_x += advance;
                }
            }
            cell_x += cw;
        }
    }

    (box_out, cells)
}

// ── TruckConvertible ──────────────────────────────────────────────────────────

impl TruckConvertible for Tolerance {
    fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
        if self.text.is_empty() {
            return None;
        }

        let snap_pt = glam::DVec3::new(
            self.insertion_point.x,
            self.insertion_point.y,
            self.insertion_point.z,
        );

        // Build the feature-control frame in local space; origin stored as f64.
        let (box_strokes, cells) = tessellate_tolerance(self, document);
        let ins = [self.insertion_point.x, self.insertion_point.y];

        // Frame geometry first (run-less → always strokes; also the anchor
        // group so its origin = the insertion point), then one run-group per
        // text cell so the cell text renders as SDF glyphs (or strokes when
        // SDF is off).
        let mut groups: Vec<TextStroke> = Vec::with_capacity(1 + cells.len());
        if !box_strokes.is_empty() {
            groups.push(TextStroke {
                strokes: box_strokes,
                origin: ins,
                color: None,
                fill_tris: vec![],
                run: None,
            });
        }
        for cell in cells {
            groups.push(TextStroke {
                strokes: cell.strokes,
                origin: [
                    ins[0] + cell.origin[0] as f64,
                    ins[1] + cell.origin[1] as f64,
                ],
                color: None,
                fill_tris: vec![],
                run: Some(GlyphRun {
                    text: cell.text,
                    font: cell.font.to_string(),
                    height: cell.height,
                    rotation: cell.rotation,
                    width_factor: 1.0,
                    oblique: 0.0,
                    // `tracking` scales the font's own letter spacing, so 0
                    // collapses the gap between glyphs and the characters run
                    // together. Every other text-bearing entity passes 1.0, and
                    // so do both the stroke path (`tessellate_text_ex`) and the
                    // width measurement above — leaving this at 0 made the cells
                    // measure wider than the text they drew.
                    tracking: 1.0,
                    bold: false,
                }),
            });
        }
        if groups.is_empty() {
            return None;
        }

        Some(TruckEntity {
            pick_tris: Vec::new(),
            object: TruckObject::Text(groups),
            snap_pts: vec![(snap_pt, SnapHint::Insertion)],
            tangent_geoms: vec![],
            key_vertices: vec![],
            fill_tris: vec![],
        })
    }
}

// ── Grippable ─────────────────────────────────────────────────────────────────

impl Grippable for Tolerance {
    fn grips(&self) -> Vec<GripDef> {
        vec![square_grip(
            0,
            glam::DVec3::new(
                self.insertion_point.x,
                self.insertion_point.y,
                self.insertion_point.z,
            ),
        )]
    }

    fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
        if grip_id == 0 {
            match apply {
                GripApply::Translate(d) => {
                    self.insertion_point.x += d.x as f64;
                    self.insertion_point.y += d.y as f64;
                    self.insertion_point.z += d.z as f64;
                }
                GripApply::Absolute(p) => {
                    self.insertion_point.x = p.x as f64;
                    self.insertion_point.y = p.y as f64;
                    self.insertion_point.z = p.z as f64;
                }
            }
        }
    }
}

// ── PropertyEditable ──────────────────────────────────────────────────────────

impl PropertyEditable for Tolerance {
    fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
        vec![
            PropSection {
                title: "Text".into(),
                props: vec![
                    ro("Text style", "tol_text_style", String::new()),
                    edit("Text height", "tol_text_height", self.text_height),
                ],
            },
            PropSection {
                title: "Geometry".into(),
                props: vec![
                    edit("Position X", "tol_ix", self.insertion_point.x),
                    edit("Position Y", "tol_iy", self.insertion_point.y),
                    edit("Position Z", "tol_iz", self.insertion_point.z),
                ],
            },
            PropSection {
                title: "Misc".into(),
                props: vec![
                    ro(
                        "Dimension style",
                        "tol_dim_style",
                        if self.dimension_style_name.is_empty() {
                            "(default)".to_string()
                        } else {
                            self.dimension_style_name.clone()
                        },
                    ),
                    edit("Direction X", "tol_dir_x", self.direction.x),
                    edit("Direction Y", "tol_dir_y", self.direction.y),
                    edit("Direction Z", "tol_dir_z", self.direction.z),
                ],
            },
        ]
    }

    fn apply_geom_prop(&mut self, field: &str, value: &str) {
        let Ok(v) = value.trim().parse::<f64>() else {
            return;
        };
        match field {
            "tol_ix" => self.insertion_point.x = v,
            "tol_iy" => self.insertion_point.y = v,
            "tol_iz" => self.insertion_point.z = v,
            "tol_text_height" if v > 0.0 => self.text_height = v,
            "tol_dir_x" => self.direction.x = v,
            "tol_dir_y" => self.direction.y = v,
            "tol_dir_z" => self.direction.z = v,
            _ => {}
        }
    }
}

// ── Transformable ─────────────────────────────────────────────────────────────

impl Transformable for Tolerance {
    fn apply_transform(&mut self, t: &EntityTransform) {
        transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
            transform::reflect_xy_point(
                &mut entity.insertion_point.x,
                &mut entity.insertion_point.y,
                p1,
                p2,
            );
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The benchmark's own string, byte for byte, in the form a DWG carries it
    /// (a raw newline). This one assertion is the whole reported bug: it used to
    /// collapse to a single row of ASCII soup.
    const BENCH: &str =
        "{\\Fgdt;r}%%v{\\Fgdt;n}tol{\\Fgdt;m}%%v{\\Fgdt;n}tol{\\Fgdt;s}%%v1{\\Fgdt;m}%%v%%v\n2{\\Fgdt;p}\nA";

    #[test]
    fn the_benchmark_frame_parses_to_four_one_one() {
        let rows = parse_gdt_rows(BENCH);
        // Rendered as "<font>:<text>" per run so a wrong font fails loudly.
        let shown: Vec<Vec<String>> = rows
            .iter()
            .map(|row| {
                row.iter()
                    .map(|cell| {
                        cell.iter()
                            .map(|r| format!("{}:{}", r.font, r.text))
                            .collect::<Vec<_>>()
                            .join("|")
                    })
                    .collect()
            })
            .collect();
        assert_eq!(
            shown,
            vec![
                vec![
                    "gdt:r".to_string(),
                    "gdt:n|txt:tol|gdt:m".to_string(),
                    "gdt:n|txt:tol|gdt:s".to_string(),
                    "txt:1|gdt:m".to_string(),
                ],
                vec!["txt:2|gdt:p".to_string()],
                vec!["txt:A".to_string()],
            ],
            "the reference draws [◎][⌀tolⓂ][⌀tolⓈ][1Ⓜ] / [2Ⓟ] / [A], each symbol \
             from the font its escape names"
        );
    }

    /// A row break reaches us in four encodings depending on the format and who
    /// wrote it. All four must land on the same frame.
    #[test]
    fn every_row_break_encoding_gives_the_same_rows() {
        let want = parse_gdt_rows(BENCH);
        assert_eq!(want.len(), 3, "guard: the baseline itself must be 3 rows");
        for (label, variant) in [
            ("^J", BENCH.replace('\n', "^J")),
            ("\\P", BENCH.replace('\n', "\\P")),
            ("CRLF", BENCH.replace('\n', "\r\n")),
            ("CR", BENCH.replace('\n', "\r")),
        ] {
            assert_eq!(parse_gdt_rows(&variant), want, "{label} parsed differently");
        }
    }

    /// Spare compartments at the end of a row are unused slots, not empty boxes
    /// — the benchmark's first row ends `%%v%%v` yet draws four.
    #[test]
    fn trailing_empty_compartments_are_dropped_interior_ones_kept() {
        let shown = |raw: &str| -> Vec<Vec<String>> {
            parse_gdt_rows(raw)
                .iter()
                .map(|row| row.iter().map(cell_text).collect())
                .collect()
        };
        assert_eq!(shown("a%%vb%%v%%v"), vec![vec!["a", "b"]]);
        assert_eq!(shown("a%%v%%vc"), vec![vec!["a", "", "c"]]);
    }

    /// The font switch legally carries an extension and parameters; the bare
    /// form is only the common spelling.
    #[test]
    fn a_parameterised_font_switch_still_yields_its_symbol() {
        assert_eq!(symbol_font_switch("\\Fgdt;j"), Some('j'));
        assert_eq!(symbol_font_switch("\\Fgdt.shx|b0|i0|c134|p6;j"), Some('j'));
        assert_eq!(symbol_font_switch("\\FGDT;j"), Some('j'));
        // Not the symbol font — leave it to the generic format-code stripper.
        assert_eq!(symbol_font_switch("\\Farial;j"), None);
        assert_eq!(symbol_font_switch("\\C1"), None);
    }

    /// The frame must take its size from the dimension style. A DWG carries
    /// only the style's handle and leaves the entity's own height at a small
    /// non-zero default, so trusting that field drew the frame ~10x too small.
    #[test]
    fn the_frame_is_sized_from_the_dimension_style_not_the_entity_default() {
        use acadrust::tables::DimStyle;
        let mut doc = acadrust::CadDocument::new();
        let handle = acadrust::Handle::from(0x27_u64);
        let mut style = DimStyle::new("ISO-25");
        style.handle = handle;
        style.dimtxt = 2.5;
        style.dimgap = 0.625;
        style.dimscale = 1.0;
        doc.dim_styles.add_or_replace(style);

        let mut tol = Tolerance::new();
        tol.text = "A".into();
        // What a DWG gives us: the handle set, the metrics left at defaults.
        tol.dimension_style_handle = Some(handle);
        assert!(
            tol.text_height < 1.0,
            "guard: the default height must stay small for this test to mean anything"
        );

        let (boxes, _) = tessellate_tolerance(&tol, &doc);
        // 2 x dimtxt. The entity's own default (0.18) would give 0.36.
        let height = frame_height(&boxes);
        assert!(
            (height - 5.0).abs() < 1e-3,
            "frame is {height} tall; 2 x the style's 2.5 character height is 5.0"
        );
    }

    /// An entity that overrides a style variable on itself wins over the style.
    /// The benchmark's tolerance keeps ISO-25 but overrides its text height to
    /// 0.42; reading the style alone drew the frame several times too large.
    #[test]
    fn an_entity_override_beats_its_dimension_style() {
        use acadrust::tables::DimStyle;
        use acadrust::xdata::XDataValue;

        let mut doc = acadrust::CadDocument::new();
        let handle = acadrust::Handle::from(0x27_u64);
        let mut style = DimStyle::new("ISO-25");
        style.handle = handle;
        style.dimtxt = 2.5;
        style.dimgap = 0.625;
        style.dimscale = 1.0;
        doc.dim_styles.add_or_replace(style);

        let mut tol = Tolerance::new();
        tol.text = "A".into();
        tol.dimension_style_handle = Some(handle);

        // Without the override: 2 x the style's 2.5 character height.
        let (boxes, _) = tessellate_tolerance(&tol, &doc);
        assert!(
            (frame_height(&boxes) - 5.0).abs() < 1e-3,
            "guard: style-sized, got {}",
            frame_height(&boxes)
        );

        // The entity's own overrides, shaped exactly as the file carries them:
        // application "ACAD", naming itself "DSTYLE" in its first string value,
        // then (variable code, value) pairs. Variable 140 = text height.
        let mut rec = acadrust::xdata::ExtendedDataRecord::new("ACAD");
        rec.add_value(XDataValue::String("DSTYLE".into()));
        rec.add_value(XDataValue::ControlString("{".into()));
        rec.add_value(XDataValue::Integer16(140));
        rec.add_value(XDataValue::Real(0.42));
        rec.add_value(XDataValue::ControlString("}".into()));
        tol.common.extended_data.add_record(rec);

        let (boxes, _) = tessellate_tolerance(&tol, &doc);
        // 2 x 0.42 — the frame follows the entity's own character height.
        assert!(
            (frame_height(&boxes) - 0.84).abs() < 1e-3,
            "override ignored: frame is {} tall, expected 0.84",
            frame_height(&boxes)
        );
    }

    fn frame_height(boxes: &[Vec<[f32; 2]>]) -> f32 {
        let ys: Vec<f32> = boxes.iter().flatten().map(|p| p[1]).collect();
        ys.iter().cloned().fold(f32::MIN, f32::max) - ys.iter().cloned().fold(f32::MAX, f32::min)
    }

    /// Rows read downward and start at the left edge.
    #[test]
    fn rows_stack_downward_and_left_align() {
        let doc = acadrust::CadDocument::new();
        let mut tol = Tolerance::new();
        tol.text = BENCH.to_string();
        let (boxes, _) = tessellate_tolerance(&tol, &doc);

        // One closed rect per row (plus dividers); every rect starts at x = 0.
        let rects: Vec<&Vec<[f32; 2]>> = boxes.iter().filter(|b| b.len() == 5).collect();
        assert_eq!(rects.len(), 3, "one border per row");
        for r in &rects {
            let x0 = r.iter().map(|p| p[0]).fold(f32::MAX, f32::min);
            assert!(x0.abs() < 1e-3, "row border not left-aligned: x0={x0}");
        }
        // The insertion point is the first row's centreline: row 0 straddles
        // y = 0 and the frame only ever grows downward from it, so the anchor
        // does not drift with the number of rows.
        let h = tol.text_height as f32;
        let top = rects
            .iter()
            .flat_map(|r| r.iter())
            .map(|p| p[1])
            .fold(f32::MIN, f32::max);
        let bottom = rects
            .iter()
            .flat_map(|r| r.iter())
            .map(|p| p[1])
            .fold(f32::MAX, f32::min);
        assert!(
            (top - h).abs() < 1e-4,
            "frame should reach +h ({h}) above the insertion point, reaches {top}"
        );
        // 3 rows of 2h, the first centred on 0 -> the last bottom is -5h.
        assert!(
            (bottom - (-5.0 * h)).abs() < 1e-4,
            "three rows should reach -5h ({}) below, reach {bottom}",
            -5.0 * h
        );

        // Row 0 (4 compartments) is the widest AND the topmost.
        let top = |r: &Vec<[f32; 2]>| r.iter().map(|p| p[1]).fold(f32::MIN, f32::max);
        let wide = |r: &Vec<[f32; 2]>| r.iter().map(|p| p[0]).fold(f32::MIN, f32::max);
        assert!(
            top(rects[0]) > top(rects[1]) && top(rects[1]) > top(rects[2]),
            "rows must stack downward, row 0 on top"
        );
        assert!(
            wide(rects[0]) > wide(rects[1]),
            "the lone datum row must not inherit the four-compartment row's width"
        );
    }

    /// The cell is measured with the font's normal letter spacing, so it must be
    /// DRAWN with it too. They diverged once — the measurement assumed normal
    /// spacing while the glyph run asked for none — and the characters ran
    /// together inside correctly-sized boxes.
    #[test]
    fn cells_are_drawn_with_the_spacing_they_were_measured_with() {
        use acadrust::EntityType;
        let doc = acadrust::CadDocument::new();
        let mut tol = Tolerance::new();
        tol.text = "ABC".into();

        let Some(TruckEntity {
            object: TruckObject::Text(groups),
            ..
        }) = tol.to_truck(&doc)
        else {
            panic!("tolerance produced no text");
        };
        // The frame is the run-less group; every cell carries a run.
        let runs: Vec<_> = groups.iter().filter_map(|g| g.run.as_ref()).collect();
        assert!(!runs.is_empty(), "no cell runs emitted");
        for r in &runs {
            assert_eq!(
                r.tracking, 1.0,
                "cell text must use the font's normal letter spacing, like every \
                 other entity and like the width measurement"
            );
        }
    }

    /// The symbol font is reached by NAME, through the same lookup every other
    /// font goes through — so a switch to it works wherever inline codes do, not
    /// only inside a tolerance. An MTEXT carrying `{\\Fgdt;j}`, or a text style
    /// literally named "gdt.shx", lands on it too, with no code of their own.
    #[test]
    fn the_symbol_font_is_reached_by_name_like_any_other() {
        use crate::scene::text::font_face::Face;
        for name in ["gdt", "GDT", "gdt.shx"] {
            assert!(
                crate::scene::text::lff::is_builtin(name),
                "{name:?} must resolve to the bundled symbol font"
            );
            let face = Face::resolve(name);
            let Face::Lff(font) = &face else {
                panic!("{name:?} resolved to a system outline, not the stroke font");
            };
            assert_eq!(
                font.name.to_ascii_lowercase(),
                "gdt",
                "{name:?} landed on {}",
                font.name
            );
            // Every character the escape can carry draws from it.
            for c in 'a'..='z' {
                assert!(font.glyph(c).is_some(), "{name:?} has no glyph for {c:?}");
            }
        }
    }

    /// It holds symbols, not letters: the glyph for 'm' is the circled M, and
    /// must not be the letter m the text fonts draw.
    #[test]
    fn a_letter_in_the_symbol_font_is_a_symbol() {
        use crate::scene::text::font_face::Face;
        let gdt = Face::resolve(SYMBOL_FONT);
        let txt = Face::resolve(TEXT_FONT);
        let mut differ = 0;
        for c in 'a'..='z' {
            let (g, x) = (gdt.glyph(c), txt.glyph(c));
            if let (Some(g), Some(x)) = (g, x) {
                if g.strokes.len() != x.strokes.len() || (g.advance - x.advance).abs() > 1e-3 {
                    differ += 1;
                }
            }
        }
        assert!(
            differ >= 20,
            "only {differ} of 26 differ from the text font — the symbol font is \
             drawing letters"
        );
    }

    /// A compartment is measured by what it draws, not by where the pen lands.
    /// The pen stops one letter-space past the last glyph — that gap belongs to
    /// whatever comes next, and counting it made every compartment that much
    /// too wide.
    #[test]
    fn compartments_are_measured_by_their_ink_not_the_trailing_pen_gap() {
        // Cap height, so glyph units and world units line up.
        let h = 9.0_f32;
        for src in ["{\\Fgdt;r}", "{\\Fgdt;n}tol{\\Fgdt;m}", "1{\\Fgdt;m}", "A"] {
            let cell = parse_cell(src);
            let pen: f32 = cell.iter().map(|r| run_advance(&r.text, r.font, h)).sum();
            let ink: f32 = cell
                .iter()
                .map(|r| {
                    crate::entities::text_support::text_local_bounds(r.font, &r.text, h, 1.0, 0.0)
                        .map(|b| b.ink_max[0] - b.ink_min[0])
                        .unwrap_or(0.0)
                })
                .sum();
            let got = content_width(&cell, h);

            // Exactly one trailing gap comes off — the gaps BETWEEN runs are
            // real spacing and must stay.
            let spacing = letter_spacing(cell.last().unwrap().font, h);
            assert!(
                (got - (pen - spacing)).abs() < 1e-3,
                "{src:?}: content {got} should be pen {pen} less one {spacing} gap"
            );
            let expected = ink + (cell.len() as f32 - 1.0) * spacing;
            assert!(
                (got - expected).abs() < 1e-3,
                "{src:?}: content {got} should be ink {ink} plus {} inter-run gaps",
                cell.len() - 1
            );
        }
    }
}
