<?php

declare(strict_types=1);

namespace App\Tests\State;

use App\Entity\Absence;
use App\Entity\AbsenceType;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use App\Enum\HalfDay;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\State\SalaryRecapPrintProvider;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionProperty;

/**
 * Forfait N-1 split for the salary recap. The provider's collaborators are final classes
 * PHPUnit cannot double, so the pure split helper is exercised via reflection, with a real
 * AbsenceSegmentsResolver (no deps) injected into the uninitialized property.
 *
 * @internal
 */
final class SalaryRecapPrintProviderTest extends TestCase
{
    public function testN1BudgetPartiallyCoversADayAndOverflowsToN(): void
    {
        // Budget N-1 = 2.5 j ; 3 congés pleins (1 j) lun/mar/mer de janvier.
        // 1.0 + 1.0 + 0.5 consommés en N-1 → reste 0.5 j affiché en congé (le mercredi).
        $conges = [
            $this->buildConge('2026-01-05'),
            $this->buildConge('2026-01-06'),
            $this->buildConge('2026-01-07'),
        ];

        $result = $this->split($conges, 2.5, '2026-01-01', '2026-01-31');

        self::assertSame(2.5, $result['n1PresenceDays']);
        self::assertSame(0.5, $result['count']);
        self::assertSame('07/01', $result['dates']);
    }

    public function testN1BudgetConsumedInPriorMonthLeavesCurrentMonthFullyDisplayed(): void
    {
        // Budget 1 j, consommé par le congé de janvier. Récap de février → le congé de février
        // est entièrement imputé N (affiché, 0 présence N-1 dans le mois).
        $conges = [
            $this->buildConge('2026-01-12'),
            $this->buildConge('2026-02-09'),
        ];

        $result = $this->split($conges, 1.0, '2026-02-01', '2026-02-28');

        self::assertSame(0.0, $result['n1PresenceDays']);
        self::assertSame(1.0, $result['count']);
        self::assertSame('09/02', $result['dates']);
    }

    public function testZeroBudgetDisplaysAllCongesInMonth(): void
    {
        $conges = [$this->buildConge('2026-03-03')];

        $result = $this->split($conges, 0.0, '2026-03-01', '2026-03-31');

        self::assertSame(0.0, $result['n1PresenceDays']);
        self::assertSame(1.0, $result['count']);
        self::assertSame('03/03', $result['dates']);
    }

    public function testTerminatedContractExcludedFromMonth(): void
    {
        // Marine : contrat terminé le 26/02 → absente du récap de juin.
        $employee = $this->buildEmployeeWithPeriod('2025-02-10', '2026-02-26');

        self::assertFalse($this->hasInRange($employee, '2026-06-01', '2026-06-30'));
    }

    public function testOngoingContractIncluded(): void
    {
        $employee = $this->buildEmployeeWithPeriod('2025-01-01', null);

        self::assertTrue($this->hasInRange($employee, '2026-06-01', '2026-06-30'));
    }

    public function testContractEndingOnFromDayIncluded(): void
    {
        $employee = $this->buildEmployeeWithPeriod('2025-01-01', '2026-06-01');

        self::assertTrue($this->hasInRange($employee, '2026-06-01', '2026-06-30'));
    }

    public function testNoPeriodsExcluded(): void
    {
        self::assertFalse($this->hasInRange(new Employee(), '2026-06-01', '2026-06-30'));
    }

    public function testSundayCongeIsNotCounted(): void
    {
        // Congé (C) posé un dimanche (2026-06-07) : ne doit pas compter comme congé pris.
        $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-07', 'C')], ['C']);

        self::assertSame(0.0, $result['count']);
        self::assertSame('', $result['dates']);
    }

    public function testSaturdayCongeStillCounted(): void
    {
        // Le samedi reste hors périmètre (budget samedis dédié) : congé samedi toujours compté.
        $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-06', 'C')], ['C']);

        self::assertSame(1.0, $result['count']);
        self::assertSame('06/06', $result['dates']);
    }

    public function testWeekdayCongeCounted(): void
    {
        $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-01', 'C')], ['C']);

        self::assertSame(1.0, $result['count']);
        self::assertSame('01/06', $result['dates']);
    }

    public function testSundayMaladieStillCounted(): void
    {
        // L'exclusion du dimanche ne concerne que les congés (C) : maladie/AT inchangés.
        $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-07', 'M')], ['M', 'AT']);

        self::assertSame(1.0, $result['count']);
        self::assertSame('07/06', $result['dates']);
    }

    /**
     * @param list<Absence> $absences
     * @param list<string>  $codes
     *
     * @return array{count: float, dates: string}
     */
    private function countByCode(array $absences, array $codes): array
    {
        $provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor();

        return new ReflectionClass($provider::class)
            ->getMethod('countAbsencesByCode')
            ->invoke($provider, $absences, $codes)
        ;
    }

    private function buildAbsenceWithCode(string $date, string $code): Absence
    {
        return new Absence()
            ->setType(new AbsenceType()->setCode($code)->setLabel($code)->setColor('#000'))
            ->setStartDate(new DateTime($date))
            ->setEndDate(new DateTime($date))
            ->setStartHalf(HalfDay::AM)
            ->setEndHalf(HalfDay::PM)
        ;
    }

    private function hasInRange(Employee $employee, string $from, string $to): bool
    {
        $provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor();

        return new ReflectionClass($provider::class)
            ->getMethod('hasContractInRange')
            ->invoke($provider, $employee, new DateTimeImmutable($from), new DateTimeImmutable($to))
        ;
    }

    private function buildEmployeeWithPeriod(string $start, ?string $end): Employee
    {
        $employee = new Employee();
        $period   = new EmployeeContractPeriod();
        $period->setEmployee($employee);
        $period->setStartDate(new DateTimeImmutable($start));
        $period->setEndDate(null !== $end ? new DateTimeImmutable($end) : null);
        $employee->getContractPeriods()->add($period);

        return $employee;
    }

    /**
     * @param list<Absence> $conges
     *
     * @return array{count: float, dates: string, n1PresenceDays: float}
     */
    private function split(array $conges, float $budget, string $from, string $to): array
    {
        $provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor();
        new ReflectionProperty(SalaryRecapPrintProvider::class, 'absenceSegmentsResolver')
            ->setValue($provider, new AbsenceSegmentsResolver())
        ;

        return new ReflectionClass($provider::class)
            ->getMethod('splitForfaitCongesByN1')
            ->invoke($provider, $conges, $budget, new DateTimeImmutable($from), new DateTimeImmutable($to))
        ;
    }

    private function buildConge(string $date): Absence
    {
        return new Absence()
            ->setStartDate(new DateTime($date))
            ->setEndDate(new DateTime($date))
            ->setStartHalf(HalfDay::AM)
            ->setEndHalf(HalfDay::PM)
        ;
    }
}
