/*
 * Tests for the pure dynamic-PM scheduling math: the lifespan wear curve, usage-rate inference,
 * usage-threshold projection, and the earliest-wins composite next-due. No DB — plain objects in,
 * dates out.
 */

const {
  ageFraction, computeNextDue, intervalToDays, lifespanFactor, projectUsageDue, usageRatePerDay
} = require('./pmScheduling');

describe('pm scheduling — lifespan curve', () => {
  test('factor is 1 when new and minFactor at end of life', () => {
    expect(lifespanFactor(0, { minFactor: 0.5, exponent: 2 })).toBeCloseTo(1);
    expect(lifespanFactor(1, { minFactor: 0.5, exponent: 2 })).toBeCloseTo(0.5);
  });

  test('factor decreases monotonically with age', () => {
    const early = lifespanFactor(0.25, { minFactor: 0.5, exponent: 2 });
    const mid = lifespanFactor(0.5, { minFactor: 0.5, exponent: 2 });
    const late = lifespanFactor(0.9, { minFactor: 0.5, exponent: 2 });
    expect(early).toBeGreaterThan(mid);
    expect(mid).toBeGreaterThan(late);
  });

  test('ageFraction clamps to [0,1] and handles missing life', () => {
    expect(ageFraction(null, 60, '2026-01-01')).toBe(0);
    expect(ageFraction('2020-01-01', 0, '2026-01-01')).toBe(0);
    // 12 years old against a 5-year (60mo) life → clamped to 1.
    expect(ageFraction('2014-01-01', 60, '2026-01-01')).toBe(1);
  });
});

describe('pm scheduling — usage', () => {
  test('intervalToDays converts units', () => {
    expect(intervalToDays(1, 'weeks')).toBe(7);
    expect(intervalToDays(2, 'months')).toBe(60);
    expect(intervalToDays(1, 'years')).toBe(365);
  });

  test('usageRatePerDay needs ≥2 increasing readings over time', () => {
    expect(usageRatePerDay([{ reading: 100, reading_date: '2026-01-01' }])).toBeNull();
    const rate = usageRatePerDay([
      { reading: 100, reading_date: '2026-01-01' },
      { reading: 200, reading_date: '2026-01-11' }
    ]);
    expect(rate).toBeCloseTo(10); // 100 units / 10 days
  });

  test('projectUsageDue returns today when already past threshold', () => {
    const meter = { due_at_reading: 500, last_reading: 520, baseline_reading: 0 };
    expect(projectUsageDue(meter, [], '2026-06-06')).toBe('2026-06-06');
  });

  test('projectUsageDue projects forward from the measured rate', () => {
    const meter = { due_at_reading: 500, last_reading: 400, baseline_reading: 0 };
    const readings = [
      { reading: 300, reading_date: '2026-01-01' },
      { reading: 400, reading_date: '2026-01-11' } // 10/day → 100 remaining ≈ 10 days
    ];
    expect(projectUsageDue(meter, readings, '2026-06-06')).toBe('2026-06-16');
  });
});

describe('pm scheduling — composite next-due (earliest wins)', () => {
  const asset = { in_service_date: '2026-01-01', useful_life_months: 60 };

  test('falls back to the calendar date when signals are disabled', () => {
    const out = computeNextDue(
      { lastDate: '2026-06-06', frequency_value: 6, frequency_unit: 'months', lifespan_adjust: true },
      asset, [], { usage_enabled: false, curve_enabled: false }, '2026-06-06'
    );
    // 6 months ≈ 180 days out, no compression.
    expect(out.next_due_date).toBe('2026-12-03');
    expect(out.driver).toBe('calendar');
  });

  test('a fast usage rate pulls the due date in before the calendar date', () => {
    const meters = [{
      usage_interval: 500, due_at_reading: 500, last_reading: 450, baseline_reading: 0,
      readings: [
        { reading: 0, reading_date: '2026-05-01' },
        { reading: 450, reading_date: '2026-06-05' } // ~12.8/day → ~4 days to 500
      ]
    }];
    const out = computeNextDue(
      { lastDate: '2026-06-06', frequency_value: 6, frequency_unit: 'months', lifespan_adjust: false },
      asset, meters, { usage_enabled: true, curve_enabled: false }, '2026-06-06'
    );
    expect(out.driver).toBe('usage');
    expect(out.next_due_date < '2026-12-03').toBe(true);
  });

  test('the lifespan curve compresses the interval for an aged asset', () => {
    const old = { in_service_date: '2021-06-06', useful_life_months: 60 }; // ~5yr old → end of life
    const settings = { usage_enabled: false, curve_enabled: true, min_factor: 0.5, curve_exponent: 1 };
    const fresh = computeNextDue(
      { lastDate: '2026-06-06', frequency_value: 6, frequency_unit: 'months', lifespan_adjust: true },
      asset, [], settings, '2026-06-06'
    );
    const aged = computeNextDue(
      { lastDate: '2026-06-06', frequency_value: 6, frequency_unit: 'months', lifespan_adjust: true },
      old, [], settings, '2026-06-06'
    );
    expect(aged.next_due_date < fresh.next_due_date).toBe(true);
    expect(aged.driver).toBe('lifespan');
  });

  test('lifespan compression is ignored when the schedule opts out', () => {
    const old = { in_service_date: '2021-06-06', useful_life_months: 60 };
    const settings = { usage_enabled: false, curve_enabled: true, min_factor: 0.5, curve_exponent: 1 };
    const out = computeNextDue(
      { lastDate: '2026-06-06', frequency_value: 6, frequency_unit: 'months', lifespan_adjust: false },
      old, [], settings, '2026-06-06'
    );
    expect(out.driver).toBe('calendar');
    expect(out.signals.factor).toBe(1);
  });
});
