//! Shared spec fixtures for fit-quality tests and benchmarks.
//!
//! Ports the 17 reproducibility specs from eulerr's
//! `tests/testthat/test-reproducibility.R`. Each entry exposes a
//! [`CorpusEntry::build`] function returning a [`DiagramSpec`] plus a
//! permissive `diag_error` ceiling per shape type. The integration tests in
//! `fitter::corpus_quality` consume this list to characterise the *default*
//! `Fitter::<S>` user experience across a representative spec mix.

use crate::spec::{DiagramSpec, DiagramSpecBuilder, InputType};

/// Difficulty class for a corpus spec, used to pick a default `diag_error`
/// ceiling when an entry doesn't specify one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
    /// Trivially representable layouts (uniform overlap, fully disjoint).
    Easy,
    /// Realistic mixed overlaps; expected to fit well by default.
    Medium,
    /// Known-hard for the relevant shape (containment, near-degenerate).
    Hard,
}

/// How the static-corpus test should treat a spec for one shape type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fittable {
    /// Run `fit()` and assert `diag_error` is within the configured ceiling.
    Normal,
    /// `fit()` is expected to either fail (e.g. single-set specs that fail
    /// preprocessing) or succeed with no meaningful `diag_error` to assert
    /// against. The harness only checks the call doesn't panic and, if it
    /// returns `Ok`, that `loss().is_finite()`.
    SanityOnly,
    /// Skip this spec entirely for the relevant shape. Used for specs that
    /// hit a known-broken code path upstream — record the spec, don't
    /// attempt the fit. Each `Skip` carries a short note used in the
    /// report.
    Skip(&'static str),
}

/// One spec in the test corpus.
pub struct CorpusEntry {
    /// Stable identifier used in test output and lookup-by-name.
    pub name: &'static str,
    /// Builder for the spec — kept as a fn pointer so the corpus list can
    /// live in a `static` without runtime allocation.
    pub build: fn() -> DiagramSpec,
    /// Difficulty classification (drives the default ceiling).
    pub category: Category,
    /// Explicit per-shape `diag_error` ceiling. `None` falls back to the
    /// category default ([`DEFAULT_MAX_DIAG_ERROR_EASY`],
    /// [`DEFAULT_MAX_DIAG_ERROR_MEDIUM`], [`DEFAULT_MAX_DIAG_ERROR_HARD`]).
    pub max_diag_error_circle: Option<f64>,
    /// Explicit per-shape `diag_error` ceiling for ellipses.
    pub max_diag_error_ellipse: Option<f64>,
    /// Explicit per-shape `diag_error` ceiling for squares.
    pub max_diag_error_square: Option<f64>,
    /// Explicit per-shape `diag_error` ceiling for rectangles.
    pub max_diag_error_rectangle: Option<f64>,
    /// How the harness should treat the spec under [`Circle`].
    ///
    /// [`Circle`]: crate::geometry::shapes::Circle
    pub fittable_circle: Fittable,
    /// How the harness should treat the spec under [`Ellipse`].
    ///
    /// [`Ellipse`]: crate::geometry::shapes::Ellipse
    pub fittable_ellipse: Fittable,
    /// How the harness should treat the spec under [`Square`].
    ///
    /// [`Square`]: crate::geometry::shapes::Square
    pub fittable_square: Fittable,
    /// How the harness should treat the spec under [`Rectangle`].
    ///
    /// [`Rectangle`]: crate::geometry::shapes::Rectangle
    pub fittable_rectangle: Fittable,
}

impl CorpusEntry {
    /// Resolve the active `diag_error` ceiling for circles, falling back
    /// to the category default when the per-spec override is `None`.
    pub fn ceiling_circle(&self) -> f64 {
        self.max_diag_error_circle
            .unwrap_or_else(|| default_ceiling(self.category))
    }

    /// Resolve the active `diag_error` ceiling for ellipses.
    pub fn ceiling_ellipse(&self) -> f64 {
        self.max_diag_error_ellipse
            .unwrap_or_else(|| default_ceiling(self.category))
    }

    /// Resolve the active `diag_error` ceiling for squares.
    pub fn ceiling_square(&self) -> f64 {
        self.max_diag_error_square
            .unwrap_or_else(|| default_ceiling(self.category))
    }

    /// Resolve the active `diag_error` ceiling for rectangles.
    pub fn ceiling_rectangle(&self) -> f64 {
        self.max_diag_error_rectangle
            .unwrap_or_else(|| default_ceiling(self.category))
    }
}

/// Default `diag_error` ceiling for [`Category::Easy`] specs.
pub const DEFAULT_MAX_DIAG_ERROR_EASY: f64 = 5e-3;
/// Default `diag_error` ceiling for [`Category::Medium`] specs.
pub const DEFAULT_MAX_DIAG_ERROR_MEDIUM: f64 = 5e-3;
/// Default `diag_error` ceiling for [`Category::Hard`] specs.
pub const DEFAULT_MAX_DIAG_ERROR_HARD: f64 = 5e-2;

/// `issue71_4_set_extreme_scale` ellipse ceiling. The spec's 4-order-of-
/// magnitude area variation (A=38066 vs D=6) makes the final-stage
/// optimisation sensitive to FP rounding in `sin`/`cos`/conic-intersection
/// math, and seed-to-seed basin selection in LM/CMA-ES is fragile:
/// non-glibc math runtimes (Windows MSVC, macOS libm) return slightly
/// different ULP values than glibc, and the optimizer stack matters too — the
/// historical argmin 0.10 / levenberg-marquardt 0.13 stack (CRAN MSRV 1.84.1)
/// landed seed=42 on Linux in a different basin than the current basin solvers.
/// Across `TEST_SEEDS`, two of three seeds hit `~1e-4`; the third lands
/// at `~1.5e-1`. The ceiling admits that variance — the spec is not a
/// regression, just a different basin choice on a known-fragile spec.
pub const ISSUE71_ELLIPSE_CEILING: f64 = 2e-1;

fn default_ceiling(c: Category) -> f64 {
    match c {
        Category::Easy => DEFAULT_MAX_DIAG_ERROR_EASY,
        Category::Medium => DEFAULT_MAX_DIAG_ERROR_MEDIUM,
        Category::Hard => DEFAULT_MAX_DIAG_ERROR_HARD,
    }
}

/// Seeds used by the bench-style quality sweep. Wide enough to catch
/// unlucky basins without dominating runtime. Mirrors the legacy
/// `QUALITY_SEEDS` array in `benches/initial_layout.rs`.
pub const QUALITY_SEEDS: [u64; 16] = [1, 2, 3, 7, 13, 17, 23, 29, 31, 37, 41, 42, 47, 53, 59, 61];

/// Small fixed seed list for the default-suite quality test. Three seeds is
/// enough to surface obvious regressions without blowing the wall-time
/// budget; the full 16-seed sweep lives in the bench harness / future
/// quality-report binary.
pub const TEST_SEEDS: [u64; 3] = [1, 42, 7];

/// All corpus specs in a deterministic order. Entries 0-16 mirror
/// `eulerr/tests/testthat/test-reproducibility.R:1-188`; subsequent
/// entries are issue-derived and bench-shared probes.
pub fn all() -> &'static [CorpusEntry] {
    &CORPUS
}

/// Look up a corpus entry by its `name`.
pub fn get(name: &str) -> Option<&'static CorpusEntry> {
    CORPUS.iter().find(|e| e.name == name)
}

// 17 reproducibility specs from eulerr.
// The categorisation reflects what circles and ellipses can faithfully
// represent at default `Fitter` settings. Russian-doll containment and
// 6-set Wilkinson are the obvious "Hard" cases for both shapes; specs
// that can't be fit at all (single-set #16) are marked `SanityOnly`.

#[allow(clippy::too_many_lines)]
static CORPUS: [CorpusEntry; 30] = [
    CorpusEntry {
        name: "uniform_3_set",
        build: spec_uniform_3_set,
        category: Category::Easy,
        // Symmetric 3-Venn with uniform overlaps cannot be fit exactly by
        // circles (eulerr reports ~2-3% here); ellipses get it to ~0.
        // Axis-aligned squares cannot represent the 3-fold rotational
        // symmetry exactly (default fitter lands at ~3.07e-2 across seeds).
        max_diag_error_circle: Some(3e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "three_disjoint",
        build: spec_three_disjoint,
        category: Category::Easy,
        max_diag_error_circle: None,
        max_diag_error_ellipse: None,
        max_diag_error_square: None,
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "one_contained",
        build: spec_one_contained,
        category: Category::Medium,
        // Containment is hard for circles; eulerr accepts ~2-3% here.
        // Squares land in two basins across TEST_SEEDS — best ~1e-2,
        // worst ~7e-2 (seed=1).
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "two_inside_third",
        build: spec_two_inside_third,
        category: Category::Medium,
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "one_contained_others_interacting",
        build: spec_one_contained_others_interacting,
        category: Category::Medium,
        // Squares land at ~4-6e-2 across TEST_SEEDS.
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(8e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "russian_doll",
        build: spec_russian_doll,
        category: Category::Hard,
        // Circles cannot represent strict containment exactly.
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(5e-2),
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "unequal_overlaps",
        build: spec_unequal_overlaps,
        category: Category::Medium,
        // Axis-aligned squares can't represent A&C+B&C+A&B&C with A∩B=0
        // here without forcing B∩C ≠ 0; lands deterministically at 4.4e-2
        // across all TEST_SEEDS.
        max_diag_error_circle: None,
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "two_disjoint",
        build: spec_two_disjoint,
        category: Category::Easy,
        max_diag_error_circle: None,
        max_diag_error_ellipse: None,
        max_diag_error_square: None,
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "wilkinson_6_set",
        build: spec_wilkinson_6_set,
        category: Category::Hard,
        // Circles can't represent the 6-set Wilkinson layout faithfully;
        // ellipses can (eulerr reaches ~1e-9 with tighter settings, but
        // default Fitter at TEST_SEEDS varies).
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(5e-2),
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "gene_sets",
        build: spec_gene_sets,
        category: Category::Medium,
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "three_inside_fourth",
        build: spec_three_inside_fourth,
        category: Category::Hard,
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(5e-2),
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "eulerape_3_set",
        build: spec_eulerape_3_set,
        category: Category::Easy,
        // Asymmetric 3-set from the eulerAPE article: circles cannot
        // achieve the exact triple intersection. Ellipses fit this near
        // machine zero on most seeds after the log-space `(a, b)`
        // reparameterisation closed the canonical basin (median diag
        // ~7.7e-16 in `examples/quality_report`).
        max_diag_error_circle: Some(2e-2),
        max_diag_error_ellipse: Some(2e-2),
        max_diag_error_square: Some(2e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "one_disjoint_two_intersecting",
        build: spec_one_disjoint_two_intersecting,
        category: Category::Medium,
        max_diag_error_circle: None,
        max_diag_error_ellipse: None,
        max_diag_error_square: None,
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "four_uniform_interactions",
        build: spec_four_uniform_interactions,
        category: Category::Medium,
        // 4-set uniform overlaps stretch what circles can fit.
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "two_overlapping_completely",
        build: spec_two_overlapping_completely,
        category: Category::Easy,
        max_diag_error_circle: None,
        max_diag_error_ellipse: None,
        max_diag_error_square: None,
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "single_set",
        build: spec_single_set,
        category: Category::Hard,
        max_diag_error_circle: None,
        max_diag_error_ellipse: None,
        max_diag_error_square: None,
        max_diag_error_rectangle: None,
        // Preprocessing rejects n_sets <= 1, so fit() returns Err.
        fittable_circle: Fittable::SanityOnly,
        fittable_ellipse: Fittable::SanityOnly,
        fittable_square: Fittable::SanityOnly,
        fittable_rectangle: Fittable::SanityOnly,
    },
    CorpusEntry {
        name: "random_4_set",
        build: spec_random_4_set,
        category: Category::Medium,
        // Hardcoded seeded random values — no expected analytic answer,
        // but a well-formed 4-set with all pairwise + triple + quad
        // intersections. Random area inputs aren't guaranteed to be
        // representable by any shape, so the circle ceiling is loose.
        // The ellipse ceiling is tightened to 3e-2: ~13/16 master seeds
        // land in a deterministic basin at diag ≈ 2.606e-2 (loss 7.786e-3)
        // and a better basin at diag ≈ 1.147e-2 (loss 4.335e-3) is
        // reachable from a minority of MDS inits but not reliably found by
        // either more restarts or the CMA-ES global stage. See TODO.md.
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: Some(3e-2),
        // Squares land deterministically at ~7.5e-2 across TEST_SEEDS.
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    // -- issue-derived specs (eulerr issue tracker) --
    // These come from real bug reports against eulerr's `euler()` and exercise
    // pathologies the original 17-spec reproducibility suite doesn't reach:
    // small overlaps that fit-to-zero, extreme scale variation, full 6-set
    // coverage, and the only `InputType::Inclusive` entry in the corpus.
    // Ceilings are loose by default — tighten as the optimizer improves.
    CorpusEntry {
        name: "issue54_6_set_full",
        build: spec_issue54_6_set_full,
        category: Category::Hard,
        // Full 6-set with all 63 combinations; only ~20 are non-zero. Stresses
        // the high-arity intersection code path much harder than wilkinson_6_set.
        max_diag_error_circle: Some(1.5e-1),
        max_diag_error_ellipse: Some(7e-2),
        max_diag_error_square: Some(1.5e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue114_4_set_dominant_quad",
        build: spec_issue114_4_set_dominant_quad,
        category: Category::Hard,
        // Real biology-style 4-set where the 4-way intersection dominates
        // (A&B&C&D = 10336 vs A only = 7516); D singleton is huge (26642).
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(5e-2),
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue47_3_set_huge_triple",
        build: spec_issue47_3_set_huge_triple,
        category: Category::Hard,
        // 3-set where pairs are small (15-40) but A&B&C = 120 — geometrically
        // impossible for circles (triangle-inequality on overlap regions),
        // ellipses can fit it.
        max_diag_error_circle: Some(8e-2),
        max_diag_error_ellipse: Some(3e-2),
        // Squares land at ~7.0-7.3e-2 — barely under the natural 8e-2 cap.
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue92_3_set_dropped_pair",
        build: spec_issue92_3_set_dropped_pair,
        category: Category::Medium,
        // Minimal repro of "small pair → fitted=0" pathology. A&B=12 sits
        // between large A&C=459, B&C=703, A&B&C=162.
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: Some(2e-2),
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue44_4_set_inclusive",
        build: spec_issue44_4_set_inclusive,
        category: Category::Hard,
        // Only `InputType::Inclusive` (eulerr `input="union"`) entry. The
        // inclusion-exclusion decomposition zeros out A only and B only — A
        // and B are fully covered by their pairwise and triple intersections
        // — so the geometry has two "doubly covered" sets and is genuinely
        // hard. Default fitter lands ~1.5e-2 most seeds, ~9e-2 worst case.
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(1e-1),
        // Squares land at ~9.3e-2 typically but seed=7 hits 1.01e-1.
        max_diag_error_square: Some(1.5e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue71_4_set_extreme_scale",
        build: spec_issue71_4_set_extreme_scale,
        category: Category::Hard,
        // 4 orders of magnitude scale variation (A=38066 vs D=6). Stress test
        // for the `SumSquared` loss on extreme dynamic range.
        // Ellipse ceiling is platform-conditional — see
        // [`ISSUE71_ELLIPSE_CEILING`] for the rationale.
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(ISSUE71_ELLIPSE_CEILING),
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue103_4_set_missing_d",
        build: spec_issue103_4_set_missing_d,
        category: Category::Hard,
        // 4-set with all 15 combinations populated; user reports A&D and D
        // missing from the plot intermittently. B-dominated (B=455 vs others <100).
        max_diag_error_circle: Some(1e-1),
        max_diag_error_ellipse: Some(5e-2),
        max_diag_error_square: Some(1e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue32_3_set_small_triple",
        build: spec_issue32_3_set_small_triple,
        category: Category::Medium,
        // 3-set with two zero pairs (A&B=0, B&C=0) and a small triple (A&B&C=3)
        // sandwiched in a large A&C=314. Tests "small intersection drop"
        // pathology, sibling to issue92.
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: Some(2e-2),
        // Squares land at ~4.4-5.9e-2 across TEST_SEEDS.
        max_diag_error_square: Some(7e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue91_6_set",
        build: spec_issue91_6_set,
        category: Category::Hard,
        // Full 6-set with 63 mostly-positive combinations. Values were passed
        // disjoint (eulerr default) by the reporter even though they look
        // intersection-style, so the spec is far from any realizable geometry —
        // hard exclusive-input stress test for high-arity fits.
        max_diag_error_circle: Some(2e-1),
        max_diag_error_ellipse: Some(1e-1),
        max_diag_error_square: Some(2e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue111_3_set_asymmetric",
        build: spec_issue111_3_set_asymmetric,
        category: Category::Medium,
        // 3-set with two orders of magnitude scale variation
        // (A=10000, B=1000, C=100) plus moderate intersections.
        max_diag_error_circle: Some(5e-2),
        max_diag_error_ellipse: Some(2e-2),
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "issue93_5_set_kinases",
        build: spec_issue93_5_set_kinases,
        category: Category::Medium,
        // 5-set kinase example with eight small named intersections (all ≤3)
        // sitting on top of much larger singletons (9-23) and five disjoint
        // pairs. Every shape drops the largest missing region
        // (camk&cmgc&tk&tkl=3), pinning `diag_error` near 3.26e-2 across all
        // `TEST_SEEDS`. Diagnostic for the "small overlaps fit-to-zero"
        // pathology at n=5 — the SumSquared loss is dominated by the five
        // large singletons, so an optimum that abandons the smallest
        // intersections is cheap.
        max_diag_error_circle: Some(4e-2),
        max_diag_error_ellipse: Some(4e-2),
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: Some(5e-2),
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "three_set_small_overlaps",
        build: spec_three_set_small_overlaps,
        category: Category::Easy,
        // Uniform 3-set with small overlap fractions (~14% pairwise).
        // Same rotational-symmetry obstruction as `uniform_3_set` for
        // circles and squares, but the small overlap magnitude makes the
        // residual error proportionally smaller. Ellipses fit it cleanly.
        max_diag_error_circle: Some(3e-2),
        max_diag_error_ellipse: None,
        max_diag_error_square: Some(5e-2),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
    CorpusEntry {
        name: "three_set_triple_only",
        build: spec_three_set_triple_only,
        category: Category::Hard,
        // 3-set where every pairwise overlap equals the triple (no
        // pairwise-only regions). Circles and axis-aligned squares cannot
        // represent this exactly — `A∩B = A∩B∩C` forces a degenerate
        // configuration in either family — and land at `~1.22e-1` across
        // `TEST_SEEDS`. Ellipses fit it to `~3.3e-3`.
        max_diag_error_circle: Some(1.5e-1),
        max_diag_error_ellipse: Some(2e-2),
        max_diag_error_square: Some(1.5e-1),
        max_diag_error_rectangle: None,
        fittable_circle: Fittable::Normal,
        fittable_ellipse: Fittable::Normal,
        fittable_square: Fittable::Normal,
        fittable_rectangle: Fittable::Normal,
    },
];

// Builders. Names mirror the comments in `test-reproducibility.R`.
// All values use exclusive (`disjoint` in eulerr terminology) input,
// except `spec_issue44_4_set_inclusive` which uses `InputType::Inclusive`.

fn spec_uniform_3_set() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10.0)
        .set("B", 10.0)
        .set("C", 10.0)
        .intersection(&["A", "B"], 4.0)
        .intersection(&["A", "C"], 4.0)
        .intersection(&["B", "C"], 4.0)
        .intersection(&["A", "B", "C"], 2.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("uniform_3_set")
}

fn spec_three_disjoint() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 1.0)
        .set("B", 1.0)
        .set("C", 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("three_disjoint")
}

fn spec_one_contained() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10.0)
        .set("B", 10.0)
        .set("C", 0.0)
        .intersection(&["A", "B"], 4.0)
        .intersection(&["A", "C"], 0.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["A", "B", "C"], 3.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("one_contained")
}

fn spec_two_inside_third() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 15.0)
        .set("B", 0.0)
        .set("C", 0.0)
        .intersection(&["A", "B"], 3.0)
        .intersection(&["A", "C"], 3.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["A", "B", "C"], 2.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("two_inside_third")
}

fn spec_one_contained_others_interacting() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 15.0)
        .set("B", 15.0)
        .set("C", 0.0)
        .intersection(&["A", "B"], 3.0)
        .intersection(&["A", "C"], 0.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["A", "B", "C"], 3.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("one_contained_others_interacting")
}

fn spec_russian_doll() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 15.0)
        .set("B", 0.0)
        .set("C", 0.0)
        .intersection(&["A", "B"], 10.0)
        .intersection(&["A", "C"], 0.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["A", "B", "C"], 5.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("russian_doll")
}

fn spec_unequal_overlaps() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 7.0)
        .set("B", 6.0)
        .set("C", 0.0)
        .intersection(&["A", "B"], 0.0)
        .intersection(&["A", "C"], 1.0)
        .intersection(&["B", "C"], 1.0)
        .intersection(&["A", "B", "C"], 2.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("unequal_overlaps")
}

fn spec_two_disjoint() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10.0)
        .set("B", 9.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("two_disjoint")
}

fn spec_wilkinson_6_set() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 4.0)
        .set("B", 6.0)
        .set("C", 3.0)
        .set("D", 2.0)
        .set("E", 7.0)
        .set("F", 3.0)
        .intersection(&["A", "B"], 2.0)
        .intersection(&["A", "F"], 2.0)
        .intersection(&["B", "C"], 2.0)
        .intersection(&["B", "D"], 1.0)
        .intersection(&["B", "F"], 2.0)
        .intersection(&["C", "D"], 1.0)
        .intersection(&["D", "E"], 1.0)
        .intersection(&["E", "F"], 1.0)
        .intersection(&["A", "B", "F"], 1.0)
        .intersection(&["B", "C", "D"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("wilkinson_6_set")
}

fn spec_gene_sets() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("SE", 13.0)
        .set("Treat", 28.0)
        .set("Anti-CCP", 101.0)
        .set("DAS28", 91.0)
        .intersection(&["SE", "Treat"], 1.0)
        .intersection(&["SE", "DAS28"], 14.0)
        .intersection(&["Treat", "Anti-CCP"], 6.0)
        .intersection(&["SE", "Anti-CCP", "DAS28"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("gene_sets")
}

fn spec_three_inside_fourth() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 30.0)
        .intersection(&["A", "B"], 3.0)
        .intersection(&["A", "C"], 3.0)
        .intersection(&["A", "D"], 3.0)
        .intersection(&["A", "B", "C"], 2.0)
        .intersection(&["A", "B", "D"], 2.0)
        .intersection(&["A", "C", "D"], 2.0)
        .intersection(&["A", "B", "C", "D"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("three_inside_fourth")
}

fn spec_eulerape_3_set() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("a", 3491.0)
        .set("b", 3409.0)
        .set("c", 3503.0)
        .intersection(&["a", "b"], 120.0)
        .intersection(&["a", "c"], 114.0)
        .intersection(&["b", "c"], 132.0)
        .intersection(&["a", "b", "c"], 126.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("eulerape_3_set")
}

fn spec_one_disjoint_two_intersecting() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 1.0)
        .set("B", 0.4)
        .set("C", 3.0)
        .intersection(&["A", "B"], 0.2)
        .intersection(&["A", "C"], 0.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["A", "B", "C"], 0.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("one_disjoint_two_intersecting")
}

fn spec_four_uniform_interactions() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10.0)
        .set("B", 10.0)
        .set("C", 10.0)
        .set("D", 10.0)
        .intersection(&["A", "B"], 3.0)
        .intersection(&["A", "C"], 3.0)
        .intersection(&["A", "D"], 0.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["B", "D"], 3.0)
        .intersection(&["C", "D"], 3.0)
        .intersection(&["A", "B", "C"], 1.0)
        .intersection(&["A", "B", "D"], 1.0)
        .intersection(&["A", "C", "D"], 1.0)
        .intersection(&["B", "C", "D"], 1.0)
        .intersection(&["A", "B", "C", "D"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("four_uniform_interactions")
}

fn spec_two_overlapping_completely() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 0.0)
        .set("B", 0.0)
        .intersection(&["A", "B"], 10.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("two_overlapping_completely")
}

fn spec_single_set() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("single_set")
}

// 4-set spec from `set.seed(1); runif(15)` in R, ordered:
// A, B, C, E, A&B, A&C, A&E, B&C, B&E, C&E, A&B&C, A&B&E, A&C&E, B&C&E, A&B&C&E.
// Reproduces eulerr's `s[[17]]` exactly.
fn spec_random_4_set() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 0.265_508_663_142_1)
        .set("B", 0.372_123_899_636_79)
        .set("C", 0.572_853_363_351_896_4)
        .set("E", 0.908_207_789_994_776_2)
        .intersection(&["A", "B"], 0.201_681_931_037_455_8)
        .intersection(&["A", "C"], 0.898_389_684_967_696_7)
        .intersection(&["A", "E"], 0.944_675_268_605_351_4)
        .intersection(&["B", "C"], 0.660_797_792_486_846_4)
        .intersection(&["B", "E"], 0.629_114_043_898_880_5)
        .intersection(&["C", "E"], 0.061_786_270_467_564_46)
        .intersection(&["A", "B", "C"], 0.205_974_574_899_301)
        .intersection(&["A", "B", "E"], 0.176_556_752_528_995_3)
        .intersection(&["A", "C", "E"], 0.687_022_846_657_782_8)
        .intersection(&["B", "C", "E"], 0.384_103_718_213_737)
        .intersection(&["A", "B", "C", "E"], 0.769_841_419_998_556_4)
        .input_type(InputType::Exclusive)
        .build()
        .expect("random_4_set")
}

// eulerr issue #54: 6-set with all 63 combinations populated.
// `cts` / `nms` arrays from the issue body, decoded to A..F.
fn spec_issue54_6_set_full() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 27.0)
        .set("B", 7.0)
        .set("C", 17.0)
        .set("D", 12.0)
        .set("E", 12.0)
        .set("F", 11.0)
        .intersection(&["A", "B"], 5.0)
        .intersection(&["A", "C"], 0.0)
        .intersection(&["A", "D"], 12.0)
        .intersection(&["A", "E"], 0.0)
        .intersection(&["A", "F"], 1.0)
        .intersection(&["B", "C"], 1.0)
        .intersection(&["B", "D"], 0.0)
        .intersection(&["B", "E"], 4.0)
        .intersection(&["B", "F"], 0.0)
        .intersection(&["C", "D"], 1.0)
        .intersection(&["C", "E"], 0.0)
        .intersection(&["C", "F"], 4.0)
        .intersection(&["D", "E"], 1.0)
        .intersection(&["D", "F"], 1.0)
        .intersection(&["E", "F"], 0.0)
        .intersection(&["A", "B", "C"], 0.0)
        .intersection(&["A", "B", "D"], 0.0)
        .intersection(&["A", "B", "E"], 0.0)
        .intersection(&["A", "B", "F"], 0.0)
        .intersection(&["A", "C", "D"], 1.0)
        .intersection(&["A", "C", "E"], 0.0)
        .intersection(&["A", "C", "F"], 0.0)
        .intersection(&["A", "D", "E"], 0.0)
        .intersection(&["A", "D", "F"], 1.0)
        .intersection(&["A", "E", "F"], 0.0)
        .intersection(&["B", "C", "D"], 0.0)
        .intersection(&["B", "C", "E"], 0.0)
        .intersection(&["B", "C", "F"], 0.0)
        .intersection(&["B", "D", "E"], 0.0)
        .intersection(&["B", "D", "F"], 0.0)
        .intersection(&["B", "E", "F"], 0.0)
        .intersection(&["C", "D", "E"], 0.0)
        .intersection(&["C", "D", "F"], 0.0)
        .intersection(&["C", "E", "F"], 1.0)
        .intersection(&["D", "E", "F"], 0.0)
        .intersection(&["A", "B", "C", "D"], 0.0)
        .intersection(&["A", "B", "C", "E"], 0.0)
        .intersection(&["A", "B", "C", "F"], 0.0)
        .intersection(&["A", "B", "D", "E"], 0.0)
        .intersection(&["A", "B", "D", "F"], 1.0)
        .intersection(&["A", "B", "E", "F"], 0.0)
        .intersection(&["A", "C", "D", "E"], 0.0)
        .intersection(&["A", "C", "D", "F"], 0.0)
        .intersection(&["A", "C", "E", "F"], 0.0)
        .intersection(&["A", "D", "E", "F"], 0.0)
        .intersection(&["B", "C", "D", "E"], 0.0)
        .intersection(&["B", "C", "D", "F"], 0.0)
        .intersection(&["B", "C", "E", "F"], 0.0)
        .intersection(&["B", "D", "E", "F"], 0.0)
        .intersection(&["C", "D", "E", "F"], 0.0)
        .intersection(&["A", "B", "C", "D", "E"], 0.0)
        .intersection(&["A", "B", "C", "D", "F"], 0.0)
        .intersection(&["A", "B", "C", "E", "F"], 0.0)
        .intersection(&["A", "B", "D", "E", "F"], 0.0)
        .intersection(&["A", "C", "D", "E", "F"], 0.0)
        .intersection(&["B", "C", "D", "E", "F"], 0.0)
        .intersection(&["A", "B", "C", "D", "E", "F"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue54_6_set_full")
}

// eulerr issue #114: 4-set with all 15 combinations populated, exclusive input.
// Real biology-style data; A&B&C&D dominates singletons.
fn spec_issue114_4_set_dominant_quad() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 7516.0)
        .set("B", 7621.0)
        .set("C", 3152.0)
        .set("D", 26642.0)
        .intersection(&["A", "B"], 781.0)
        .intersection(&["A", "C"], 817.0)
        .intersection(&["A", "D"], 6418.0)
        .intersection(&["B", "C"], 369.0)
        .intersection(&["B", "D"], 1465.0)
        .intersection(&["C", "D"], 4118.0)
        .intersection(&["A", "B", "C"], 324.0)
        .intersection(&["A", "B", "D"], 2525.0)
        .intersection(&["A", "C", "D"], 8847.0)
        .intersection(&["B", "C", "D"], 1149.0)
        .intersection(&["A", "B", "C", "D"], 10336.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue114_4_set_dominant_quad")
}

// eulerr issue #47: 3-set where pairs are small but the triple is huge.
fn spec_issue47_3_set_huge_triple() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 500.0)
        .set("B", 400.0)
        .set("C", 400.0)
        .intersection(&["A", "B"], 30.0)
        .intersection(&["A", "C"], 40.0)
        .intersection(&["B", "C"], 15.0)
        .intersection(&["A", "B", "C"], 120.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue47_3_set_huge_triple")
}

// eulerr issue #92: 3-set with a small A&B (12) sandwiched in big A&C, B&C.
fn spec_issue92_3_set_dropped_pair() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 164.0)
        .set("B", 561.0)
        .set("C", 166.0)
        .intersection(&["A", "B"], 12.0)
        .intersection(&["A", "C"], 459.0)
        .intersection(&["B", "C"], 703.0)
        .intersection(&["A", "B", "C"], 162.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue92_3_set_dropped_pair")
}

// eulerr issue #44: 4-set with `input = "union"` (Inclusive in eunoia terms).
// Combinations not listed (A&C&D) are implicitly zero.
fn spec_issue44_4_set_inclusive() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10487.0)
        .set("B", 13190.0)
        .set("C", 15675.0)
        .set("D", 3519.0)
        .intersection(&["A", "B"], 8302.0)
        .intersection(&["A", "C"], 7501.0)
        .intersection(&["A", "D"], 2986.0)
        .intersection(&["B", "C"], 10276.0)
        .intersection(&["B", "D"], 2914.0)
        .intersection(&["C", "D"], 0.0)
        .intersection(&["A", "B", "C"], 5791.0)
        .intersection(&["A", "B", "D"], 2511.0)
        .intersection(&["A", "C", "D"], 0.0)
        .intersection(&["B", "C", "D"], 0.0)
        .intersection(&["A", "B", "C", "D"], 0.0)
        .input_type(InputType::Inclusive)
        .build()
        .expect("issue44_4_set_inclusive")
}

// eulerr issue #71: 4-set with extreme scale variation.
// User's set names "1ug, 300ng, 100ng, 50ng" mapped to A..D in spec order.
// 12 combinations listed; missing ones (B&D, B&C&D) default to 0.
fn spec_issue71_4_set_extreme_scale() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 38066.0)
        .set("B", 569.0)
        .set("C", 23.0)
        .set("D", 6.0)
        .intersection(&["A", "B"], 7211.0)
        .intersection(&["A", "C"], 88.0)
        .intersection(&["A", "D"], 9.0)
        .intersection(&["B", "C"], 15.0)
        .intersection(&["B", "D"], 0.0)
        .intersection(&["C", "D"], 1.0)
        .intersection(&["A", "B", "C"], 819.0)
        .intersection(&["A", "B", "D"], 65.0)
        .intersection(&["A", "C", "D"], 0.0)
        .intersection(&["B", "C", "D"], 0.0)
        .intersection(&["A", "B", "C", "D"], 162.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue71_4_set_extreme_scale")
}

// eulerr issue #103: 4-set with all 15 combinations populated.
// Values from `eulerResult$original.values`.
fn spec_issue103_4_set_missing_d() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 26.0)
        .set("B", 455.0)
        .set("C", 86.0)
        .set("D", 26.0)
        .intersection(&["A", "B"], 10.0)
        .intersection(&["A", "C"], 6.0)
        .intersection(&["A", "D"], 4.0)
        .intersection(&["B", "C"], 34.0)
        .intersection(&["B", "D"], 56.0)
        .intersection(&["C", "D"], 21.0)
        .intersection(&["A", "B", "C"], 2.0)
        .intersection(&["A", "B", "D"], 8.0)
        .intersection(&["A", "C", "D"], 13.0)
        .intersection(&["B", "C", "D"], 79.0)
        .intersection(&["A", "B", "C", "D"], 51.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue103_4_set_missing_d")
}

// eulerr issue #32: 3-set with two zero pairs and a small triple (3) that
// the fitter drops to zero. Values from the user's `euler(dat)` output.
fn spec_issue32_3_set_small_triple() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 201.0)
        .set("B", 72.0)
        .set("C", 266.0)
        .intersection(&["A", "B"], 0.0)
        .intersection(&["A", "C"], 314.0)
        .intersection(&["B", "C"], 0.0)
        .intersection(&["A", "B", "C"], 3.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue32_3_set_small_triple")
}

// eulerr issue #91: full 6-set with 63 combinations, exclusive input.
// User mapped sets 1..6 → A..F; values transcribed from the issue body
// (the trailing `"63"=1851` is a typo for `1&2&3&4&5&6=1851`).
#[allow(clippy::too_many_lines)]
fn spec_issue91_6_set() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 3290.0)
        .set("B", 2717.0)
        .set("C", 3569.0)
        .set("D", 3316.0)
        .set("E", 3598.0)
        .set("F", 3471.0)
        .intersection(&["A", "B"], 2717.0)
        .intersection(&["A", "C"], 2640.0)
        .intersection(&["A", "D"], 2466.0)
        .intersection(&["A", "E"], 2485.0)
        .intersection(&["A", "F"], 2415.0)
        .intersection(&["B", "C"], 2228.0)
        .intersection(&["B", "D"], 2132.0)
        .intersection(&["B", "E"], 2098.0)
        .intersection(&["B", "F"], 2059.0)
        .intersection(&["C", "D"], 3316.0)
        .intersection(&["C", "E"], 2667.0)
        .intersection(&["C", "F"], 2596.0)
        .intersection(&["D", "E"], 2495.0)
        .intersection(&["D", "F"], 2444.0)
        .intersection(&["E", "F"], 3471.0)
        .intersection(&["A", "B", "C"], 2228.0)
        .intersection(&["A", "B", "D"], 2132.0)
        .intersection(&["A", "B", "E"], 2098.0)
        .intersection(&["A", "B", "F"], 2059.0)
        .intersection(&["A", "C", "D"], 2466.0)
        .intersection(&["A", "C", "E"], 2294.0)
        .intersection(&["A", "C", "F"], 2233.0)
        .intersection(&["A", "D", "E"], 2156.0)
        .intersection(&["A", "D", "F"], 2114.0)
        .intersection(&["A", "E", "F"], 2415.0)
        .intersection(&["B", "C", "D"], 2132.0)
        .intersection(&["B", "C", "E"], 1956.0)
        .intersection(&["B", "C", "F"], 1921.0)
        .intersection(&["B", "D", "E"], 1881.0)
        .intersection(&["B", "D", "F"], 1851.0)
        .intersection(&["B", "E", "F"], 2059.0)
        .intersection(&["C", "D", "E"], 2495.0)
        .intersection(&["C", "D", "F"], 2444.0)
        .intersection(&["C", "E", "F"], 2596.0)
        .intersection(&["D", "E", "F"], 2444.0)
        .intersection(&["A", "B", "C", "D"], 2132.0)
        .intersection(&["A", "B", "C", "E"], 1956.0)
        .intersection(&["A", "B", "C", "F"], 1921.0)
        .intersection(&["A", "B", "D", "E"], 1881.0)
        .intersection(&["A", "B", "D", "F"], 1851.0)
        .intersection(&["A", "B", "E", "F"], 2059.0)
        .intersection(&["A", "C", "D", "E"], 2156.0)
        .intersection(&["A", "C", "D", "F"], 2114.0)
        .intersection(&["A", "C", "E", "F"], 2233.0)
        .intersection(&["A", "D", "E", "F"], 2114.0)
        .intersection(&["B", "C", "D", "E"], 1881.0)
        .intersection(&["B", "C", "D", "F"], 1851.0)
        .intersection(&["B", "C", "E", "F"], 1921.0)
        .intersection(&["B", "D", "E", "F"], 1851.0)
        .intersection(&["C", "D", "E", "F"], 2444.0)
        .intersection(&["A", "B", "C", "D", "E"], 1881.0)
        .intersection(&["A", "B", "C", "D", "F"], 1851.0)
        .intersection(&["A", "B", "C", "E", "F"], 1921.0)
        .intersection(&["A", "B", "D", "E", "F"], 1851.0)
        .intersection(&["A", "C", "D", "E", "F"], 2114.0)
        .intersection(&["B", "C", "D", "E", "F"], 1851.0)
        .intersection(&["A", "B", "C", "D", "E", "F"], 1851.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue91_6_set")
}

// eulerr issue #111: 3-set with two-orders-of-magnitude scale variation.
fn spec_issue111_3_set_asymmetric() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10000.0)
        .set("B", 1000.0)
        .set("C", 100.0)
        .intersection(&["A", "B"], 50.0)
        .intersection(&["A", "C"], 30.0)
        .intersection(&["B", "C"], 260.0)
        .intersection(&["A", "B", "C"], 15.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue111_3_set_asymmetric")
}

// 5-set kinase example from <https://github.com/jolars/eulerr/issues/93>. The
// original report (a typo'd `camk$tk` interpreted as `camk&tk`) has eight
// small named intersections (all ≤ 3) and five disjoint pairs across five
// large singletons (9-23). The default fitter drops the largest missing
// region (camk&cmgc&tk&tkl=3) for every shape.
fn spec_issue93_5_set_kinases() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("agc", 9.0)
        .set("camk", 17.0)
        .set("cmgc", 16.0)
        .set("tk", 16.0)
        .set("tkl", 23.0)
        .intersection(&["agc", "camk"], 1.0)
        .intersection(&["camk", "tk"], 1.0)
        .intersection(&["tk", "tkl"], 1.0)
        .intersection(&["camk", "cmgc", "tkl"], 1.0)
        .intersection(&["camk", "tk", "tkl"], 2.0)
        .intersection(&["agc", "camk", "tk", "tkl"], 1.0)
        .intersection(&["camk", "cmgc", "tk", "tkl"], 3.0)
        .intersection(&["agc", "camk", "cmgc", "tk", "tkl"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("issue93_5_set_kinases")
}

// Uniform 3-set with small overlap fractions. Used as the easy 3-circle
// case in `benches/initial_layout.rs`.
fn spec_three_set_small_overlaps() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 10.0)
        .set("B", 10.0)
        .set("C", 10.0)
        .intersection(&["A", "B"], 2.0)
        .intersection(&["B", "C"], 2.0)
        .intersection(&["A", "C"], 2.0)
        .intersection(&["A", "B", "C"], 0.5)
        .input_type(InputType::Exclusive)
        .build()
        .expect("three_set_small_overlaps")
}

// 3-set with only a triple intersection — pairwise-only regions are zero.
// User-reported case kept as a fitter probe in `benches/initial_layout.rs`.
fn spec_three_set_triple_only() -> DiagramSpec {
    DiagramSpecBuilder::new()
        .set("A", 2.2)
        .set("B", 2.0)
        .set("C", 3.0)
        .intersection(&["A", "B", "C"], 1.0)
        .input_type(InputType::Exclusive)
        .build()
        .expect("three_set_triple_only")
}

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

    #[test]
    fn corpus_has_30_unique_named_entries() {
        let entries = all();
        assert_eq!(entries.len(), 30);
        let mut names: Vec<&str> = entries.iter().map(|e| e.name).collect();
        names.sort();
        let mut deduped = names.clone();
        deduped.dedup();
        assert_eq!(names, deduped, "corpus names must be unique");
    }

    #[test]
    fn every_corpus_spec_builds() {
        for entry in all() {
            let _ = (entry.build)();
        }
    }

    #[test]
    fn lookup_by_name_works() {
        assert!(get("eulerape_3_set").is_some());
        assert!(get("nonexistent_spec").is_none());
    }
}
