<?php

declare(strict_types=1);

namespace App\Tests\State;

use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use App\Entity\EmployeeRttBalance;
use App\Enum\ContractNature;
use App\Enum\TrackingMode;
use App\Service\Contracts\EmployeeContractPhaseResolver;
use App\Service\Exercise\ExerciseYearResolver;
use App\State\EmployeeRttPaymentProcessor;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

/**
 * @internal
 *
 * Exercises the year-acceptance guard of EmployeeRttPaymentProcessor.
 *
 * The processor depends on final repositories (EmployeeRepository,
 * EmployeeRttPaymentRepository) which PHPUnit cannot double. The guard logic
 * lives in a private helper (assertYearAllowedForPayment) tested directly via
 * reflection — same pattern used in EmployeeRttSummaryProviderTest.
 */
final class EmployeeRttPaymentProcessorTest extends TestCase
{
    public function testPaymentAllowedOnCurrentExercise(): void
    {
        // Today = 2026-05-19 (env clock) → current exercise = 2026.
        $employee  = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));

        $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2026);

        // No exception → guard accepts current exercise.
        self::assertTrue(true);
    }

    public function testPaymentAllowedOnLastExerciseOfClosedPhase(): void
    {
        // Phase 39h closed 2026-04-30, FORFAIT from 2026-05-01.
        // Exercise 2026 (Juin 2025 → Mai 2026) contains the H39 phase end date.
        // Payment must be allowed on exercise 2026 even when current exercise is 2027.
        $employee  = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2027-01-15'));

        $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2026);

        self::assertTrue(true);
    }

    public function testPaymentRejectedOnEarlierExerciseOfClosedPhase(): void
    {
        $employee  = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2027-01-15'));

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2024);
    }

    public function testPaymentRejectedOnFutureExercise(): void
    {
        $employee  = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2030);
    }

    public function testPaymentAllowedOnPreviousExercise(): void
    {
        // Today = 2026-05-19 → current exercise = 2026. Retroactive payment on the
        // immediately previous exercise (2025) is now allowed (Option B).
        $employee  = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));

        $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2025);

        // No exception → previous exercise accepted.
        self::assertTrue(true);
    }

    public function testPaymentStillRejectedTwoExercisesBack(): void
    {
        // 2024 is two exercises before current (2026) and not a closed-phase end → still rejected.
        $employee  = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01');
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2024);
    }

    public function testRetroactivePaymentRefusedWhenDownstreamReportLocked(): void
    {
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));

        $locked = new EmployeeRttBalance();
        $locked->setIsLocked(true);

        $this->expectException(UnprocessableEntityHttpException::class);
        $this->invokePrivate($processor, 'assertReportNotLocked', $locked);
    }

    public function testRetroactivePaymentAllowedWhenDownstreamReportMissingOrUnlocked(): void
    {
        $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19'));

        $unlocked = new EmployeeRttBalance();
        $unlocked->setIsLocked(false);

        // Neither a missing (null) nor an unlocked downstream report must block payment.
        $this->invokePrivate($processor, 'assertReportNotLocked', null);
        $this->invokePrivate($processor, 'assertReportNotLocked', $unlocked);

        self::assertTrue(true);
    }

    // -----------------------------------------------------------------------
    // 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;
    }

    /**
     * Build an uninitialized processor with a fixed clock. The repositories are
     * declared on final classes that PHPUnit cannot double, so we bypass full
     * instantiation via newInstanceWithoutConstructor and only seed the
     * properties the tested private guard reads: phaseResolver + clock.
     */
    private function buildProcessorWithClock(DateTimeImmutable $today): EmployeeRttPaymentProcessor
    {
        $reflection = new ReflectionClass(EmployeeRttPaymentProcessor::class);
        $processor  = $reflection->newInstanceWithoutConstructor();

        $clock = new readonly class($today) implements ClockInterface {
            public function __construct(private DateTimeImmutable $now) {}

            public function now(): DateTimeImmutable
            {
                return $this->now;
            }
        };

        $this->setReadonlyProperty($processor, 'phaseResolver', new EmployeeContractPhaseResolver());
        $this->setReadonlyProperty($processor, 'clock', $clock);
        $this->setReadonlyProperty($processor, 'exerciseYearResolver', new ExerciseYearResolver());

        return $processor;
    }

    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);
    }
}
