import test from 'node:test';
import assert from 'node:assert/strict';
import {CONFIG,FIRING_ORDER,JOURNAL_PHASES,cylinderState,tourState} from '../lib/mechanics.mjs';
const near=(a,b,eps=1e-10)=>assert.ok(Math.abs(a-b)<eps,`${a} != ${b}`);
test('all eight firing events agree with the cross-plane crank and 90-degree spacing',()=>{
 assert.deepEqual(FIRING_ORDER,[1,8,4,3,6,5,7,2]);assert.equal(new Set(JOURNAL_PHASES).size,4);
 FIRING_ORDER.forEach((c,i)=>{const st=cylinderState(i*90,c);near(st.s,CONFIG.rodLength+CONFIG.radius);assert.equal(st.phase,0);assert.equal(st.stroke,'Power');near(cylinderState(i*90+180,c).s,CONFIG.rodLength-CONFIG.radius);});
});
test('slider crank preserves rod length, cylinder axis and shared crankpin orbit over 720 degrees',()=>{
 for(let a=0;a<=720;a+=.5)for(let c=1;c<=8;c++){
  const s=cylinderState(a,c),p=cylinderState(a,c%2?c+1:c-1);
  near(Math.hypot(...s.wrist.map((x,i)=>x-s.pin[i])),CONFIG.rodLength);
  near(s.wrist[0]*Math.cos(s.beta)-s.wrist[1]*Math.sin(s.beta),0);
  near(s.pin[0],p.pin[0]);near(s.pin[1],p.pin[1]);near(s.pin[2],s.wrist[2]);
 }
});
test('piston period 360; stroke and valve period 720; cam at half speed',()=>{
 for(let a=0;a<720;a+=17)for(let c=1;c<=8;c++){
  const x=cylinderState(a,c),y=cylinderState(a+360,c),z=cylinderState(a+720,c);
  x.wrist.forEach((v,i)=>near(v,y.wrist[i]));near(x.s,z.s);assert.equal(x.stroke,z.stroke);assert.notEqual(x.stroke,y.stroke);near(x.intakeLift,z.intakeLift);near(x.exhaustLift,z.exhaustLift);near(x.camAngle,a/2);
 }
});
test('ideal valve windows and reproducible tour boundaries',()=>{
 for(let a=0;a<720;a++)for(let c=1;c<=8;c++){const s=cylinderState(a,c);assert.ok(!(s.intakeLift>0&&s.exhaustLift>0));if(s.stroke==='Power'||s.stroke==='Compression'){near(s.intakeLift,0);near(s.exhaustLift,0);}}
 assert.equal(tourState(0).view,'assembled');assert.equal(tourState(5).view,'section');assert.equal(tourState(14).view,'exploded');assert.equal(tourState(18).view,'assembled');assert.deepEqual(tourState(8.25),tourState(8.25));
});
