<?php

declare(strict_types=1);

namespace App\Tests\State;

use App\Dto\Contracts\ContractPhase;
use App\Dto\Rtt\EmployeeRttWeekSummary;
use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use App\Enum\ContractNature;
use App\Enum\ContractType;
use App\Enum\TrackingMode;
use App\Service\Contracts\EmployeeContractPhaseResolver;
use App\Service\Exercise\ExerciseYearResolver;
use App\State\EmployeeRttSummaryProvider;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

/**
 * @internal
 */
final class EmployeeRttSummaryProviderTest extends TestCase
{
    // -----------------------------------------------------------------------
    // Phase resolution tests (Task 4 — phaseId support).
    // The repository / service dependencies are typed against final classes
    // which PHPUnit cannot double, so phase resolution is exercised via
    // reflection on private methods to avoid instantiating the full DI graph.
    // -----------------------------------------------------------------------

    public function testResolveTargetPhasePicksH39PhaseFromPhaseId(): void
    {
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases   = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $h39Phase = $phases[1]; // oldest = 39h

        $provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id]);
        $resolved = $this->invokePrivate($provider, 'resolveTargetPhase', $employee);

        self::assertInstanceOf(ContractPhase::class, $resolved);
        self::assertSame($h39Phase->id, $resolved->id);
        self::assertSame(ContractType::H39, $resolved->contractType);
        self::assertFalse($resolved->isCurrent);
    }

    public function testResolveTargetPhaseDefaultsToCurrentPhaseWhenPhaseIdAbsent(): void
    {
        $employee     = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases       = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $currentPhase = $phases[0]; // most recent = FORFAIT

        $provider = $this->buildProvider([]);
        $resolved = $this->invokePrivate($provider, 'resolveTargetPhase', $employee);

        self::assertSame($currentPhase->id, $resolved->id);
        self::assertTrue($resolved->isCurrent);
    }

    public function testPastH39PhaseRttSummaryIsCappedAtPhaseEndDate(): void
    {
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases   = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $h39Phase = $phases[1];

        $provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2026']);
        $year     = $this->invokePrivate($provider, 'resolveYear', $h39Phase);

        self::assertSame(2026, $year);

        // The phase-cap branch in provide() narrows periodTo to the phase end date.
        // Reproduce that logic to validate the resulting window.
        $periodFrom = new DateTimeImmutable('2025-06-01');
        $periodTo   = new DateTimeImmutable('2026-05-31');

        if (!$h39Phase->isCurrent && null !== $h39Phase->endDate && $h39Phase->endDate < $periodTo) {
            $periodTo = $h39Phase->endDate;
        }
        if ($h39Phase->startDate > $periodFrom) {
            $periodFrom = $h39Phase->startDate;
        }

        self::assertSame('2025-06-01', $periodFrom->format('Y-m-d'));
        self::assertSame('2026-04-30', $periodTo->format('Y-m-d'));
    }

    public function testYearOutsidePhaseRangeIsSilentlyClampedToPhaseLastExercise(): void
    {
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases   = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $h39Phase = $phases[1];

        $provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2030']);
        $year     = $this->invokePrivate($provider, 'resolveYear', $h39Phase);

        // Phase ends 2026-04-30 → exercice (Juin-Mai) containing it = 2026.
        self::assertSame(2026, $year);
    }

    public function testYearBeforePhaseIsClampedToPhaseFirstExercise(): void
    {
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases   = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $h39Phase = $phases[1];

        // Phase starts 2020-06-01 → first exercise (Juin-Mai) = 2021.
        $provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id, 'year' => '2010']);
        $year     = $this->invokePrivate($provider, 'resolveYear', $h39Phase);

        self::assertSame(2021, $year);
    }

    public function testInvalidPhaseIdReturns422(): void
    {
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $provider = $this->buildProvider(['phaseId' => '99999']);

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($provider, 'resolveTargetPhase', $employee);
    }

    public function testNonNumericPhaseIdReturns422(): void
    {
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $provider = $this->buildProvider(['phaseId' => 'abc']);

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($provider, 'resolveTargetPhase', $employee);
    }

    public function testDefaultYearForPhaseIdOnClosedPhaseUsesPhaseEndDate(): void
    {
        // No `year` param + explicit phaseId → default year is derived from $phase->endDate.
        // H39 phase ends 2026-04-30 → RTT exercise containing that date = 2026.
        $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases   = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $h39Phase = $phases[1];

        $provider = $this->buildProvider(['phaseId' => (string) $h39Phase->id]);
        $year     = $this->invokePrivate($provider, 'resolveYear', $h39Phase);

        self::assertSame(2026, $year);
    }

    public function testNoQueryParamsKeepsLegacyYearDefaulting(): void
    {
        $employee     = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases       = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $currentPhase = $phases[0];

        $provider = $this->buildProvider([]);
        $year     = $this->invokePrivate($provider, 'resolveYear', $currentPhase);

        // No params → current RTT exercise (Juin N-1 → Mai N). Derive the expectation
        // from today so the test is not pinned to a single calendar date.
        $today    = new DateTimeImmutable('today');
        $expected = (int) $today->format('n') >= 6 ? (int) $today->format('Y') + 1 : (int) $today->format('Y');
        self::assertSame($expected, $year);
    }

    public function testInvalidYearFormatReturns422(): void
    {
        $employee     = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases       = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $currentPhase = $phases[0];

        $provider = $this->buildProvider(['year' => '20XX']);

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($provider, 'resolveYear', $currentPhase);
    }

    public function testYearOutsideBoundsReturns422(): void
    {
        $employee     = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases       = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $currentPhase = $phases[0];

        $provider = $this->buildProvider(['year' => '1900']);

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($provider, 'resolveYear', $currentPhase);
    }

    public function testYearWithoutPhaseIdIsNotClamped(): void
    {
        // No `phaseId` → legacy callers must keep their requested year as-is,
        // even if it falls outside the current phase range.
        $employee     = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $phases       = new EmployeeContractPhaseResolver()->resolvePhases($employee);
        $currentPhase = $phases[0];

        $provider = $this->buildProvider(['year' => '2030']);
        $year     = $this->invokePrivate($provider, 'resolveYear', $currentPhase);

        self::assertSame(2030, $year);
    }

    public function testFlatDeficitWeekIsNotDrainedFromTiers(): void
    {
        $provider = $this->buildProvider([]);

        // Semaine CUSTOM déficitaire (-120), aucune tranche accumulée.
        $weeks  = [$this->weekSummary(-120, true)];
        $result = $this->invokePrivate($provider, 'applyDeficitCascade', $weeks, 0, 0);

        // Buckets restent à 0 ; le total négatif est conservé (le cumul est calculé ailleurs).
        self::assertSame(0, $result[0]->base25Minutes);
        self::assertSame(0, $result[0]->base50Minutes);
        self::assertSame(-120, $result[0]->totalMinutes);
        self::assertTrue($result[0]->isFlatRecovery);
    }

    public function testStandardDeficitWeekDrainsFiftyThenTwentyFive(): void
    {
        $provider = $this->buildProvider([]);

        // Semaine 35h/39h déficitaire (-100), avec 60 en 50% et 120 en 25% accumulés.
        $weeks  = [$this->weekSummary(-100, false)];
        $result = $this->invokePrivate($provider, 'applyDeficitCascade', $weeks, 120, 60);

        self::assertSame(-60, $result[0]->base50Minutes); // 60 drainés du 50%
        self::assertSame(-40, $result[0]->base25Minutes); // 40 restants drainés du 25%
        self::assertSame(-100, $result[0]->totalMinutes);
    }

    public function testFlatPositiveWeekIsUntouched(): void
    {
        $provider = $this->buildProvider([]);

        $weeks  = [$this->weekSummary(180, true)];
        $result = $this->invokePrivate($provider, 'applyDeficitCascade', $weeks, 0, 0);

        self::assertSame(180, $result[0]->totalMinutes);
        self::assertSame(0, $result[0]->base25Minutes);
    }

    // -----------------------------------------------------------------------
    // Test harness helpers.
    // -----------------------------------------------------------------------

    /**
     * Build a two-period employee transitioning from H39 to FORFAIT.
     */
    private function buildEmployeeWithTransition(string $h39Start, string $h39End, string $forfaitStart): Employee
    {
        $employee = new Employee();
        $this->setEntityId($employee, 1);

        $h39Contract = new Contract();
        $h39Contract->setName('39H');
        $h39Contract->setTrackingMode(TrackingMode::TIME->value);
        $h39Contract->setWeeklyHours(39);

        $forfaitContract = new Contract();
        $forfaitContract->setName('Forfait');
        $forfaitContract->setTrackingMode(TrackingMode::PRESENCE->value);
        $forfaitContract->setWeeklyHours(null);

        $h39Period = new EmployeeContractPeriod();
        $this->setEntityId($h39Period, 1);
        $h39Period->setEmployee($employee);
        $h39Period->setContract($h39Contract);
        $h39Period->setStartDate(new DateTimeImmutable($h39Start));
        $h39Period->setEndDate(new DateTimeImmutable($h39End));
        $h39Period->setContractNature(ContractNature::CDI);
        $h39Period->setIsDriver(false);

        $forfaitPeriod = new EmployeeContractPeriod();
        $this->setEntityId($forfaitPeriod, 2);
        $forfaitPeriod->setEmployee($employee);
        $forfaitPeriod->setContract($forfaitContract);
        $forfaitPeriod->setStartDate(new DateTimeImmutable($forfaitStart));
        $forfaitPeriod->setEndDate(null);
        $forfaitPeriod->setContractNature(ContractNature::CDI);
        $forfaitPeriod->setIsDriver(false);

        $employee->getContractPeriods()->add($h39Period);
        $employee->getContractPeriods()->add($forfaitPeriod);

        return $employee;
    }

    private function weekSummary(int $totalMinutes, bool $isFlat, int $base25 = 0, int $base50 = 0): EmployeeRttWeekSummary
    {
        return new EmployeeRttWeekSummary(
            month: 6,
            weekNumber: 1,
            weekStart: '2026-06-01',
            weekEnd: '2026-06-07',
            overtimeMinutes: $totalMinutes,
            base25Minutes: $base25,
            base50Minutes: $base50,
            totalMinutes: $totalMinutes,
            isFlatRecovery: $isFlat,
        );
    }

    /**
     * Build an uninitialized provider with a RequestStack pre-loaded with the given query.
     *
     * The provider's repository/service dependencies are typed against final classes
     * (EmployeeRepository, RttRecoveryComputationService, etc.) which PHPUnit cannot
     * double. We bypass full instantiation by using newInstanceWithoutConstructor and
     * only setting the properties that the tested private methods actually read:
     * `requestStack` and `phaseResolver`.
     *
     * @param array<string, string> $request
     */
    private function buildProvider(array $request = []): EmployeeRttSummaryProvider
    {
        $requestStack = new RequestStack();
        $requestStack->push(new Request(query: $request));

        $reflection = new ReflectionClass(EmployeeRttSummaryProvider::class);
        $provider   = $reflection->newInstanceWithoutConstructor();

        $this->setReadonlyProperty($provider, 'requestStack', $requestStack);
        $this->setReadonlyProperty($provider, 'phaseResolver', new EmployeeContractPhaseResolver());
        $this->setReadonlyProperty($provider, 'exerciseYearResolver', new ExerciseYearResolver());
        $this->setReadonlyProperty($provider, 'rttStartDate', null);

        return $provider;
    }

    private function invokePrivate(object $obj, string $method, mixed ...$args): mixed
    {
        $reflection = new ReflectionClass($obj::class);
        $m          = $reflection->getMethod($method);

        return $m->invoke($obj, ...$args);
    }

    private function setReadonlyProperty(object $obj, string $property, mixed $value): void
    {
        $reflection = new ReflectionProperty($obj::class, $property);
        $reflection->setValue($obj, $value);
    }

    private function setEntityId(object $entity, int $id): void
    {
        $reflection = new ReflectionProperty($entity::class, 'id');
        $reflection->setValue($entity, $id);
    }
}
