insert([ 'date' => $start->copy()->addWeeks($i)->toDateString(), 'ulsp_pence' => $base + ($i * $stepUlsp), 'ulsd_pence' => 15000 + ($i * $stepUlsd), 'ulsp_duty_pence' => 5295, 'ulsd_duty_pence' => 5295, 'ulsp_vat_pct' => 20, 'ulsd_vat_pct' => 20, ]); } } it('DeltaUlspLag(0) returns ULSP[t-7d] − ULSP[t-14d]', function () { seedRisingTenWeeks(); $loader = new WeeklyPumpPriceLoader; $feature = new DeltaUlspLag($loader, lag: 0); // Target = 2024-02-26 → t-7d = 2024-02-19 (ulsp=14700), t-14d = 2024-02-12 (14600). $value = $feature->valueFor(Carbon::parse('2024-02-26')); expect($value)->toBe(100.0); }); it('DeltaUlspLag(3) returns ULSP[t-28d] − ULSP[t-35d]', function () { seedRisingTenWeeks(); $loader = new WeeklyPumpPriceLoader; $feature = new DeltaUlspLag($loader, lag: 3); // Target = 2024-03-04 → t-28d = 2024-02-05 (14500), t-35d = 2024-01-29 (14400). $value = $feature->valueFor(Carbon::parse('2024-03-04')); expect($value)->toBe(100.0); }); it('DeltaUlspLag returns null when underlying data is missing', function () { seedRisingTenWeeks(); $loader = new WeeklyPumpPriceLoader; $feature = new DeltaUlspLag($loader, lag: 0); // Target before any seeded data → both lookups miss. $value = $feature->valueFor(Carbon::parse('2017-01-01')); expect($value)->toBeNull(); }); it('DeltaUlspLag source dates are strictly before target', function () { $loader = new WeeklyPumpPriceLoader; $feature = new DeltaUlspLag($loader, lag: 0); $target = Carbon::parse('2024-06-03'); $sources = $feature->sourceDates($target); foreach ($sources as $s) { expect($s->lessThan($target))->toBeTrue(); } }); it('DeltaUlsdLag(0) returns ULSD difference for the previous week', function () { seedRisingTenWeeks(); $loader = new WeeklyPumpPriceLoader; $feature = new DeltaUlsdLag($loader, lag: 0); // Diesel rises by 80 each week. lag 0 = t-7 minus t-14. $value = $feature->valueFor(Carbon::parse('2024-02-26')); expect($value)->toBe(80.0); }); it('UlspMinusMa8 returns the gap between latest and 8-week mean', function () { seedRisingTenWeeks(); $loader = new WeeklyPumpPriceLoader; $feature = new UlspMinusMa8($loader); // Target = 2024-03-04. Window = 2024-02-26 (latest) ... 2024-01-08 (oldest). // Values: 14800, 14700, 14600, 14500, 14400, 14300, 14200, 14100. // Latest = 14800, mean = 14450. Gap = 350. $value = $feature->valueFor(Carbon::parse('2024-03-04')); expect($value)->toBe(350.0); }); it('UlspMinusMa8 returns null when 8-week window is incomplete', function () { seedRisingTenWeeks(); $loader = new WeeklyPumpPriceLoader; $feature = new UlspMinusMa8($loader); // Target only has 1 week of history before it. $value = $feature->valueFor(Carbon::parse('2024-01-08')); expect($value)->toBeNull(); }); it('UlspMinusMa8 source dates are 8 weeks back, all before target', function () { $loader = new WeeklyPumpPriceLoader; $feature = new UlspMinusMa8($loader); $target = Carbon::parse('2024-06-03'); $sources = $feature->sourceDates($target); expect($sources)->toHaveCount(8); foreach ($sources as $s) { expect($s->lessThan($target))->toBeTrue(); } }); it('WeekOfYearTrig returns sin/cos values bounded by [-1,1]', function () { $sin = new WeekOfYearTrig('sin'); $cos = new WeekOfYearTrig('cos'); foreach (['2024-01-01', '2024-04-15', '2024-07-29', '2024-12-30'] as $d) { $sv = $sin->valueFor(Carbon::parse($d)); $cv = $cos->valueFor(Carbon::parse($d)); expect($sv)->toBeGreaterThanOrEqual(-1.0)->toBeLessThanOrEqual(1.0) ->and($cv)->toBeGreaterThanOrEqual(-1.0)->toBeLessThanOrEqual(1.0); } }); it('WeekOfYearTrig source dates are empty (calendar feature)', function () { $feature = new WeekOfYearTrig('sin'); expect($feature->sourceDates(Carbon::parse('2024-06-03')))->toBe([]); }); it('WeekOfYearTrig rejects unknown components', function () { new WeekOfYearTrig('tan'); })->throws(InvalidArgumentException::class); it('IsPreBankHoliday is 1.0 when a UK bank holiday is in the next 7 days', function () { $feature = new IsPreBankHoliday; // 2024-04-01 is Easter Monday → that week itself contains a holiday. expect($feature->valueFor(Carbon::parse('2024-04-01')))->toBe(1.0); }); it('IsPreBankHoliday is 0.0 in a quiet stretch', function () { $feature = new IsPreBankHoliday; expect($feature->valueFor(Carbon::parse('2024-07-15')))->toBe(0.0); });