// Run: node tests/analytics_core.cjs. No browser or third-party packages needed. const assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs'),path=require('node:path'); const window={addEventListener(){}}; vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../ui_builder/static/analytics.js'),'utf8'),{window,URL,Date}); const A=window.HockeyAnalytics; let checks=0; function test(name,fn){fn();checks++;console.log('PASS '+name);} const player=(extra={})=>({side:'home',id:'a',name:'Иван Иванов',khl_id:'1',role:'skater',birth_date:'2000-09-27',career_statistics:{available:true,games:498,goals:149,assists:181,points:330},...extra}); test('missing cells and non-finite values never become zero',()=>{for(const x of [null,undefined,'',' ','-','—','NaN','Infinity',false])assert.equal(A.number(x),null);assert.equal(A.number('0'),0);assert.equal(A.number('12,5'),12.5);}); test('per game divides additive counts only',()=>{const m=A.METRICS.skater.find(m=>m.key==='points');assert.equal(A.valueFor({games:10,points:25},m,true),2.5);assert.equal(A.valueFor({games:0,points:25},m,true),null);assert.equal(A.valueFor({games:10,points:25},m,false),25);}); test('games percentages and goalie GAA retain units',()=>{for(const [role,key,n]of [['skater','games',40],['skater','shot_pct',12.5],['goalkeeper','save_pct',92.3],['goalkeeper','goals_against_average',2.1]]){const m=A.METRICS[role].find(m=>m.key===key);assert.equal(A.valueFor({games:40,[key]:n},m,true),n);}}); test('unknown and unavailable do not imply zero; negative plus/minus stays signed',()=>{const m=A.METRICS.skater.find(m=>m.key==='plus_minus');assert.equal(A.valueFor({games:10,plus_minus:-12},m,true),-1.2);assert.equal(A.valueFor({available:false,games:10,plus_minus:0},m,false),null);}); test('near milestones are factual and limited by threshold',()=>{const hints=A.makeHints([player()],5);assert.equal(hints.length,2);assert.equal(hints[0].key,'goals');assert.equal(hints[0].remaining,1);assert.equal(hints[1].target,500);assert.equal(A.makeHints([player()],1).length,1);}); test('exact milestone differs from a future milestone',()=>{const h=A.makeHints([player({career_statistics:{games:500}})],5);assert.equal(h[0].remaining,0);assert.equal(h[0].target,500);}); test('unknown fractional negative unmatched statistics produce no milestones',()=>{for(const value of [null,'',-1,0,49.5])assert.equal(A.makeHints([player({career_statistics:{games:value}})],5).length,0);assert.equal(A.makeHints([player({khl_id:''})],5).length,0);}); test('goalie milestones use games wins shutouts',()=>{const h=A.makeHints([player({role:'goalkeeper',career_statistics:{games:498,wins:149,shutouts:29,goals:149}})],5);assert.equal(h.length,3);assert.ok(!h.some(h=>h.key==='goals'));}); test('birthday on match date uses that date rather than wall clock',()=>{const h=A.makeBirthdays([player()],'2026-09-27',0);assert.equal(h.length,1);assert.equal(h[0].age,26);assert.equal(h[0].days,0);}); test('birthdays before and after match preserve direction and anniversary age',()=>{const hs=A.makeBirthdays([player({birth_date:'1999-09-24'}),player({id:'b',birth_date:'2001-09-30'})],'2026-09-27',7);assert.equal(hs[0].days,-3);assert.equal(hs[0].age,27);assert.equal(hs[1].days,3);assert.equal(hs[1].age,25);}); test('December and January crossing gives correct year and age',()=>{const h=A.makeBirthdays([player({birth_date:'2000-01-02'})],'2026-12-29',7)[0];assert.equal(h.days,4);assert.equal(h.age,27);assert.equal(h.date,'2027-01-02');const p=A.makeBirthdays([player({birth_date:'2000-12-30'})],'2027-01-02',7)[0];assert.equal(p.days,-3);assert.equal(p.age,26);}); test('leap day stays February 29 without invented substitute date',()=>{const p=player({birth_date:'2000-02-29'});assert.equal(A.makeBirthdays([p],'2027-02-28',7).length,0);assert.equal(A.makeBirthdays([p],'2028-02-28',7)[0].days,1);}); test('missing invalid future birthdays and missing game date are skipped',()=>{for(const d of ['',null,'2000-02-31','2100-09-27','2026-09-27'])assert.equal(A.makeBirthdays([player({birth_date:d})],'2026-09-27',7).length,0);assert.equal(A.makeBirthdays([player()],'',7).length,0);}); console.log(JSON.stringify({ok:true,checks}));