calc = new ResourceFreeTimeCalculator(); } /** @return array{starts_at: int, ends_at: int} */ private function at(int $fromMinute, int $toMinute): array { return [ 'starts_at' => self::DAY + $fromMinute * 60, 'ends_at' => self::DAY + $toMinute * 60, ]; } // ── ✅ موفق ────────────────────────────────────────────────────────────── public function testAnEmptyDayIsEntirelyFree(): void { $load = $this->calc->forDay([['start_minute' => 540, 'end_minute' => 1020]], [], self::DAY, 1); self::assertSame(480, $load['shift_minutes']); self::assertSame(0, $load['busy_minutes']); self::assertSame(480, $load['free_minutes']); } public function testASingleAppointmentOnACapacityOneResourceIsBusy(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 1020]], [$this->at(600, 660)], self::DAY, 1, ); self::assertSame(60, $load['busy_minutes']); self::assertSame(420, $load['free_minutes']); } // ── ⚠️ مرزی ───────────────────────────────────────────────────────────── /** همان حالتی که کل این محاسبه برایش نوشته شده. */ public function testCapacityThreeWithTwoOverlappingAppointmentsStillHasRoom(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 1020]], [$this->at(600, 660), $this->at(600, 660)], self::DAY, 3, ); self::assertSame(0, $load['busy_minutes'], 'دو نوبت هم‌زمان روی ظرفیت ۳ هیچ دقیقه‌ای را پر نمی‌کند'); self::assertSame(480, $load['free_minutes']); } public function testCapacityIsOnlyFullWhenEveryPlaceIsTaken(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 1020]], [$this->at(600, 660), $this->at(600, 660), $this->at(600, 630)], self::DAY, 3, ); // فقط ۱۰:۰۰ تا ۱۰:۳۰ هر سه جا گرفته است. self::assertSame(30, $load['busy_minutes']); self::assertSame(450, $load['free_minutes']); } /** اشغالِ بیرون از شیفت، وقتِ آزادِ داخل شیفت را کم نمی‌کند. */ public function testOccupancyOutsideTheShiftDoesNotCount(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 1020]], [$this->at(1100, 1200)], self::DAY, 1, ); self::assertSame(0, $load['busy_minutes']); self::assertSame(480, $load['free_minutes']); } /** اشغالی که نیمی‌اش بیرون شیفت است، فقط نیمهٔ داخلش شمرده می‌شود. */ public function testOnlyTheOverlappingPartOfAnAppointmentCounts(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 1020]], [$this->at(480, 600)], self::DAY, 1, ); self::assertSame(60, $load['busy_minutes'], 'از ۰۸:۰۰ تا ۱۰:۰۰ فقط یک ساعتش داخل شیفت است'); } public function testARestDayHasNoFreeTime(): void { $load = $this->calc->forDay([], [$this->at(600, 660)], self::DAY, 1); self::assertSame(0, $load['shift_minutes']); self::assertSame(0, $load['free_minutes']); } /** ظرفیت صفر یا منفی نباید تقسیم‌بر-صفرِ منطقی بسازد؛ کف ۱ است. */ public function testCapacityBelowOneIsTreatedAsOne(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 600]], [$this->at(540, 600)], self::DAY, 0, ); self::assertSame(60, $load['busy_minutes']); } /** دو شیفت در یک روز (صبح و عصر) هر دو شمرده می‌شوند. */ public function testTwoShiftsInADayAreBothCounted(): void { $load = $this->calc->forDay( [['start_minute' => 540, 'end_minute' => 720], ['start_minute' => 960, 'end_minute' => 1200]], [$this->at(600, 660)], self::DAY, 1, ); self::assertSame(420, $load['shift_minutes']); self::assertSame(60, $load['busy_minutes']); self::assertSame(360, $load['free_minutes']); } }