import { describe, expect, it } from 'vitest';
import type { EntityState, SimSnapshot } from '../contracts';
import { computeStrain, STRAIN_HORIZON_SECONDS } from './strain';

// v4 fixture hygiene: commissionQueue and research both harden in v5, so they are populated
// here now rather than left to break the day the contract tightens.
function snap(bandwidth: Partial<SimSnapshot['bandwidth']>, entities: Partial<EntityState>[] = []): SimSnapshot {
  return {
    tick: 0,
    paused: false,
    entities: entities.map((e, i) => ({
      id: i, def: 'x', pos: { x: 0, y: 0 }, dir: 0, recipe: null, progress: 0,
      inputBuf: {}, outputBuf: {}, jammed: null, heat: 0, ...e,
    })),
    beltItems: [],
    bandwidth: { gen: 0, draw: 0, stored: 0, brownout: false, ...bandwidth },
    shippedTotal: {},
    activeCommission: null,
    commissionQueue: [],
    commissionProgress: {},
    research: { active: null, progress: {}, unlocked: [] },
  };
}

describe('the NOTHING IS WRONG era stays sterile', () => {
  it('is inert with no snapshot at all', () => {
    expect(computeStrain(undefined)).toEqual({ tremor: 0, fever: 0, pallor: 0 });
  });

  it('does not leak strain into a fresh factory (gen=draw=stored=0)', () => {
    // The boot state. Nothing built, nothing drawing: dread here would be a lie.
    expect(computeStrain(snap({}))).toEqual({ tremor: 0, fever: 0, pallor: 0 });
  });

  it('stays sterile with no tanks built, even though stored is 0', () => {
    // The trap in the second dread axis: stored/capacity with no capacity must not read as
    // "empty". You cannot be low on a reserve you never built.
    expect(computeStrain(snap({ capacity: 0 })).pallor).toBe(0);
    expect(computeStrain(snap({ gen: 50, draw: 20, capacity: undefined })).pallor).toBe(0);
  });

  it('stays calm for a healthy factory running a surplus', () => {
    expect(computeStrain(snap({ gen: 100, draw: 40, stored: 500, capacity: 500 })))
      .toEqual({ tremor: 0, fever: 0, pallor: 0 });
  });

  it('stays calm in surplus even with an empty reserve', () => {
    // Nothing is draining, so there is nothing to dread — banked reserve is irrelevant.
    expect(computeStrain(snap({ gen: 100, draw: 100, stored: 0 })).tremor).toBe(0);
  });

  it('stays calm with cold machines', () => {
    expect(computeStrain(snap({ gen: 10, draw: 5 }, [{ heat: 0 }, { heat: 0 }])).fever).toBe(0);
  });
});

describe('tremor tracks seconds-of-reserve, not raw stored', () => {
  it('is silent while the reserve outlasts the horizon', () => {
    // deficit 10/s, 200 banked = 20s of runway: further out than the horizon.
    expect(computeStrain(snap({ gen: 0, draw: 10, stored: 200 })).tremor).toBe(0);
  });

  it('is total when the reserve is gone and the draw continues', () => {
    expect(computeStrain(snap({ gen: 0, draw: 10, stored: 0 })).tremor).toBe(1);
  });

  it('sits halfway at half the horizon', () => {
    const stored = 10 * (STRAIN_HORIZON_SECONDS / 2); // deficit 10/s -> horizon/2 seconds left
    expect(computeStrain(snap({ gen: 0, draw: 10, stored })).tremor).toBeCloseTo(0.5, 5);
  });

  it('rises monotonically as the reserve drains', () => {
    let prev = -1;
    for (let stored = 200; stored >= 0; stored -= 10) {
      const t = computeStrain(snap({ gen: 0, draw: 10, stored })).tremor;
      expect(t).toBeGreaterThanOrEqual(prev);
      prev = t;
    }
    expect(prev).toBe(1);
  });

  it('reads the same dread from a big factory as a small one at equal runway', () => {
    // A megafactory bleeding 1000/s with 4000 banked is in exactly as much trouble as a
    // shack bleeding 10/s with 40 banked. Seconds, not units, is the honest measure.
    const big = computeStrain(snap({ gen: 0, draw: 1000, stored: 4000 })).tremor;
    const small = computeStrain(snap({ gen: 0, draw: 10, stored: 40 })).tremor;
    expect(big).toBeCloseTo(small, 5);
  });

  it('never leaves 0..1, even absurdly overdrawn', () => {
    const t = computeStrain(snap({ gen: 0, draw: 1e9, stored: -50 })).tremor;
    expect(t).toBeGreaterThanOrEqual(0);
    expect(t).toBeLessThanOrEqual(1);
  });
});

describe('the two dreads are different questions', () => {
  it('full tank draining fast: tremor without pallor', () => {
    // 4 seconds from dark, but the tanks are brimming. Acute, not chronic.
    const s = computeStrain(snap({ gen: 0, draw: 10, stored: 40, capacity: 40 }));
    expect(s.tremor).toBe(0.5);
    expect(s.pallor).toBe(0);
  });

  it('near-empty tank coasting: pallor without tremor', () => {
    // Nothing is draining — draw <= gen — so there is no cliff. But there is also no cushion.
    const s = computeStrain(snap({ gen: 100, draw: 100, stored: 5, capacity: 500 }));
    expect(s.tremor).toBe(0);
    expect(s.pallor).toBeCloseTo(0.99, 5);
  });

  it('empty tank AND draining: both, which is the real emergency', () => {
    const s = computeStrain(snap({ gen: 0, draw: 10, stored: 0, capacity: 500 }));
    expect(s.tremor).toBe(1);
    expect(s.pallor).toBe(1);
  });

  it('pallor tracks the fraction, not the amount', () => {
    // Same 100 banked reads as comfortable in a small tank and dire in a huge one.
    expect(computeStrain(snap({ stored: 100, capacity: 100 })).pallor).toBe(0);
    expect(computeStrain(snap({ stored: 100, capacity: 1000 })).pallor).toBeCloseTo(0.9, 5);
  });

  it('never leaves 0..1 when the sim overfills or overdraws a tank', () => {
    expect(computeStrain(snap({ stored: 900, capacity: 500 })).pallor).toBe(0);
    expect(computeStrain(snap({ stored: -20, capacity: 500 })).pallor).toBe(1);
  });
});

describe('fever reads the machines that actually run hot', () => {
  it('is not diluted by a sea of cold belts', () => {
    // One decoder at 0.9 surrounded by 50 cold belts is a fever, not a rounding error.
    const entities = [{ heat: 0.9 }, ...Array.from({ length: 50 }, () => ({ heat: 0 }))];
    expect(computeStrain(snap({}), ).fever).toBe(0);
    expect(computeStrain(snap({}, entities)).fever).toBeCloseTo(0.9, 5);
  });

  it('averages across the hot machines', () => {
    expect(computeStrain(snap({}, [{ heat: 0.4 }, { heat: 0.8 }])).fever).toBeCloseTo(0.6, 5);
  });

  it('stays within 0..1 for a fully scrammed factory', () => {
    const f = computeStrain(snap({}, [{ heat: 1, scrammed: true }, { heat: 1 }])).fever;
    expect(f).toBe(1);
  });
});
