instructorId); self::assertSame('2026-04-01 09:00:00', $slot->startDt); self::assertSame(30, $slot->durationMinutes); self::assertSame(8, $slot->offeringId); self::assertSame(100, $slot->recurrenceGroup); self::assertFalse($slot->isBooked); self::assertSame(42, $slot->id); } public function testDefaults(): void { $slot = new AvailabilitySlot(1, '2026-04-01 09:00:00', '2026-04-01 10:00:00'); self::assertSame(60, $slot->durationMinutes); self::assertNull($slot->offeringId); self::assertFalse($slot->isBooked); self::assertNull($slot->recurrenceGroup); self::assertNull($slot->id); } public function testFromRowMapsCorrectlyAndCastsNullables(): void { $row = (object) [ 'id' => '7', 'instructor_id' => '3', 'offering_id' => null, 'start_dt' => '2026-05-10 14:00:00', 'end_dt' => '2026-05-10 15:00:00', 'duration_minutes' => '60', 'is_booked' => '1', 'recurrence_group' => '7', ]; $slot = AvailabilitySlot::fromRow($row); self::assertSame(7, $slot->id); self::assertSame(3, $slot->instructorId); self::assertNull($slot->offeringId); self::assertSame(60, $slot->durationMinutes); self::assertSame(7, $slot->recurrenceGroup); self::assertTrue($slot->isBooked); } public function testNormalizeDateTimeAcceptsCanonicalAndDatetimeLocalForms(): void { self::assertSame('2026-04-01 09:00:00', AvailabilitySlot::normalizeDateTime('2026-04-01 09:00:00')); self::assertSame('2026-04-01 09:00:00', AvailabilitySlot::normalizeDateTime('2026-04-01 09:00')); self::assertSame('2026-04-01 09:00:00', AvailabilitySlot::normalizeDateTime('2026-04-01T09:00')); self::assertSame('2026-04-01 09:00:30', AvailabilitySlot::normalizeDateTime('2026-04-01T09:00:30')); } public function testNormalizeDateTimeRejectsGarbageAndImpossibleDates(): void { self::assertNull(AvailabilitySlot::normalizeDateTime('')); self::assertNull(AvailabilitySlot::normalizeDateTime('not a date')); self::assertNull(AvailabilitySlot::normalizeDateTime('next tuesday')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-04-01')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-13-01 09:00:00')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-02-30 09:00:00')); self::assertNull(AvailabilitySlot::normalizeDateTime('2026-04-01 25:00:00')); self::assertNull(AvailabilitySlot::normalizeDateTime("2026-04-01 09:00:00'); DROP TABLE x;--")); } public function testSplitByDurationChunksWindowIntoLessonLengthSlots(): void { $window = new AvailabilitySlot(5, '2026-07-06 09:00:00', '2026-07-06 12:00:00', 60, 8); $slots = $window->splitByDuration(); self::assertCount(3, $slots); self::assertSame('2026-07-06 09:00:00', $slots[0]->startDt); self::assertSame('2026-07-06 10:00:00', $slots[0]->endDt); self::assertSame('2026-07-06 11:00:00', $slots[2]->startDt); self::assertSame('2026-07-06 12:00:00', $slots[2]->endDt); // Each chunk keeps the window's instructor, duration, and offering. self::assertSame(5, $slots[1]->instructorId); self::assertSame(60, $slots[1]->durationMinutes); self::assertSame(8, $slots[1]->offeringId); } public function testSplitByDurationDropsRemainderShorterThanALesson(): void { $window = new AvailabilitySlot(5, '2026-07-06 09:00:00', '2026-07-06 16:30:00', 60); $slots = $window->splitByDuration(); self::assertCount(7, $slots); self::assertSame('2026-07-06 16:00:00', $slots[6]->endDt); } public function testSplitByDurationReturnsEmptyWhenWindowTooShort(): void { $window = new AvailabilitySlot(5, '2026-07-06 09:00:00', '2026-07-06 09:45:00', 60); self::assertSame([], $window->splitByDuration()); } public function testSplitByDurationHandlesThirtyMinuteLessons(): void { $window = new AvailabilitySlot(5, '2026-07-06 09:00:00', '2026-07-06 10:30:00', 30); $slots = $window->splitByDuration(); self::assertCount(3, $slots); self::assertSame('2026-07-06 09:30:00', $slots[0]->endDt); } public function testToArrayContainsExpectedKeys(): void { $slot = new AvailabilitySlot(1, '2026-04-01 09:00:00', '2026-04-01 10:00:00', 30, 8, false, null, 10); $arr = $slot->toArray(); foreach (['id', 'instructor_id', 'offering_id', 'start_dt', 'end_dt', 'duration_minutes', 'is_booked', 'recurrence_group'] as $key) { self::assertArrayHasKey($key, $arr); } } }