<?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\Services;

use App\Services\CapacityCalculator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class CapacityCalculatorTest extends TestCase
{
    // -------------------------------------------------------------------
    // roundHalf
    // -------------------------------------------------------------------

    /** @return list<array{float,float}> */
    public static function roundHalfCases(): array
    {
        return [
            [0.0,  0.0],
            [0.24, 0.0],
            [0.25, 0.5],   // round(0.5) = 1 -> /2 = 0.5
            [0.26, 0.5],
            [0.75, 1.0],
            [1.24, 1.0],
            [1.25, 1.5],
            [1.74, 1.5],
            [1.75, 2.0],
            [3.10, 3.0],
            [3.30, 3.5],
            [5.00, 5.0],
            [16.8, 17.0],  // 21 * 0.8
            [14.4, 14.5],  // 18 * 0.8
            [13.6, 13.5],  // 17 * 0.8
        ];
    }

    #[DataProvider('roundHalfCases')]
    public function testRoundHalf(float $input, float $expected): void
    {
        $this->assertSame($expected, CapacityCalculator::roundHalf($input));
    }

    // -------------------------------------------------------------------
    // forWorker — spec §6.5 examples
    // -------------------------------------------------------------------

    public function testForWorkerEmptySprint(): void
    {
        $r = CapacityCalculator::forWorker(0.0, 0.2, 0.0);
        $this->assertSame(0.0, $r['ressourcen']);
        $this->assertSame(0.0, $r['after_reserves']);
        $this->assertSame(0.0, $r['committed_prio1']);
        $this->assertSame(0.0, $r['available']);
    }

    public function testForWorkerSpecExample(): void
    {
        // 20 days raw, 20% reserve → 20*0.8 = 16 → available = 16 (no prio-1)
        $r = CapacityCalculator::forWorker(20.0, 0.2, 0.0);
        $this->assertSame(16.0, $r['after_reserves']);
        $this->assertSame(16.0, $r['available']);
    }

    public function testForWorkerFractionalReserveRoundsToHalfStep(): void
    {
        // 21 * 0.8 = 16.8 → round_half = 17.0
        $r = CapacityCalculator::forWorker(21.0, 0.2, 0.0);
        $this->assertSame(17.0, $r['after_reserves']);
    }

    public function testForWorkerPrio1CommitmentsReduceAvailable(): void
    {
        // 20 res, 20% reserve → after=16; 17 committed → available = -1
        $r = CapacityCalculator::forWorker(20.0, 0.2, 17.0);
        $this->assertSame(-1.0, $r['available']);
    }

    public function testForWorkerOverCommitmentStillCompletesCleanly(): void
    {
        $r = CapacityCalculator::forWorker(10.0, 0.2, 50.0);
        // Negative available is intentional (shown in red, not blocked).
        $this->assertLessThan(0.0, $r['available']);
    }

    public function testForWorkerPrio2DoesNotAppearInCommitted(): void
    {
        // The caller is responsible for only passing prio-1 committed days;
        // this method doesn't filter anything. What we verify here is that
        // a prio-1 value of 0 (because there were no prio-1 tasks) leaves
        // Available unaffected by whatever prio-2 allocations existed.
        $r = CapacityCalculator::forWorker(20.0, 0.2, 0.0);
        $this->assertSame(16.0, $r['available']);
    }

    public function testForWorkerZeroReserveFraction(): void
    {
        $r = CapacityCalculator::forWorker(18.0, 0.0, 3.0);
        $this->assertSame(18.0, $r['after_reserves']);
        $this->assertSame(15.0, $r['available']);
    }

    public function testForWorkerFullReserveFraction(): void
    {
        $r = CapacityCalculator::forWorker(18.0, 1.0, 0.0);
        $this->assertSame(0.0, $r['after_reserves']);
        $this->assertSame(0.0, $r['available']);
    }

    // -------------------------------------------------------------------
    // Day-value validation (spec §3 constraints enforced in PHP)
    // -------------------------------------------------------------------

    /** @return list<array{float,bool}> */
    public static function halfStepCases(): array
    {
        return [
            [0.0,  true],
            [0.5,  true],
            [1.0,  true],
            [2.5,  true],
            [5.0,  true],
            // rejected
            [0.3,  false],   // §10 literal example
            [1.7,  false],   // §10 literal example
            [-0.5, false],   // §10 literal example: negative
            [-1.0, false],
            [5.5,  false],
            [6.0,  false],
            [0.25, false],
            [0.75, false],
        ];
    }

    #[DataProvider('halfStepCases')]
    public function testIsHalfStep(float $v, bool $expected): void
    {
        $this->assertSame($expected, CapacityCalculator::isHalfStep($v, 0.0, 5.0));
    }

    /** @return list<array{float,bool}> */
    public static function rtbStepCases(): array
    {
        return [
            [0.0,  true],
            [0.05, true],
            [0.10, true],
            [0.35, true],
            [1.0,  true],
            // rejected
            [0.03, false],
            [1.1,  false],
            [-0.05, false],
            [0.07, false],
        ];
    }

    #[DataProvider('rtbStepCases')]
    public function testIsRtbStep(float $v, bool $expected): void
    {
        $this->assertSame($expected, CapacityCalculator::isRtbStep($v));
    }
}
