'use strict'; // P0-3 — player/market grouping. Same player must present as ONE row per market // family (alt lines nested), and no single market may flood a ranked list. const { dedupeLeaders, groupIntoLadders, marketFamily } = require('../../web/src/lib/playerGrouping'); describe('dedupeLeaders', () => { test("collapses a player's alt-line ladder to ONE leaderboard row (Bohm strikeouts)", () => { const rows = [ { player: 'Alec Bohm', stat: 'strikeouts', line: 1.6, side: 'U', grade: 'B' }, { player: 'Alec Bohm', stat: 'strikeouts', line: 1.3, side: 'U', grade: 'B' }, { player: 'Alec Bohm', stat: 'strikeouts', line: 1.5, side: 'O', grade: 'B' }, { player: 'Mike Trout', stat: 'hits', line: 0.5, side: 'O', grade: 'A' }, ]; const out = dedupeLeaders(rows); expect(out).toHaveLength(2); expect(out[0]).toMatchObject({ player: 'Alec Bohm', line: 1.6 }); // first (best-ranked) kept expect(out[1].player).toBe('Mike Trout'); }); test('caps a flooding market so 9 identical stolen_bases rows do not dominate', () => { const rows = Array.from({ length: 9 }, (_, i) => ({ player: `Runner ${i}`, stat: 'stolen_bases', line: 0.5, side: 'U', grade: 'B' })); rows.push({ player: 'Slugger', stat: 'home_runs', line: 0.5, side: 'O', grade: 'A' }); const out = dedupeLeaders(rows, 4); expect(out.filter((r) => r.stat === 'stolen_bases')).toHaveLength(4); // capped expect(out.some((r) => r.stat === 'home_runs')).toBe(true); // other markets survive }); test('name variants merge (accents/nicknames) — no double entry', () => { const out = dedupeLeaders([ { player: 'José Ramírez', stat: 'total_bases', line: 1.5 }, { player: 'Jose Ramirez', stat: 'total_bases', line: 2.5 }, ]); expect(out).toHaveLength(1); }); test('empty / malformed → empty, no throw', () => { expect(dedupeLeaders(null)).toEqual([]); expect(dedupeLeaders([{}])).toEqual([]); }); }); describe('groupIntoLadders', () => { test('nests alt lines under one player+market entry, rungs sorted by line', () => { const groups = groupIntoLadders([ { player: 'Alec Bohm', stat: 'strikeouts', line: 1.6, grade: 'B' }, { player: 'Alec Bohm', stat: 'strikeouts', line: 1.3, grade: 'A' }, { player: 'Alec Bohm', stat: 'strikeouts', line: 1.5, grade: 'B' }, ], (a, b) => (b.grade < a.grade ? b : a)); // pick the best grade as primary expect(groups).toHaveLength(1); expect(groups[0].ladder.map((r) => r.line)).toEqual([1.3, 1.5, 1.6]); // rungs ascending expect(groups[0].primary.grade).toBe('A'); // betterOf picked the A rung }); test('different markets for the same player stay separate ladders', () => { const groups = groupIntoLadders([ { player: 'X', stat: 'hits', line: 0.5 }, { player: 'X', stat: 'total_bases', line: 1.5 }, ]); expect(groups).toHaveLength(2); }); }); describe('marketFamily', () => { test('strips case/space; side+line are not part of the family', () => { expect(marketFamily('Total Bases')).toBe('total_bases'); expect(marketFamily('stolen_bases')).toBe('stolen_bases'); }); });