* 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\Repositories; use App\Domain\SprintWeek; use App\Repositories\SprintRepository; use App\Repositories\SprintWeekRepository; use App\Tests\TestCase; /** * Phase 12: per-week weekday selection drives Arbeitstage. */ final class SprintWeekRepositoryTest extends TestCase { /** @return array{SprintRepository, SprintWeekRepository, int} */ private function seedSprint(): array { $pdo = $this->makeDb(); $sprints = new SprintRepository($pdo); $weeks = new SprintWeekRepository($pdo); $sprint = $sprints->create('S', '2026-01-05', '2026-01-30', 0.2); $sprints->materializeWeeks($sprint->id, '2026-01-05', '2026-01-30', 4); return [$sprints, $weeks, $sprint->id]; } public function testMaterializeWeeksDefaultsToAllFiveDays(): void { [, $weeks, $sprintId] = $this->seedSprint(); $rows = $weeks->allForSprint($sprintId); $this->assertCount(4, $rows); foreach ($rows as $w) { $this->assertSame(SprintWeek::MASK_ALL, $w->activeDaysMask); $this->assertSame(5.0, $w->maxWorkingDays); } } public function testMaterializeWeeksTrimsTrailingWeekToSprintEnd(): void { $pdo = $this->makeDb(); $sprints = new SprintRepository($pdo); $weeks = new SprintWeekRepository($pdo); // Mon 2026-01-05 → Wed 2026-01-14 → 2 weeks; the trailing week stops on Wed. $sprint = $sprints->create('S', '2026-01-05', '2026-01-14', 0.2); $sprints->materializeWeeks($sprint->id, '2026-01-05', '2026-01-14', 2); $rows = $weeks->allForSprint($sprint->id); $this->assertCount(2, $rows); $this->assertSame(SprintWeek::MASK_ALL, $rows[0]->activeDaysMask); $this->assertSame(5.0, $rows[0]->maxWorkingDays); // Mo + Di + Mi = bit0|bit1|bit2 = 7, three workdays. $this->assertSame(0b00111, $rows[1]->activeDaysMask); $this->assertSame(3.0, $rows[1]->maxWorkingDays); } public function testMaterializeWeeksTrimsLeadingPartialWeekFromMidWeekStart(): void { $pdo = $this->makeDb(); $sprints = new SprintRepository($pdo); $weeks = new SprintWeekRepository($pdo); // Sprint runs Wed 2026-01-07 → Fri 2026-01-23 (3 weeks). $sprint = $sprints->create('S', '2026-01-07', '2026-01-23', 0.2); $sprints->materializeWeeks($sprint->id, '2026-01-07', '2026-01-23', 3); $rows = $weeks->allForSprint($sprint->id); $this->assertCount(3, $rows); // W1: Wed–Tue spans all five Mo..Fr weekdays inside the sprint. $this->assertSame(SprintWeek::MASK_ALL, $rows[0]->activeDaysMask); // W2: same, fully interior. $this->assertSame(SprintWeek::MASK_ALL, $rows[1]->activeDaysMask); // W3 starts Wed Jan 21 — only Wed/Thu/Fri fall inside [start, end]. // Mi + Do + Fr = bit2|bit3|bit4 = 28. $this->assertSame(0b11100, $rows[2]->activeDaysMask); $this->assertSame(3.0, $rows[2]->maxWorkingDays); } public function testMaterializeWeeksWeekendOnlySpanProducesEmptyMask(): void { $pdo = $this->makeDb(); $sprints = new SprintRepository($pdo); $weeks = new SprintWeekRepository($pdo); // Single-day sprint on Sat 2026-01-10 → no weekdays in range. $sprint = $sprints->create('S', '2026-01-10', '2026-01-10', 0.2); $sprints->materializeWeeks($sprint->id, '2026-01-10', '2026-01-10', 1); $rows = $weeks->allForSprint($sprint->id); $this->assertCount(1, $rows); $this->assertSame(0, $rows[0]->activeDaysMask); $this->assertSame(0.0, $rows[0]->maxWorkingDays); } public function testUpdateActiveDaysWritesBothColumns(): void { [, $weeks, $sprintId] = $this->seedSprint(); $first = $weeks->allForSprint($sprintId)[0]; $result = $weeks->updateActiveDays($first->id, 0b01111); // drop Fr $this->assertSame(SprintWeek::MASK_ALL, $result['before']->activeDaysMask); $this->assertSame(0b01111, $result['after']->activeDaysMask); $this->assertSame(4.0, $result['after']->maxWorkingDays); // Second query hydrates the same row from disk. $reloaded = $weeks->find($first->id); $this->assertNotNull($reloaded); $this->assertSame(0b01111, $reloaded->activeDaysMask); $this->assertSame(4.0, $reloaded->maxWorkingDays); } public function testUpdateActiveDaysClampsBitsOutsideMoFr(): void { [, $weeks, $sprintId] = $this->seedSprint(); $first = $weeks->allForSprint($sprintId)[0]; $result = $weeks->updateActiveDays($first->id, 0xFF); $this->assertSame(SprintWeek::MASK_ALL, $result['after']->activeDaysMask); $this->assertSame(5.0, $result['after']->maxWorkingDays); } public function testUpdateActiveDaysToEmptyMaskZeroesCount(): void { [, $weeks, $sprintId] = $this->seedSprint(); $first = $weeks->allForSprint($sprintId)[0]; $result = $weeks->updateActiveDays($first->id, 0); $this->assertSame(0, $result['after']->activeDaysMask); $this->assertSame(0.0, $result['after']->maxWorkingDays); $this->assertSame([], $result['after']->activeDays()); } public function testSyncCountAppendsWeeksWithAllDaysActive(): void { [, $weeks, $sprintId] = $this->seedSprint(); $diff = $weeks->syncCount($sprintId, '2026-01-05', 6); $this->assertCount(2, $diff['added']); foreach ($diff['added'] as $w) { $this->assertSame(SprintWeek::MASK_ALL, $w->activeDaysMask); $this->assertSame(5.0, $w->maxWorkingDays); } } public function testRealignDatesRewritesOffsetsAndPreservesMask(): void { [, $weeks, $sprintId] = $this->seedSprint(); // Customise week 2's mask so we can verify it survives realignment. $week2 = $weeks->allForSprint($sprintId)[1]; $weeks->updateActiveDays($week2->id, 0b00111); // Mo Di Mi only // Move the sprint start one week forward. $diffs = $weeks->realignDates($sprintId, '2026-01-12'); $this->assertCount(4, $diffs, 'every existing week shifted one week'); $reloaded = $weeks->allForSprint($sprintId); $this->assertSame('2026-01-12', $reloaded[0]->startDate); $this->assertSame('2026-01-19', $reloaded[1]->startDate); $this->assertSame('2026-01-26', $reloaded[2]->startDate); $this->assertSame('2026-02-02', $reloaded[3]->startDate); // Mask + maxWorkingDays untouched by realignment. $this->assertSame(0b00111, $reloaded[1]->activeDaysMask); $this->assertSame(3.0, $reloaded[1]->maxWorkingDays); } public function testRealignDatesNoOpWhenAlreadyAligned(): void { [, $weeks, $sprintId] = $this->seedSprint(); $diffs = $weeks->realignDates($sprintId, '2026-01-05'); $this->assertSame([], $diffs, 'no UPDATEs when offsets already match'); } }