frame_plugins/
elections.rs

1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// ``````````````````````````````` ELECTION PLUGINS ``````````````````````````````
14// ===============================================================================
15
16//! Defines two distinct **pluggable election models** for ranking and selecting
17//! candidates based on their stake, backing, or influence metrics.
18//!
19//! Elections are abstracted into two main models:
20//!
21//! ## Flat Election (`flat`)
22//!
23//! - Aggregates all contributions (including the candidates's own stake-if any)
24//! into a single scalar metric.
25//! - Candidates are compared using their **flattened total weight**.
26//! - Useful for scenarios where **every unit of support counts equally**
27//!   and simple proportionality is desired.
28//!
29//! ## Fair Election (`fair`)
30//!
31//! - Each backer's contribution is **kept unaggregated** (including the
32//! candidate's own stake-if any as one of the backing), preserving individual
33//! influence granularity.
34//! - Useful when the goal is to **prevent candidates from dominating through
35//! self-funding** and emphasize external support.
36//!
37//! ## Purpose
38//!
39//! Separating election models into `flat` and `fair` provides flexibility:
40//! - **Flat** for simple, proportional elections where total stake matters.
41//! - **Fair** for more security-conscious or governance-focused elections
42//! emphasizing community support.
43//!
44//! Both models implement a **pluggable algorithm interface**, enabling runtime
45//! substitution, testing of different strategies, and easy extension with new
46//! election rules.
47
48// ===============================================================================
49// ```````````````````````````` FLAT-ELECTION PLUGINS ````````````````````````````
50// ===============================================================================
51
52pub use flat::*;
53
54/// Contains **FlatElection plugin models**, which rank entities using a
55/// **single aggregated scalar (flat weight)** computed from a list of entities and
56/// their corresponding weights.
57///
58/// In this model:
59/// - Each entity is represented as a `(entity, flat_weight)` pair.
60/// - Individual contributions are **not tracked separately**; only the total flat
61/// weight per entity matters.
62/// - The plugin takes the list of entities with their flat weights and **produces
63/// a ranked output**.
64///
65/// ## Characteristics
66/// - Requires a **list of entities with their flat weights** as input.
67/// - Produces a **sorted list of entities** according to their flat weight.
68/// - Ignores the structure or distribution of contributions, focusing purely on the
69/// aggregated value.
70///
71/// ## Example Flow
72/// 1. Prepare a list of entities with their flat weights:
73///     `[(entity1, w1), (entity2, w2), ...]`.  
74/// 2. Pass this list to a FlatElection plugin model.  
75/// 3. The model sorts and outputs entities in **descending order of flat weight**.
76pub mod flat {
77
78    // ===============================================================================
79    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
80    // ===============================================================================
81
82    // --- FRAME Suite ---
83    use frame_suite::plugin_model;
84
85    // --- Substrate primitives ---
86    use sp_runtime::Vec;
87
88    // ===============================================================================
89    // ```````````````````````````` TOP-DOWN FLAT-ELECTION ```````````````````````````
90    // ===============================================================================
91
92    plugin_model! {
93        /// The **TopDownFlat** model ranks candidates based solely on their **aggregated
94        /// flat weight**. Each candidate's score represents the total support or stake
95        /// they possess.
96        ///
97        /// **Concept**: **Total-Weight Dominance**
98        ///
99        /// The resulting order is strictly determined by these scalar totals - higher
100        /// weights dominate, ensuring proportional fairness where every unit of influence
101        /// counts equally.
102        ///
103        /// ## Characteristics:
104        /// - **Simple aggregation**: Treats all contributions as additive and equivalent.
105        /// - **Self-inclusive**: Candidate's own weight contributes to their score.
106        /// - **Deterministic ordering**: Sorted descending by total flat weight.
107        /// - **Context-free**: Requires no external context or normalization.
108        ///
109        /// ## Applications:
110        /// - **Stake-based elections**
111        /// - **Token-weighted voting**
112        /// - **Simple ranking systems** where total influence determines outcome
113        ///
114        /// ## Example:
115        /// ```ignore
116        /// let input = vec![
117        ///     ("Alice", 90),
118        ///     ("Bob", 50),
119        ///     ("Charlie", 80),
120        /// ];
121        /// let output = TopDownFlat::compute(input, None);
122        /// assert_eq!(output, vec!["Alice", "Charlie", "Bob"]);
123        /// ```
124        ///
125        /// ## References
126        /// - [Sorting - Wikipedia](https://en.wikipedia.org/wiki/Sorting)
127        name: pub TopDownFlatModel,
128        input: Input,
129        output: Output,
130        others: [Candidate, FlatWeight],
131        bounds: [
132            Input: FromIterator<(Candidate, FlatWeight)>
133                + IntoIterator<Item = (Candidate, FlatWeight)> + Clone,
134            Output: FromIterator<Candidate>,
135            FlatWeight: Ord + Clone,
136            Candidate: Clone,
137        ],
138        /// Computes the **descending rank** of candidates by total flat weight.
139        ///
140        /// 1. Collects all `(Candidate, FlatWeight)` pairs.
141        /// 2. Sorts them in **descending order** of their weight.
142        /// 3. Returns the ordered list of candidates.
143        compute: |input, _context| {
144            // Step 1: Collect input into a mutable vector
145            let mut items: Vec<(Candidate, FlatWeight)> = Vec::new();
146            for pair in input.clone() {
147                items.push(pair);
148            }
149
150            // Step 2: Sort items in descending order by FlatWeight
151            items.sort_by(|a, b| b.1.cmp(&a.1));
152
153            // Step 3: Extract only the candidates in order
154            let mut output: Vec<Candidate> = Vec::new();
155            for (candidate, _) in items {
156                output.push(candidate);
157            }
158
159            // Step 4: Return candidates as iterator
160            output.into_iter().collect()
161        }
162    }
163
164    // ===============================================================================
165    // ``````````````````````````` THRESHOLD FLAT-ELECTION ```````````````````````````
166    // ===============================================================================
167
168    /// Context Config for Plugin Model [`ThresholdFlatModel`]
169    ///
170    /// This struct provides the configuration for threshold-based elections, where
171    /// only candidates whose weight meets or exceeds a specified threshold are
172    /// considered.
173    pub struct ThresholdFlatModelConfig<T> {
174        /// The minimum weight required for a candidate to be included in the output.
175        ///
176        /// - Candidates with a weight **greater than or equal to** this value will be
177        /// retained.
178        /// - Candidates with a weight **below** this value will be filtered out.
179        ///
180        /// ## Example
181        /// ```ignore
182        /// let config = ThresholdModelConfig { threshold: 50 };
183        /// // Only candidates with weight >= 50 will be returned by the model
184        /// ```
185        pub threshold: T,
186    }
187
188    plugin_model! {
189        /// The **ThresholdFlat** model filters candidates based on a **minimum flat
190        /// weight threshold**. Only candidates whose weight meets or exceeds the threshold
191        /// are included in the output.
192        ///
193        /// **Concept**: **Minimum Weight Filtering**
194        ///
195        /// This is useful in elections or ranking systems where very low-contributing
196        /// candidates should be excluded from consideration.
197        ///
198        /// ## Characteristics:
199        /// - **Threshold-based**: Filters out candidates below a specified weight.
200        /// - **Self-inclusive**: Candidate's own weight counts toward the threshold.
201        /// - **Deterministic ordering**: Relative ordering of remaining candidates is
202        /// preserved from the input (or can be further sorted in downstream models).
203        /// - **Context-driven**: Uses an external [`ThresholdModelConfig`] struct to
204        /// provide the threshold.
205        ///
206        /// ## Applications:
207        /// - Removing candidates with insufficient stake or support.
208        /// - Pre-filtering before proportional or ranked elections.
209        /// - Reward distribution where a minimum contribution is required.
210        ///
211        /// ## Example:
212        /// ```ignore
213        /// let input = vec![
214        ///     ("Alice", 90),
215        ///     ("Bob", 50),
216        ///     ("Charlie", 30),
217        /// ];
218        /// let config = ThresholdModelConfig { threshold: 50 };
219        /// let output = ThresholdModel::compute(input, Some(config));
220        /// assert_eq!(output, vec!["Alice", "Bob"]);
221        /// ```
222        ///
223        /// ## References:
224        /// - [Thresholding and Filtering - Wikipedia](https://en.wikipedia.org/wiki/Thresholding)
225        /// - Standard minimum weight / eligibility filtering in proportional systems.
226        name: pub ThresholdFlatModel,
227        input: Input,
228        output: Output,
229        others: [Candidate, FlatWeight],
230        context: ThresholdFlatModelConfig<FlatWeight>,  // external struct
231        bounds: [
232            Input: IntoIterator<Item = (Candidate, FlatWeight)> + Clone,
233            Output: FromIterator<Candidate>,
234            FlatWeight: Ord + Clone,
235            Candidate: Clone,
236        ],
237        /// Filters candidates below a configurable flat weight threshold.
238        ///
239        /// - Iterates over all `(Candidate, FlatWeight)` pairs.
240        /// - Keeps only candidates where weight >= `ctx.threshold`.
241        /// - Returns an iterator over the remaining candidates.
242        compute: |input, ctx| {
243            // Step 1: Collect input into a mutable vector (optional, ensures
244            // we can iterate multiple times if needed)
245            let items: Vec<(Candidate, FlatWeight)> = input.clone().into_iter().collect();
246
247            // Step 2: Prepare an output vector to store candidates that meet the threshold
248            let mut output: Vec<Candidate> = Vec::new();
249
250            // Step 3: Iterate over each candidate and weight
251            for (candidate, weight) in items {
252                // Step 4: Check if the weight meets or exceeds the threshold
253                if weight >= ctx.threshold.clone() {
254                    // Step 5: If yes, add the candidate to the output
255                    output.push(candidate);
256                }
257            }
258
259            // Step 6: Return the output as an iterator (FromIterator)
260            output.into_iter().collect()
261        }
262    }
263}
264
265// ===============================================================================
266// ```````````````````````````` FAIR-ELECTION PLUGINS ````````````````````````````
267// ===============================================================================
268
269pub use fair::*;
270
271/// Contains **FairElection plugin models**, which rank entities using a
272/// **fair weight**, where each entity's weight is derived from a **list of
273/// contributors** and their individual contributions.
274///
275/// In this model:
276/// - Each entity is represented as `(entity, fair_weight)`.
277/// - The `fair_weight` itself is a **collection of `(contributor, contribution)`
278/// pairs**.
279/// - The plugin aggregates or evaluates these individual contributions according
280/// to its algorithm to produce a ranked output.
281///
282/// ## Characteristics
283/// - Requires a **list of entities with their fair weights** as input.
284/// - Each fair weight preserves the **granularity of individual contributions**,
285/// unlike flat weights.
286/// - Produces a **sorted list of entities**, taking into account the distribution and
287/// magnitude of contributions.
288/// - Ideal for scenarios where **fairness or proportional representation** matters,
289/// rather than just total aggregated value.
290///
291/// ## Example Flow
292/// 1. Prepare a list of entities with their fair weights:
293///     `[(entity1, [(c1, v1), (c2, v2)]), ...]`.  
294/// 2. Pass this list to a FairElection plugin model.  
295/// 3. The model computes a ranking based on the **individual contributions within
296/// each fair weight** and outputs entities in order.
297pub mod fair {
298
299    // ===============================================================================
300    // ````````````````````````````````` IMPORTS `````````````````````````````````````
301    // ===============================================================================
302
303    // --- FRAME Suite ---
304    use frame_suite::plugin_model;
305
306    // --- Substrate primitives ---
307    use sp_runtime::{
308        traits::{Saturating, Zero},
309        Vec,
310    };
311
312    // --- Substrate std (no_std helpers) ---
313    use sp_std::ops::{Add, Div, Mul};
314
315    // ===============================================================================
316    // ``````````````````````````` TOP-DOWN FAIR-ELECTION ````````````````````````````
317    // ===============================================================================
318
319    plugin_model! {
320        /// The **TopDownFair** model evaluates candidates based on the **aggregate
321        /// support** they receive from **external backers**.
322        ///
323        /// **Concept**: **Aggregated External Fairness**
324        ///
325        /// Each candidate has a list of `(Backer, Backed)` pairs representing
326        /// individual endorsements. The model sums these contributions using
327        /// **saturating addition** to prevent overflow and sorts candidates
328        /// in **descending order** by their total received backing.
329        ///
330        /// ## Characteristics:
331        /// - **Fair aggregation**: Each candidate's score is the sum of their
332        /// backers' support.
333        /// - **No self-weighting**: Excludes self-stake to prevent dominance
334        /// by self-funding.
335        /// - **Overflow-safe**: Uses `Saturating` arithmetic to avoid overflow
336        /// errors.
337        /// - **Context-free**: Deterministic and stateless ranking.
338        ///
339        /// ## Applications:
340        /// - **Governance elections** prioritizing external community support
341        /// - **Fair staking models** with anti-self-dealing constraints
342        /// - **Collaborative credit or delegation-based systems**
343        ///
344        /// ## Example:
345        /// ```ignore
346        /// let input = vec![
347        ///     ("Alice", vec![("Bob", 10), ("Carol", 20)]),
348        ///     ("Dave", vec![("Eve", 15), ("Frank", 15)]),
349        ///     ("Grace", vec![("Heidi", 5), ("Ivan", 10)]),
350        /// ];
351        /// let output = TopDownFair::compute(input, None);
352        /// // Aggregated totals:
353        /// // Alice: 30, Dave: 30, Grace: 15
354        /// assert_eq!(output, vec!["Alice", "Dave", "Grace"]);
355        /// ```
356        ///
357        /// ## Reference:
358        /// - [Weighted voting - Wikipedia](https://en.wikipedia.org/wiki/Weighted_voting)
359        /// - [Delegated voting / Liquid democracy](https://en.wikipedia.org/wiki/Liquid_democracy)
360        /// - Osborne, M. J., *An Introduction to Game Theory*, Oxford University Press, 2004 (proportional influence)
361        name: pub TopDownFairModel,
362        input: Input,
363        output: Output,
364        others: [Candidate, FairWeight, Backer, Backed],
365        bounds: [
366            Input: FromIterator<(Candidate, FairWeight)>
367                + IntoIterator<Item = (Candidate, FairWeight)> + Clone,
368            Output: FromIterator<Candidate>,
369            FairWeight: FromIterator<(Backer, Backed)>
370                + IntoIterator<Item = (Backer, Backed)> + Clone,
371            Backed: Clone + Ord + Zero + Saturating,
372            Candidate: Clone,
373        ],
374        /// Computes the **fair aggregated rank** of candidates imperatively.
375        ///
376        /// - Iterates over each candidate and sums their backers' contributions
377        /// using saturating addition.
378        /// - Sorts candidates by descending total backing.
379        /// - Returns only the ordered list of candidates.
380        compute: |input, _context| {
381            // Step 1: Collect input into a vector for processing
382            let items_input: Vec<(Candidate, FairWeight)> = input.clone().into_iter().collect();
383
384            // Step 2: Prepare vector to store total backing for each candidate
385            let mut items: Vec<(Candidate, Backed)> = Vec::new();
386
387            // Step 3: Sum external backing for each candidate
388            for (candidate, fairweight) in items_input {
389                let mut total = Backed::zero();
390                for (_, backed) in fairweight {
391                    total = total.saturating_add(backed);
392                }
393                items.push((candidate, total));
394            }
395
396            // Step 4: Sort candidates by descending total backing
397            items.sort_by(|a, b| b.1.cmp(&a.1));
398
399            // Step 5: Collect only the candidates in order
400            let mut output: Vec<Candidate> = Vec::new();
401            for (candidate, _) in items {
402                output.push(candidate);
403            }
404
405            output.into_iter().collect()
406        }
407    }
408
409    // ===============================================================================
410    // ``````````````````````````` BALANCED FAIR-ELECTION ````````````````````````````
411    // ===============================================================================
412
413    plugin_model! {
414        /// The **BalancedFair** model evaluates candidates by combining both the
415        /// **total backing** and the **average backing per backer**. This approach
416        /// balances candidates who have a few very large backers versus those with
417        /// many smaller backers.
418        ///
419        /// **Concept**: **Total + Average External Fairness**
420        ///
421        /// Each candidate has a list of `(Backer, Backed)` pairs.
422        /// The model computes:
423        /// 1. `total = sum of all Backed values`
424        /// 2. `average = total / number of backers`
425        /// 3. `score = total + average`
426        ///
427        /// Candidates are then sorted in descending order by this combined score.
428        ///
429        /// ## Characteristics:
430        /// - **Balanced aggregation**: Rewards both high total support and broad
431        ///   distribution.
432        /// - **No self-weighting**: Excludes candidate self-contributions.
433        /// - **Overflow-safe**: Uses saturating arithmetic.
434        /// - **Deterministic ordering**: Sorted descending by combined score.
435        /// - **Context-free**: Stateless and reproducible ranking.
436        ///
437        /// ## Applications:
438        /// - Governance systems balancing "big backers" vs "broad support".
439        /// - Collaborative projects or token-based delegation systems.
440        /// - Reward allocation where both total and average contributions matter.
441        ///
442        /// ## References:
443        /// - [Weighted voting - Wikipedia](https://en.wikipedia.org/wiki/Weighted_voting)
444        /// - [Delegated voting / Liquid democracy](https://en.wikipedia.org/wiki/Liquid_democracy)
445        /// - Osborne, M. J., *An Introduction to Game Theory*, Oxford University Press, 2004
446        name: pub BalancedModel,
447        input: Input,
448        output: Output,
449        others: [Candidate, FairWeight, Backer, Backed],
450        bounds: [
451            Input: IntoIterator<Item = (Candidate, FairWeight)> + Clone,
452            Output: FromIterator<Candidate>,
453            FairWeight: IntoIterator<Item = (Backer, Backed)> + Clone,
454            Backed: Clone + Ord + Zero + Saturating + Add<Output = Backed> + Div<usize, Output = Backed>,
455            Candidate: Clone,
456        ],
457        /// Computes the balanced fair rank of candidates imperatively.
458        ///
459        /// Steps:
460        /// 1. Iterate over each candidate and collect all `(Backer, Backed)` pairs.
461        /// 2. Compute total backing as the saturating sum of `Backed`.
462        /// 3. Compute average backing per backer.
463        /// 4. Compute score = total + average.
464        /// 5. Sort candidates by descending score.
465        /// 6. Return only the ordered candidates.
466        compute: |input, _context| {
467            // Step 1: Collect input into a vector for processing
468            let items_input: Vec<(Candidate, FairWeight)> = input.clone().into_iter().collect();
469
470            // Step 2: Prepare vector to store scores for each candidate
471            let mut scores: Vec<(Candidate, Backed)> = Vec::new();
472
473            // Step 3: Compute total, average, and combined score for each candidate
474            for (candidate, fairweight) in items_input {
475                let entries: Vec<(Backer, Backed)> = fairweight.into_iter().collect();
476                // avoid division by zero
477                let count = if entries.is_empty() { 1 } else { entries.len() };
478
479                let mut total = Backed::zero();
480                for (_, value) in &entries {
481                    total = total.saturating_add(value.clone());
482                }
483
484                let average = total.clone() / count;
485                let score = total.saturating_add(average);
486
487                scores.push((candidate, score));
488            }
489
490            // Step 4: Sort candidates by descending score
491            scores.sort_by(|a, b| b.1.cmp(&a.1));
492
493            // Step 5: Collect only candidates in order
494            let mut output: Vec<Candidate> = Vec::new();
495            for (candidate, _) in scores {
496                output.push(candidate);
497            }
498
499            output.into_iter().collect()
500        }
501    }
502
503    // ===============================================================================
504    // ``````````````````````````` PHRAGMEN FAIR-ELECTION ````````````````````````````
505    // ===============================================================================
506
507    /// Configuration for the [`PhragmenModel`] plugin model
508    ///
509    /// This struct allows fine-tuning of the Phragmen election algorithm
510    /// to produce different variants of fair elections, including weighted,
511    /// sequential, and scaled variants.
512    ///
513    /// Each field modifies how candidates are evaluated and how backer loads
514    /// are computed.
515    ///
516    /// ## References:
517    /// - [Phragmen voting - Wikipedia](https://en.wikipedia.org/wiki/Phragmen%27s_voting_rules)
518    /// - Enestrom, E. "Mathematical Theory of Proportional Representation", 1896
519    /// - Thiele, T. N., "Proportional Representation in Elections", 1895
520    pub struct PhragmenModelConfig<Backed> {
521        /// `weighted`: If true, the algorithm **scales each backer's contribution** by their
522        /// influence or stake.
523        ///
524        /// - **Behavior when true:** Candidates supported by high-influence backers contribute more
525        ///   to the backers' load. The algorithm prioritizes minimizing load while respecting
526        ///   weighted influence, meaning candidates with concentrated high-weight backers may be
527        ///   elected later to balance maximum loads.
528        /// - **Behavior when false:** All backers' contributions are treated equally. Only the sum
529        ///   of contributions matters, ignoring stake or influence differences.
530        pub weighted: bool,
531
532        /// `scale`: Optional multiplier applied to backer contributions when `weighted` is true.
533        ///
534        /// - **Behavior when Some(value):** Each backer's contribution is multiplied by `scale`.
535        ///   This allows amplifying or reducing the effective weight of all backers, controlling
536        ///   how heavily influence/stake impacts candidate ranking.
537        /// - **Behavior when None:** Contributions are used as-is, without additional scaling.
538        pub scale: Option<Backed>,
539    }
540
541    plugin_model! {
542        /// The **Phragmen** model ranks candidates based on fair distribution of
543        /// voter/backer load. Each candidate has `(Backer, Backed)` pairs representing
544        /// support, and the algorithm selects candidates sequentially to **minimize
545        /// the maximum load** among all backers.
546        ///
547        /// **Concept**: **Proportional Load Balancing**
548        ///
549        /// This plugin supports multiple Phragmen variants through the plugin
550        /// context [`PhragmenModelConfig`]:
551        /// - Weighted vs unweighted contributions
552        /// - Deterministic tie-breaking
553        /// - Optional scaling of backer contributions
554        ///
555        /// ## Applications:
556        /// - Governance elections with proportional representation
557        /// - Token or stake weighted systems
558        /// - Delegation or liquid democracy systems
559        ///
560        /// ## References:
561        /// - [Phragmen voting - Wikipedia](https://en.wikipedia.org/wiki/Phragmen%27s_voting_rules)
562        /// - Enestrom, E. "Mathematical Theory of Proportional Representation", 1896
563        /// - Thiele, T. N., "Proportional Representation in Elections", 1895
564        name: pub PhragmenModel,
565        input: Input,
566        output: Output,
567        others: [Candidate, FairWeight, Backer, Backed],
568        context: PhragmenModelConfig<Backed>,
569        bounds: [
570            Input: IntoIterator<Item = (Candidate, FairWeight)> + Clone,
571            Output: FromIterator<Candidate>,
572            FairWeight: IntoIterator<Item = (Backer, Backed)> + Clone,
573            Backed: Clone + Zero + Add<Output = Backed> + Ord + Div<usize, Output = Backed> + Mul<Backed, Output = Backed>,
574            Candidate: Clone + Eq,
575            Backer: Clone + Eq + sp_std::hash::Hash + Ord,
576        ],
577
578        /// Computes the **Phragmen ranking** of candidates imperatively.
579        ///
580        /// Steps:
581        /// 1. Initialize all backer loads to zero.
582        /// 2. While candidates remain:
583        ///    - Compute maximum load increase for each candidate if elected.
584        ///    - Apply weighting/scaling if configured.
585        ///    - Deterministically select candidate with minimal max load (tie = first in list).
586        ///    - Update ranking and backer loads.
587        /// 3. Return all candidates in ranked order.
588        compute: |input, ctx| {
589            use sp_std::collections::btree_map::BTreeMap;
590            use sp_std::vec::Vec;
591
592            // Step 1: collect candidates and initialize ranking
593            let mut remaining: Vec<(Candidate, FairWeight)> = input.clone().into_iter().collect();
594            let mut ranking: Vec<Candidate> = Vec::new();
595
596            // Step 1b: initialize all backer loads to zero
597            let mut load: BTreeMap<Backer, Backed> = BTreeMap::new();
598            for (_, fairweight) in &remaining {
599                for (backer, _) in fairweight.clone().into_iter() {
600                    load.entry(backer).or_insert(Backed::zero());
601                }
602            }
603
604            // Step 2: select candidates until all are ranked
605            while !remaining.is_empty() {
606                let mut best_candidate = None;
607                let mut best_max_load = None;
608
609                for (candidate, fairweight) in &remaining {
610                    // Compute maximum load this candidate would impose on their backers
611                    let mut max_load = Backed::zero();
612
613                    for (backer, backed) in fairweight.clone().into_iter() {
614                        let mut contribution = backed;
615
616                        // Apply weighting if configured
617                        if ctx.weighted {
618                            if let Some(scale) = &ctx.scale {
619                                contribution = contribution * scale.clone();
620                            }
621                        }
622
623                        let new_load = load.get(&backer).unwrap().clone() + contribution;
624                        if new_load > max_load {
625                            max_load = new_load;
626                        }
627                    }
628
629                    // Determine if this candidate is currently the best choice
630                    if best_max_load.is_none() || max_load < best_max_load.clone().unwrap() {
631                        best_candidate = Some(candidate.clone());
632                        best_max_load = Some(max_load);
633                    }
634                    // Tie is resolved deterministically by keeping first found
635                }
636
637                // Remove chosen candidate from remaining and add to ranking
638                let chosen_index = remaining
639                    .iter()
640                    .position(|(c, _)| *c == best_candidate.clone().unwrap())
641                    .unwrap();
642                let (_candidate, fairweight) = remaining.remove(chosen_index);
643                ranking.push(best_candidate.unwrap());
644
645                // Step 2c: update backer loads
646                for (backer, backed) in fairweight.into_iter() {
647                    let mut contribution = backed;
648                    if ctx.weighted {
649                        if let Some(scale) = &ctx.scale {
650                            contribution = contribution * scale.clone();
651                        }
652                    }
653                    let entry = load.entry(backer).or_insert(Backed::zero());
654                    *entry = entry.clone() + contribution;
655                }
656            }
657
658            // Step 3: return ranked candidates
659            ranking.into_iter().collect()
660        }
661    }
662
663    // ===============================================================================
664    // ````````````````````````` MAX-MIN LOAD FAIR-ELECTION ``````````````````````````
665    // ===============================================================================
666
667    plugin_model! {
668        /// The **Max-Min Load Fair** model ranks candidates to achieve a **fair
669        /// distribution of influence among all backers**. It selects candidates
670        /// sequentially to **minimize the maximum load any backer bears**, ensuring
671        /// no single backer is disproportionately overrepresented.
672        ///
673        /// **Concept**: **Load-Balanced Candidate Selection**
674        ///
675        /// Each candidate is associated with a list of `(Backer, Backed)` pairs
676        /// representing the support they receive. The algorithm repeatedly:
677        /// 1. Computes, for each remaining candidate, the maximum load their
678        /// selection would impose on their backers.
679        /// 2. Selects the candidate whose election results in the **smallest
680        /// maximum load increase**.
681        /// 3. Updates backer loads accordingly.
682        ///
683        /// ## Characteristics:
684        /// - **Fairness-focused**: Prioritizes balancing backer loads rather
685        /// than simply maximizing totals.
686        /// - **Deterministic**: Tie-breaking is deterministic (first candidate
687        /// in iteration).
688        /// - **Context-free**: Does not require any external configuration or
689        /// parameters.
690        ///
691        /// ## Applications:
692        /// - Governance or board elections where proportional influence is desired.
693        /// - Delegation-based or collaborative voting systems.
694        /// - Any scenario where minimizing backer overload improves fairness.
695        name: pub MaxMinLoadModel,
696        input: Input,
697        output: Output,
698        others: [Candidate, Backer, Backed, FairWeight],
699        bounds: [
700            Input: IntoIterator<Item = (Candidate, FairWeight)> + Clone,
701            Output: FromIterator<Candidate>,
702            FairWeight: IntoIterator<Item = (Backer, Backed)> + Clone,
703            Backed: Clone + Zero + Ord + Add<Output = Backed>,
704            Candidate: Clone + Eq,
705            Backer: Clone + Eq + sp_std::hash::Hash + Ord,
706        ],
707        /// Computes a fair ranking of candidates by minimizing maximum load
708        /// per backer.
709        ///
710        /// Steps :
711        /// 1. **Collect input** into a vector for sequential processing.
712        /// 2. **Initialize backer loads** to zero.
713        /// 3. While there are unranked candidates:
714        ///    a. For each candidate, compute the maximum load increase across
715        ///       their backers.
716        ///    b. Select the candidate with the smallest maximum load.
717        ///    c. Remove the selected candidate from remaining candidates and
718        ///       append to ranking.
719        ///    d. Update backer loads by adding this candidate's contributions.
720        /// 4. Return the ranked candidates as an iterator.
721        compute: |input, _ctx| {
722            use sp_std::collections::btree_map::BTreeMap;
723            use sp_std::vec::Vec;
724
725            // Step 1: Collect candidates and their backing
726            let mut remaining: Vec<(Candidate, FairWeight)> = input.clone().into_iter().collect();
727            let mut ranking: Vec<Candidate> = Vec::new();
728
729            // Step 2: Initialize backer loads to zero
730            let mut load: BTreeMap<Backer, Backed> = BTreeMap::new();
731            for (_, fairweight) in &remaining {
732                for (backer, _) in fairweight.clone().into_iter() {
733                    load.entry(backer).or_insert(Backed::zero());
734                }
735            }
736
737            // Step 3: Iteratively select candidates
738            while !remaining.is_empty() {
739                let mut best_candidate = None;
740                let mut min_max_load = None;
741
742                // Evaluate each remaining candidate
743                for (candidate, fairweight) in &remaining {
744                    let mut max_load = Backed::zero();
745
746                    for (backer, backed) in fairweight.clone().into_iter() {
747                        let new_load = load.get(&backer).unwrap().clone() + backed;
748                        if new_load > max_load {
749                            max_load = new_load;
750                        }
751                    }
752
753                    // Candidate minimizes maximum load
754                    if min_max_load.is_none() || max_load < min_max_load.clone().unwrap() {
755                        best_candidate = Some(candidate.clone());
756                        min_max_load = Some(max_load);
757                    }
758                }
759
760                // Remove selected candidate from remaining
761                let idx = remaining
762                    .iter()
763                    .position(|(c, _)| *c == best_candidate.clone().unwrap())
764                    .unwrap();
765                let (_c, fairweight) = remaining.remove(idx);
766                ranking.push(best_candidate.unwrap());
767
768                // Update backer loads for selected candidate
769                for (backer, backed) in fairweight.into_iter() {
770                    let entry = load.entry(backer).or_insert(Backed::zero());
771                    *entry = entry.clone() + backed;
772                }
773            }
774
775            // Step 4: Return final ranking
776            ranking.into_iter().collect()
777        }
778    }
779
780    // ===============================================================================
781    // ``````````````````````````` THRESHOLD FAIR-ELECTION ```````````````````````````
782    // ===============================================================================
783
784    /// Defines the configuration for [`ThresholdFairModel`] election plugin.
785    ///
786    /// Candidates are filtered based on a minimum total backing before being
787    /// considered for ranking or selection.
788    pub struct ThresholdFairModelConfig<T> {
789        /// Minimum total backing required for a candidate to be considered.
790        /// - `threshold`: The minimum backing value a candidate must have
791        /// to be eligible.
792        /// - Candidates with total backing **less than `threshold` are
793        /// excluded** from the election.
794        /// - Useful for preventing candidates with negligible support
795        /// from affecting the outcome.
796        pub threshold: T,
797    }
798
799    plugin_model! {
800        /// Filters candidates whose **total backing** is below a configurable
801        /// threshold.
802        ///
803        /// Each candidate is represented as `(Candidate, FairWeight)` where
804        /// `FairWeight` is a list of `(Backer, Backed)` pairs. The algorithm
805        /// sums all backings for a candidate and includes only those whose
806        /// total meets or exceeds `ctx.threshold`.
807        ///
808        /// ## Characteristics:
809        /// - Context-driven via [`ThresholdModelConfig`].
810        /// - Deterministic ordering of candidates passing the threshold.
811        /// - Prevents candidates with insufficient support from participating.
812        ///
813        /// ## Use cases:
814        /// - Governance systems requiring minimum backing for eligibility.
815        /// - Stake-weighted elections where very low-supported candidates
816        /// should be ignored.
817        name: pub ThresholdFairModel,
818        input: Input,
819        output: Output,
820        others: [Candidate, Backer, Backed, FairWeight],
821        context: ThresholdFairModelConfig<Backed>,
822        bounds: [
823            Input: IntoIterator<Item = (Candidate, FairWeight)> + Clone,
824            Output: FromIterator<Candidate>,
825            FairWeight: IntoIterator<Item = (Backer, Backed)> + Clone,
826            Backed: Clone + Zero + Add<Output = Backed> + Ord,
827            Candidate: Clone,
828            Backer: Clone,
829        ],
830
831        /// Computes the filtered candidates imperatively.
832        ///
833        /// Steps:
834        /// 1. Initialize an empty `output` vector.
835        /// 2. Iterate over all candidates and their fair weights.
836        /// 3. Sum the `Backed` values for each candidate to get `total`.
837        /// 4. Include candidate in `output` if `total >= ctx.threshold`.
838        /// 5. Return an iterator over the filtered candidates.
839        compute: |input, ctx| {
840            // Step 1: Prepare output vector
841            let mut output = Vec::new();
842
843            // Step 2: Iterate over candidates
844            for (candidate, fairweight) in input.clone() {
845                // Step 3: Compute total backing
846                let total: Backed = fairweight
847                    .into_iter()
848                    .map(|(_, b)| b)
849                    .fold(Backed::zero(), |acc, x| acc + x);
850
851                // Step 4: Include only candidates meeting threshold
852                if total >= ctx.threshold {
853                    output.push(candidate);
854                }
855            }
856
857            // Step 5: Return as iterator
858            output.into_iter().collect()
859        }
860    }
861}
862
863// ===============================================================================
864// ```````````````````````` ELECTION MODELS PLUGIN TESTS `````````````````````````
865// ===============================================================================
866
867#[cfg(test)]
868mod tests {
869
870    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
871    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
872    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
873
874    // --- Local crate imports ---
875    use crate::elections::{
876        fair,
877        flat::{self},
878    };
879
880    // --- FRAME Suite ---
881    use frame_suite::plugin_test;
882
883    // --- Substrate primitives ---
884    use sp_runtime::AccountId32;
885
886    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
887    // `````````````````````````````````` CONSTANTS ``````````````````````````````````
888    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
889
890    const fn account_frm_seed(seed: u8) -> AccountId32 {
891        let mut data = [0u8; 32];
892        data[31] = seed;
893        AccountId32::new(data)
894    }
895
896    const ALICE: AccountId32 = account_frm_seed(1);
897    const BOB: AccountId32 = account_frm_seed(2);
898    const CHARLIE: AccountId32 = account_frm_seed(3);
899    const ALAN: AccountId32 = account_frm_seed(4);
900    const MIKE: AccountId32 = account_frm_seed(5);
901    const CAROL: AccountId32 = account_frm_seed(6);
902    const DAVE: AccountId32 = account_frm_seed(7);
903    const FRANK: AccountId32 = account_frm_seed(8);
904    const GRACE: AccountId32 = account_frm_seed(9);
905    const IVAN: AccountId32 = account_frm_seed(10);
906    const EVE: AccountId32 = account_frm_seed(11);
907    const WADE: AccountId32 = account_frm_seed(12);
908    const NIX: AccountId32 = account_frm_seed(13);
909    const LAYA: AccountId32 = account_frm_seed(14);
910
911    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
912    // ````````````````````````````` FLAT-ELECTION MODELS ````````````````````````````
913    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
914       
915    // ----------------------------------- TOP-DOWN ----------------------------------
916
917    plugin_test! {
918        model: flat::TopDownFlatModel,
919        input: Vec<(AccountId32, u64)>,
920        output: Vec<AccountId32>,
921        cases : {
922            (top_down_model_basic_flat_election,
923            vec![(ALICE, 180), (BOB, 170), (CHARLIE, 200), (ALAN, 210), (MIKE, 90)],
924            vec![ALAN, CHARLIE, ALICE, BOB, MIKE]
925            ),
926            (top_down_model_equal_weights,
927            // All candidates have equal weight - order preserved from input
928            vec![(ALICE, 100), (BOB, 100), (CHARLIE, 100), (ALAN, 100)],
929            vec![ALICE, BOB, CHARLIE, ALAN]
930            ),
931            (top_down_model_multiple_ties,
932            // Multiple groups of tied weights
933            vec![(ALICE, 200), (BOB, 100), (CHARLIE, 200), (ALAN, 100), (MIKE, 300)],
934            vec![MIKE, ALICE, CHARLIE, BOB, ALAN]
935            ),
936        }
937    }
938
939    // ---------------------------------- THRESHOLD ----------------------------------
940
941    plugin_test! {
942        model: flat::ThresholdFlatModel,
943        input: Vec<(AccountId32, u64)>,
944        output: Vec<AccountId32>,
945        context: flat::ThresholdFlatModelConfig<u64>,
946        value: flat::ThresholdFlatModelConfig {
947            threshold: 200
948        },
949        cases : {
950            (threshold_model_basic_flat_election,
951            vec![(ALICE, 180), (BOB, 230), (CHARLIE, 200), (ALAN, 270), (MIKE, 90)],
952            vec![BOB, CHARLIE, ALAN]
953            ),
954            (threshold_model_exact_threshold,
955            // Candidate exactly at threshold should be included
956            vec![(ALICE, 200), (BOB, 200), (CHARLIE, 170)],
957            vec![ALICE, BOB]
958            ),
959            (threshold_model_preserves_order,
960            // Verify input order is preserved for qualifying candidates
961            vec![(MIKE, 250), (ALICE, 300), (BOB, 280), (CHARLIE, 190), (ALAN, 260)],
962            vec![MIKE, ALICE, BOB, ALAN]
963            ),
964        }
965    }
966
967    plugin_test! {
968        model: flat::ThresholdFlatModel,
969        input: Vec<(AccountId32, u64)>,
970        output: Vec<AccountId32>,
971        context: flat::ThresholdFlatModelConfig<u64>,
972        value: flat::ThresholdFlatModelConfig {
973            threshold: 1000
974        },
975        cases : {
976            (threshold_model_high_threshold,
977            // high threshold, only top candidates qualify
978            vec![(ALICE, 500), (BOB, 1200), (CHARLIE, 900), (ALAN, 1500)],
979            vec![BOB, ALAN]
980            ),
981            (threshold_model_none_qualify_high_threshold,
982            // no candidates qualify
983            vec![(ALICE, 500), (BOB, 800), (CHARLIE, 900)],
984            vec![]
985            )
986        }
987    }
988
989    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
990    // ````````````````````````````` FAIR-ELECTION MODELS ````````````````````````````
991    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
992
993    // ----------------------------------- TOP-DOWN ----------------------------------
994
995    plugin_test! {
996        model: fair::TopDownFairModel,
997        input: Vec<(AccountId32, Vec<(AccountId32, u64)>)>,
998        output: Vec<AccountId32>,
999        cases : {
1000            (top_down_model_basic_fair_election,
1001            // ALICE : 110, BOB : 170, CHARLIE: 180, ALAN: 60
1002            vec![(ALICE, vec![(CAROL, 50), (DAVE, 60)]), (BOB, vec![(FRANK, 50), (GRACE, 120)]), (CHARLIE, vec![(MIKE, 80), (IVAN, 100)]), (ALAN, vec![(EVE, 60)])],
1003            vec![CHARLIE, BOB, ALICE, ALAN]
1004            ),
1005            (top_down_model_equal_total_backing,
1006            // All candidates have equal total backing - preserves input order
1007            // ALICE: 100, BOB: 100, CHARLIE: 100
1008            vec![
1009                (ALICE, vec![(CAROL, 50), (DAVE, 50)]),
1010                (BOB, vec![(FRANK, 60), (GRACE, 40)]),
1011                (CHARLIE, vec![(MIKE, 30), (IVAN, 70)])
1012            ],
1013            vec![ALICE, BOB, CHARLIE]
1014            ),
1015            (top_down_model_no_backers,
1016            // candidates with no backers (total = 0)
1017            vec![
1018                (ALICE, vec![]),
1019                (BOB, vec![]),
1020                (CHARLIE, vec![])
1021            ],
1022            vec![ALICE, BOB, CHARLIE]
1023            ),
1024            (top_down_model_mixed_fair_election,
1025            vec![
1026                (ALICE, vec![(IVAN, 200)]), (NIX, vec![(FRANK, 300)]), (MIKE, vec![(GRACE, 200)]), (BOB, vec![]), (CHARLIE, vec![]),
1027                (DAVE, vec![]), (LAYA, vec![]), (ALAN, vec![(CAROL, 150)])
1028            ],
1029            vec![NIX, ALICE, MIKE, ALAN, BOB, CHARLIE, DAVE, LAYA]
1030            )
1031        }
1032    }
1033
1034    // ----------------------------------- BALANCED ----------------------------------
1035
1036    plugin_test! {
1037        model: fair::BalancedModel,
1038        input: Vec<(AccountId32, Vec<(AccountId32, usize)>)>,
1039        output: Vec<AccountId32>,
1040        cases : {
1041            (balanced_model_basic_fair_election,
1042            // ALICE : total=120 (3 backer), avg=40, score=140
1043            // BOB : total=170 (2 backer), avg=85, score=255
1044            // CHARLIE: total=180 (2 backer), avg=90, score=270
1045            // ALAN: total=60 (1 backer), avg=60, score=120
1046            vec![(ALICE, vec![(CAROL, 50), (DAVE, 50), (EVE, 20)]), (BOB, vec![(FRANK, 50), (GRACE, 120)]), (CHARLIE, vec![(MIKE, 80), (IVAN, 100)]), (ALAN, vec![(EVE, 60)])],
1047            vec![CHARLIE, BOB, ALICE, ALAN]
1048            ),
1049            (balanced_model_single_backer_advantage,
1050            // Single large backer vs many small backers
1051            // ALICE: total=100 (1 backer), avg=100, score=200
1052            // BOB: total=100 (5 backers), avg=20, score=120
1053            // ALICE wins due to higher average
1054            vec![
1055                (ALICE, vec![(CAROL, 100)]),
1056                (BOB, vec![(DAVE, 20), (FRANK, 20), (GRACE, 20), (MIKE, 20), (IVAN, 20)])
1057            ],
1058            vec![ALICE, BOB]
1059            ),
1060            (balanced_model_many_small_backers,
1061            // Candidate with many small backers vs few large backers
1062            // ALICE: total=500 (10 backers), avg=50, score=550
1063            // BOB: total=500 (2 backers), avg=250, score=750
1064            // BOB wins due to higher average
1065            vec![
1066                (ALICE, vec![
1067                    (CAROL, 50), (DAVE, 50), (FRANK, 50), (GRACE, 50), (MIKE, 50),
1068                    (IVAN, 50), (EVE, 50), (WADE, 50), (ALAN, 50), (CHARLIE, 50)
1069                ]),
1070                (BOB, vec![(CAROL, 250), (DAVE, 250)])
1071            ],
1072            vec![BOB, ALICE]
1073            ),
1074            (balanced_model_equal_combined_score,
1075            // Equal combined scores - preserves input order
1076            // ALICE: total=100, avg=50, score=150
1077            // BOB: total=120, avg=30, score=150
1078            // CHARLIE: total=90, avg=60, score=150
1079            vec![
1080                (ALICE, vec![(CAROL, 50), (DAVE, 50)]),
1081                (BOB, vec![(FRANK, 30), (GRACE, 30), (MIKE, 30), (IVAN, 30)]),
1082                (CHARLIE, vec![(EVE, 30), (WADE, 60)])
1083            ],
1084            vec![ALICE, BOB, CHARLIE]
1085            ),
1086            (balanced_model_no_backers,
1087            // Edge case: candidates with no backers
1088            // score = 0 + 0 = 0 for all
1089            vec![
1090                (ALICE, vec![]),
1091                (BOB, vec![]),
1092                (CHARLIE, vec![])
1093            ],
1094            vec![ALICE, BOB, CHARLIE]
1095            ),
1096        }
1097    }
1098
1099    // ----------------------------------- PHRAGMEN ----------------------------------
1100
1101    plugin_test! {
1102        model: fair::PhragmenModel,
1103        input: Vec<(AccountId32, Vec<(AccountId32, usize)>)>,
1104        output: Vec<AccountId32>,
1105        context: fair::PhragmenModelConfig<usize>,
1106        value: fair::PhragmenModelConfig {
1107            weighted: false,
1108            scale: None
1109        },
1110        cases: {
1111            (phragmen_sequential_true_fair_election,
1112            // ALICE: 110 (CAROL:50, DAVE:60) - max_load=60
1113            // BOB: 170 (FRANK:50, GRACE:120) - max_load=120
1114            // CHARLIE: 180 (MIKE:80, IVAN:100) - max_load=100
1115            // ALAN: 60 (EVE:60) - max_load=60
1116            // Round 1: ALICE wins (60, first encountered)
1117            // Round 2: ALAN wins (60, loads updated: CAROL=50, DAVE=60)
1118            // Round 3: CHARLIE wins (100 < 120)
1119            // Round 4: BOB remains
1120            vec![
1121                (ALICE, vec![(CAROL, 50), (DAVE, 60)]),
1122                (BOB, vec![(FRANK, 50), (GRACE, 120)]),
1123                (CHARLIE, vec![(MIKE, 80), (IVAN, 100)]),
1124                (ALAN, vec![(EVE, 60)])
1125            ],
1126            vec![ALICE, ALAN, CHARLIE, BOB]
1127            ),
1128            (phragmen_whale_monopoly_prevention,
1129            // Real-world: Company board election with 3 seats
1130            // BigCorp (10,000 shares) tries to control all 3 seats
1131            // Small shareholders (100 shares each) band together for 1 candidate
1132            //
1133            // Candidates:
1134            // - ALICE, BOB, CHARLIE: backed by BigCorp (10,000 each)
1135            // - DAVE: backed by 10 small shareholders (100 each, total 1,000)
1136            //
1137            // TopDown would give: [ALICE, BOB, CHARLIE] - BigCorp controls 100%
1138            // Phragmen gives: [DAVE, ALICE, BOB] - Small shareholders get 33% representation
1139            //
1140            // Round 1: DAVE wins (max_load = 100 vs 10,000)
1141            // Round 2: ALICE wins (BigCorp load still 0, max_load = 10,000)
1142            // Round 3: BOB wins (BigCorp now at 10,000, max_load = 20,000)
1143            vec![
1144                (ALICE, vec![(WADE, 10000)]),
1145                (BOB, vec![(WADE, 10000)]),
1146                (CHARLIE, vec![(WADE, 10000)]),
1147                (DAVE, vec![
1148                    (CAROL, 100), (DAVE, 100), (FRANK, 100), (GRACE, 100),
1149                    (IVAN, 100), (EVE, 100), (MIKE, 100), (ALAN, 100)
1150                ])
1151            ],
1152            vec![DAVE, ALICE, BOB, CHARLIE]
1153            ),
1154            (phragmen_single_backer_multiple_candidates,
1155            // CAROL backs all three candidates
1156            // ALICE: CAROL:100 - max_load=100
1157            // BOB: CAROL:50 - max_load=50
1158            // CHARLIE: CAROL:75 - max_load=75
1159            // Round 1: BOB wins (50)
1160            // Round 2: CHARLIE wins (50+75=125 vs 50+100=150)
1161            // Round 3: ALICE (150)
1162            vec![
1163                (ALICE, vec![(CAROL, 100)]),
1164                (BOB, vec![(CAROL, 50)]),
1165                (CHARLIE, vec![(CAROL, 75)])
1166            ],
1167            vec![BOB, CHARLIE, ALICE]
1168            ),
1169            (phragmen_equal_contributions_fair_election,
1170            // All candidates have same total backing and max_load
1171            // Tie-breaking by input order
1172            vec![
1173                (ALICE, vec![(CAROL, 50), (DAVE, 50)]),
1174                (BOB, vec![(FRANK, 50), (GRACE, 50)]),
1175                (CHARLIE, vec![(MIKE, 50), (IVAN, 50)])
1176            ],
1177            vec![ALICE, BOB, CHARLIE]
1178            ),
1179            (phragmen_no_backers_fair_election,
1180            // All candidates have zero backing
1181            vec![
1182                (ALICE, vec![]),
1183                (BOB, vec![]),
1184                (CHARLIE, vec![])
1185            ],
1186            vec![ALICE, BOB, CHARLIE]
1187            ),
1188            (phragmen_overlapping_backers_fair_election,
1189            // CAROL backs ALICE(60) and BOB(40)
1190            // DAVE backs only ALICE(80)
1191            // ALICE: max(CAROL:60, DAVE:80) = 80
1192            // BOB: CAROL:40 = 40
1193            // Round 1: BOB wins (40)
1194            // Round 2: ALICE (CAROL now at 40, so max(40+60=100, 0+80=80) = 100)
1195            vec![
1196                (ALICE, vec![(CAROL, 60), (DAVE, 80)]),
1197                (BOB, vec![(CAROL, 40)])
1198            ],
1199            vec![BOB, ALICE]
1200            ),
1201        }
1202    }
1203
1204    plugin_test! {
1205        model: fair::PhragmenModel,
1206        input: Vec<(AccountId32, Vec<(AccountId32, usize)>)>,
1207        output: Vec<AccountId32>,
1208        context: fair::PhragmenModelConfig<usize>,
1209        value: fair::PhragmenModelConfig {
1210            weighted: true,
1211            scale: Some(2)  // Double all contributions
1212        },
1213        cases: {
1214            (phragmen_weighted_scaled_fair_election,
1215            // All contributions doubled:
1216            // ALICE: CAROL:100, DAVE:120 - max_load=120
1217            // BOB: FRANK:100, GRACE:240 - max_load=240
1218            // CHARLIE: MIKE:160, IVAN:200 - max_load=200
1219            // ALAN: EVE:120 - max_load=120
1220            // Round 1: ALICE wins (120, first encountered)
1221            // Round 2: ALAN wins (120, loads: CAROL=100, DAVE=120)
1222            // Round 3: CHARLIE wins (200 < 240)
1223            // Round 4: BOB
1224            vec![
1225                (ALICE, vec![(CAROL, 50), (DAVE, 60)]),
1226                (BOB, vec![(FRANK, 50), (GRACE, 120)]),
1227                (CHARLIE, vec![(MIKE, 80), (IVAN, 100)]),
1228                (ALAN, vec![(EVE, 60)])
1229            ],
1230            vec![ALICE, ALAN, CHARLIE, BOB]
1231            )
1232        }
1233    }
1234    plugin_test! {
1235        model: fair::PhragmenModel,
1236        input: Vec<(AccountId32, Vec<(AccountId32, usize)>)>,
1237        output: Vec<AccountId32>,
1238        context: fair::PhragmenModelConfig<usize>,
1239        value: fair::PhragmenModelConfig {
1240            weighted: true,
1241            scale: Some(10)  // 10x multiplier
1242        },
1243        cases: {
1244            (phragmen_large_scale_fair_election,
1245            // All contributions multiplied by 10
1246            // ALICE: max(CAROL:500, DAVE:600) = 600
1247            // BOB: max(FRANK:500, GRACE:1200) = 1200
1248            // CHARLIE: max(MIKE:800, IVAN:1000) = 1000
1249            vec![
1250                (ALICE, vec![(CAROL, 50), (DAVE, 60)]),
1251                (BOB, vec![(FRANK, 50), (GRACE, 120)]),
1252                (CHARLIE, vec![(MIKE, 80), (IVAN, 100)])
1253            ],
1254            vec![ALICE, CHARLIE, BOB]
1255            )
1256        }
1257    }
1258
1259    // --------------------------------- MAX-MIN LOAD --------------------------------
1260
1261    plugin_test! {
1262        model: fair::MaxMinLoadModel,
1263        input: Vec<(AccountId32, Vec<(AccountId32, u64)>)>,
1264        output: Vec<AccountId32>,
1265        cases: {
1266            (max_min_load_basic,
1267            // ALICE: max_load=60, BOB: max_load=120, CHARLIE: max_load=100, ALAN: max_load=60
1268            vec![
1269                (ALICE, vec![(CAROL, 50), (DAVE, 60)]),
1270                (BOB, vec![(FRANK, 50), (GRACE, 120)]),
1271                (CHARLIE, vec![(MIKE, 80), (IVAN, 100)]),
1272                (ALAN, vec![(EVE, 60)])
1273            ],
1274            vec![ALICE, ALAN, CHARLIE, BOB]
1275            ),
1276            (max_min_load_single_backer,
1277            // CAROL backs all - load accumulates
1278            vec![
1279                (ALICE, vec![(CAROL, 100)]),
1280                (BOB, vec![(CAROL, 50)]),
1281                (CHARLIE, vec![(CAROL, 75)])
1282            ],
1283            vec![BOB, CHARLIE, ALICE]
1284            ),
1285            (max_min_load_overlapping_backers,
1286            // Demonstrates load balancing across shared backers
1287            vec![
1288                (ALICE, vec![(CAROL, 60), (DAVE, 80)]),
1289                (BOB, vec![(CAROL, 40)])
1290            ],
1291            vec![BOB, ALICE]
1292            ),
1293            (max_min_load_no_backers,
1294            vec![
1295                (ALICE, vec![]),
1296                (BOB, vec![]),
1297                (CHARLIE, vec![])
1298            ],
1299            vec![ALICE, BOB, CHARLIE]
1300            )
1301        }
1302    }
1303
1304    // ---------------------------------- THRESHOLD ----------------------------------
1305
1306    plugin_test! {
1307        model: fair::ThresholdFairModel,
1308        input: Vec<(AccountId32, Vec<(AccountId32, u64)>)>,
1309        output: Vec<AccountId32>,
1310        context: fair::ThresholdFairModelConfig<u64>,
1311        value: fair::ThresholdFairModelConfig {
1312            threshold: 100
1313        },
1314        cases: {
1315            (threshold_model_basic_fair_election,
1316            // Basic case: filter candidates by total backing >= threshold
1317            // ALICE: 80 (below), BOB: 110 (above), CHARLIE: 130 (above), ALAN: 40 (below)
1318            vec![
1319                (ALICE, vec![(CAROL, 50), (DAVE, 30)]),
1320                (BOB, vec![(FRANK, 50), (GRACE, 60)]),
1321                (CHARLIE, vec![(MIKE, 80), (IVAN, 50)]),
1322                (ALAN, vec![(EVE, 40)])
1323            ],
1324            vec![BOB, CHARLIE]
1325            ),
1326            (threshold_model_all_qualify,
1327            // All candidates meet threshold
1328            // ALICE: 150, BOB: 200, CHARLIE: 180
1329            vec![
1330                (ALICE, vec![(CAROL, 80), (DAVE, 70)]),
1331                (BOB, vec![(FRANK, 100), (GRACE, 100)]),
1332                (CHARLIE, vec![(MIKE, 90), (IVAN, 90)])
1333            ],
1334            vec![ALICE, BOB, CHARLIE]
1335            ),
1336            (threshold_model_none_qualify,
1337            // No candidates meet threshold
1338            // ALICE: 90, BOB: 80, CHARLIE: 70
1339            vec![
1340                (ALICE, vec![(CAROL, 50), (DAVE, 40)]),
1341                (BOB, vec![(FRANK, 40), (GRACE, 40)]),
1342                (CHARLIE, vec![(MIKE, 30), (IVAN, 40)])
1343            ],
1344            vec![]
1345            ),
1346            (threshold_model_no_backers,
1347            // Edge case: candidates with no backers (total = 0)
1348            vec![
1349                (ALICE, vec![]),
1350                (BOB, vec![]),
1351                (CHARLIE, vec![])
1352            ],
1353            vec![]
1354            ),
1355        }
1356    }
1357
1358    plugin_test! {
1359        model: fair::ThresholdFairModel,
1360        input: Vec<(AccountId32, Vec<(AccountId32, u64)>)>,
1361        output: Vec<AccountId32>,
1362        context: fair::ThresholdFairModelConfig<u64>,
1363        value: fair::ThresholdFairModelConfig {
1364            threshold: 0
1365        },
1366        cases: {
1367            (threshold_model_zero_threshold_all_qualify,
1368            // Zero threshold - all candidates qualify, even with zero backing
1369            vec![
1370                (ALICE, vec![(CAROL, 50), (DAVE, 30)]),
1371                (BOB, vec![]),
1372                (CHARLIE, vec![(MIKE, 80)]),
1373                (ALAN, vec![])
1374            ],
1375            vec![ALICE, BOB, CHARLIE, ALAN]
1376            ),
1377
1378            (threshold_model_zero_threshold_preserves_order,
1379            // Verify all candidates preserved in input order
1380            vec![
1381                (CHARLIE, vec![(MIKE, 100)]),
1382                (ALICE, vec![(CAROL, 50)]),
1383                (BOB, vec![(DAVE, 75)]),
1384                (ALAN, vec![])
1385            ],
1386            vec![CHARLIE, ALICE, BOB, ALAN]
1387            ),
1388        }
1389    }
1390}