* SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the LICENSE file in the project root for the full license text. */ declare(strict_types=1); namespace App\Tests\Domain; use App\Domain\SprintWeek; use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** * Phase 12: SprintWeek's weekday helpers. Pure logic — no DB. */ final class SprintWeekTest extends TestCase { public function testPopcountCoversAllFiveBits(): void { $this->assertSame(0, SprintWeek::popcount(0)); $this->assertSame(5, SprintWeek::popcount(31)); $this->assertSame(1, SprintWeek::popcount(1)); $this->assertSame(4, SprintWeek::popcount(15)); // bits outside the 5-day range are clamped away $this->assertSame(5, SprintWeek::popcount(0xFFFF)); } public function testMaskToDaysReturnsCanonicalOrder(): void { $this->assertSame(['Mo', 'Di', 'Mi', 'Do', 'Fr'], SprintWeek::maskToDays(31)); $this->assertSame(['Mo', 'Fr'], SprintWeek::maskToDays(0b10001)); $this->assertSame([], SprintWeek::maskToDays(0)); } public function testDaysToMaskRoundTripsBothDirections(): void { foreach ([0, 1, 2, 7, 15, 17, 31] as $m) { $days = SprintWeek::maskToDays($m); $this->assertSame($m, SprintWeek::daysToMask($days), "round-trip for mask={$m}"); } } public function testDaysToMaskRejectsUnknownLabel(): void { $this->expectException(InvalidArgumentException::class); SprintWeek::daysToMask(['Mo', 'Sa']); // Sa not in Mo–Fr set } public function testDaysToMaskRejectsDuplicates(): void { $this->expectException(InvalidArgumentException::class); SprintWeek::daysToMask(['Mo', 'Mo']); } public function testHasDayMatchesMaskBit(): void { $w = new SprintWeek( id: 1, sprintId: 1, sortOrder: 1, isoWeek: 1, startDate: '2026-01-05', maxWorkingDays: 3.0, activeDaysMask: 0b00111, // Mo Di Mi ); $this->assertTrue($w->hasDay('Mo')); $this->assertTrue($w->hasDay('Mi')); $this->assertFalse($w->hasDay('Do')); $this->assertFalse($w->hasDay('Fr')); // Unknown label is a miss, not an error. $this->assertFalse($w->hasDay('Sa')); } public function testWithMaskRecomputesMaxWorkingDays(): void { $w = new SprintWeek( id: 1, sprintId: 1, sortOrder: 1, isoWeek: 1, startDate: '2026-01-05', maxWorkingDays: 5.0, activeDaysMask: 31, ); $dropped = $w->withMask(0b01111); // Mo Di Mi Do $this->assertSame(15, $dropped->activeDaysMask); $this->assertSame(4.0, $dropped->maxWorkingDays); $this->assertSame(['Mo', 'Di', 'Mi', 'Do'], $dropped->activeDays()); // Bits outside Mo–Fr are trimmed. $this->assertSame(31, $w->withMask(0xFF)->activeDaysMask); } public function testAuditSnapshotIncludesMask(): void { $w = new SprintWeek( id: 77, sprintId: 9, sortOrder: 2, isoWeek: 8, startDate: '2026-02-16', maxWorkingDays: 2.0, activeDaysMask: 0b00011, ); $snap = $w->toAuditSnapshot(); $this->assertSame(0b00011, $snap['active_days_mask']); $this->assertSame(2.0, $snap['max_working_days']); } }