<?php
/*
 * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
 * 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');
    }
}
