<?php

declare(strict_types=1);

namespace App\Tests\Service\Rtt;

use App\Entity\Contract;
use App\Enum\TrackingMode;
use App\Service\Rtt\RttRecoveryComputationService;
use PHPUnit\Framework\TestCase;
use ReflectionClass;

/**
 * The service constructor takes several final-class collaborators that PHPUnit cannot
 * double. Pure helpers are exercised via newInstanceWithoutConstructor + reflection.
 *
 * @internal
 */
final class RttRecoveryComputationServiceTest extends TestCase
{
    public function testResolveWeekAnchorDateReturnsFirstContractedDayWhenWeekStartsBeforeHire(): void
    {
        $service  = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
        $contract = new Contract();

        $weekDays        = ['2026-03-16', '2026-03-17', '2026-03-18', '2026-03-19', '2026-03-20', '2026-03-21', '2026-03-22'];
        $contractsByDate = [
            '2026-03-16' => null,
            '2026-03-17' => null,
            '2026-03-18' => null,
            '2026-03-19' => $contract,
            '2026-03-20' => $contract,
            '2026-03-21' => $contract,
            '2026-03-22' => $contract,
        ];

        $anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);

        self::assertSame('2026-03-19', $anchor);
    }

    public function testResolveWeekAnchorDateReturnsFirstDayWhenItIsContracted(): void
    {
        $service  = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
        $contract = new Contract();

        $weekDays        = ['2026-03-23', '2026-03-24', '2026-03-25'];
        $contractsByDate = [
            '2026-03-23' => $contract,
            '2026-03-24' => $contract,
            '2026-03-25' => $contract,
        ];

        $anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);

        self::assertSame('2026-03-23', $anchor);
    }

    public function testResolveWeekAnchorDateFallsBackToFirstDayWhenNoContract(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $weekDays        = ['2026-03-16', '2026-03-17'];
        $contractsByDate = ['2026-03-16' => null, '2026-03-17' => null];

        $anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate);

        self::assertSame('2026-03-16', $anchor);
    }

    public function testResolveOvertime25BandWidthIs4hForH39(): void
    {
        $service  = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
        $contract = new Contract()->setWeeklyHours(39);

        self::assertSame(4 * 60, $this->invokePrivate($service, 'resolveOvertime25BandWidthMinutes', $contract));
    }

    public function testResolveOvertime25BandWidthIs8hForH35(): void
    {
        $service  = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
        $contract = new Contract()->setWeeklyHours(35);

        self::assertSame(8 * 60, $this->invokePrivate($service, 'resolveOvertime25BandWidthMinutes', $contract));
    }

    /**
     * Dylan Chaboisson, semaine 12 : embauché le jeudi sur un contrat 39h.
     * Total travaillé 22h (1320 min), départ 25 % proraté aux jours contractés = 15h (900 min),
     * plafond 25 %/50 % = 15h + bande 4h = 19h (1140 min). Le plafond se décale avec
     * l'embauche au lieu de rester bloqué à 43h, ouvrant la tranche 50 %.
     */
    public function testMidWeekHireSplitsOvertimeAcross25And50(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        [$base25, $base50] = $this->invokePrivate($service, 'computeOvertimeBaseMinutes', 1320, 900, 1140);

        self::assertSame(4 * 60, $base25);
        self::assertSame(3 * 60, $base50);
    }

    /**
     * Régression : semaine pleine 39h (départ 39h, plafond 43h), 46h travaillées →
     * 4h à 25 % (39→43) et 3h à 50 % (43→46), comportement inchangé.
     */
    public function testFullWeekOvertimeSplitUnchanged(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        [$base25, $base50] = $this->invokePrivate($service, 'computeOvertimeBaseMinutes', 2760, 2340, 2580);

        self::assertSame(4 * 60, $base25);
        self::assertSame(3 * 60, $base50);
    }

    public function testBuildWeekDetailCustomDeficitKeepsSignedTotalAndFlatFlag(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        // CUSTOM, semaine sous les heures : overtime -120 (worked 2h sur réf 4h).
        $detail = $this->invokePrivate(
            $service,
            'buildWeekRecoveryDetail',
            false, // isPresence
            false, // disableBonuses
            true,  // isCustom
            -120,  // overtimeTotalMinutes
            0,     // rawBase25
            0,     // rawBase50
            [],    // dailyMinutes
        );

        self::assertSame(-120, $detail->totalMinutes);
        self::assertTrue($detail->isFlatRecovery);
        self::assertSame(0, $detail->base25Minutes);
        self::assertSame(0, $detail->base50Minutes);
    }

    public function testBuildWeekDetailCustomPositiveIsFlatOneToOne(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $detail = $this->invokePrivate($service, 'buildWeekRecoveryDetail', false, false, true, 180, 0, 0, []);

        self::assertSame(180, $detail->totalMinutes); // 1h = 1h
        self::assertTrue($detail->isFlatRecovery);
    }

    public function testBuildWeekDetailStandardKeepsBucketsAndBonuses(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        // 39h : overtime 300, base25 240, base50 60.
        $detail = $this->invokePrivate($service, 'buildWeekRecoveryDetail', false, false, false, 300, 240, 60, []);

        self::assertFalse($detail->isFlatRecovery);
        self::assertSame(240, $detail->base25Minutes);
        self::assertSame(60, $detail->bonus25Minutes);  // round(240 * 0.25)
        self::assertSame(60, $detail->base50Minutes);
        self::assertSame(30, $detail->bonus50Minutes);  // round(60 * 0.5)
        self::assertSame(300 + 60 + 30, $detail->totalMinutes);
    }

    /**
     * CUSTOM 4h, jour de solidarité non travaillé (RTT posé ou vide) : delta = (attendu − 0) − prorata.
     * attendu lundi = workDaysHours = 120 ; prorata = round(4×12) = 48 ; delta = 120 − 48 = 72.
     * (Combiné au naturel −120 de la semaine, donne −48 min.).
     */
    public function testSolidarityAdjustmentCustomNotWorkedNeutralisesToProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate(
            $service,
            'computeSolidarityDeficitAdjustment',
            self::customContract(4),
            120, // expectedMinutes (workDaysHours du lundi)
            0,   // workedMinutes (RTT posé / vide)
        );

        self::assertSame(72, $delta);
    }

    /**
     * CUSTOM 4h, jour de solidarité travaillé normalement (120) : delta = (120 − 120) − 48 = −48.
     */
    public function testSolidarityAdjustmentCustomWorkedNormallyChargesProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 120, 120);

        self::assertSame(-48, $delta);
    }

    /**
     * CUSTOM 4h, jour de solidarité travaillé en plus (240) : delta = (120 − 240) − 48 = −168.
     * Le surplus du jour de solidarité n'est PAS crédité (jour neutralisé, net forcé à −prorata).
     */
    public function testSolidarityAdjustmentCustomWorkedExtraStillNetsProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 120, 240);

        self::assertSame(-168, $delta);
    }

    /**
     * CUSTOM 4h NE travaillant PAS le jour de solidarité (lundi non planifié, ex. Nadia Mar+Ven) :
     * workDaysHours[lundi] absent → expected = 0. Le jour de solidarité ne la concerne pas → delta 0,
     * aucun déficit imputé. C'est la correction du bug : (0 − 0) − 48 ne doit PAS donner −48.
     */
    public function testSolidarityAdjustmentCustomNotScheduledThatDayIsZero(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 0, 0);

        self::assertSame(0, $delta);
    }

    /**
     * CUSTOM 28h : prorata = round(28×12) = 336 (5h36). worked 0, expected 336 → delta 0.
     * Le delta est nul ici par coïncidence du fallback uniforme (expected = prorata) ; avec un vrai
     * workDaysHours où la valeur du lundi diffère, expected ≠ prorata et le delta serait non nul.
     */
    public function testSolidarityAdjustmentCustom28hUsesProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(28), 336, 0);

        self::assertSame(0, $delta);
    }

    /**
     * CUSTOM ≥ 35h (36h) : hors périmètre → delta 0.
     */
    public function testSolidarityAdjustmentCustom36hOutOfScope(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(36), 999, 0);

        self::assertSame(0, $delta);
    }

    /**
     * 35h : type H35 (pas CUSTOM) → delta 0 (comportement inchangé, RTT posé fait foi).
     */
    public function testSolidarityAdjustment35hOutOfScope(): void
    {
        $service  = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
        $contract = new Contract()->setName('35h')->setTrackingMode(TrackingMode::TIME)->setWeeklyHours(35);

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', $contract, 420, 0);

        self::assertSame(0, $delta);
    }

    /**
     * Aucun contrat ce jour-là (salarié parti / pas encore embauché) → delta 0.
     */
    public function testSolidarityAdjustmentNoContractIsZero(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', null, 0, 0);

        self::assertSame(0, $delta);
    }

    private static function customContract(int $weeklyHours): Contract
    {
        return new Contract()
            ->setName('Temps partiel')
            ->setTrackingMode(TrackingMode::TIME)
            ->setWeeklyHours($weeklyHours)
        ;
    }

    private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
    {
        return new ReflectionClass($obj::class)->getMethod($method)->invoke($obj, ...$args);
    }
}
