recalculate('24h'))->toBe(0); expect(app(TrendingService::class)->recalculate('7d'))->toBe(0); }); it('updates trending_score_24h for artworks published within 7 days', function () { $artwork = Artwork::factory()->create([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subHours(6), ]); $updated = app(TrendingService::class)->recalculate('24h'); expect($updated)->toBe(1); $artwork->refresh(); expect($artwork->trending_score_24h)->toBeFloat(); expect($artwork->last_trending_calculated_at)->not->toBeNull(); })->skip('Requires MySQL: uses GREATEST() and TIMESTAMPDIFF() which are not available in SQLite'); it('updates trending_score_7d for artworks published within 30 days', function () { $artwork = Artwork::factory()->create([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDays(10), ]); $updated = app(TrendingService::class)->recalculate('7d'); expect($updated)->toBe(1); $artwork->refresh(); expect($artwork->trending_score_7d)->toBeFloat(); })->skip('Requires MySQL: uses GREATEST() and TIMESTAMPDIFF() which are not available in SQLite'); it('skips artworks published outside the look-back window', function () { Artwork::factory()->create([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDays(45), // outside 30-day window ]); expect(app(TrendingService::class)->recalculate('7d'))->toBe(0); }); it('skips private artworks', function () { Artwork::factory()->create([ 'is_public' => false, 'is_approved' => true, 'published_at' => now()->subDay(), ]); expect(app(TrendingService::class)->recalculate('24h'))->toBe(0); }); it('skips unapproved artworks', function () { Artwork::factory()->create([ 'is_public' => true, 'is_approved' => false, 'published_at' => now()->subDay(), ]); expect(app(TrendingService::class)->recalculate('24h'))->toBe(0); }); it('score is always non-negative (GREATEST clamp)', function () { // Artwork with no stats — time decay may be large, but score is clamped to ≥ 0 $artwork = Artwork::factory()->create([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDays(6), ]); app(TrendingService::class)->recalculate('24h'); $artwork->refresh(); expect($artwork->trending_score_24h)->toBeGreaterThanOrEqualTo(0.0); })->skip('Requires MySQL: uses GREATEST() and TIMESTAMPDIFF() which are not available in SQLite'); it('processes multiple artworks in a single run', function () { Artwork::factory()->count(5)->create([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay(), ]); expect(app(TrendingService::class)->recalculate('7d'))->toBe(5); })->skip('Requires MySQL: uses GREATEST() and TIMESTAMPDIFF() which are not available in SQLite');