<?php

declare(strict_types=1);

namespace App\Tests\State;

use ApiPlatform\Metadata\Get;
use App\Entity\Absence;
use App\Entity\AbsenceType;
use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\User;
use App\Enum\ContractNature;
use App\Enum\HalfDay;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\FormationReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\PublicHolidayServiceInterface;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\DailyReferenceMinutesResolver;
use App\Service\WorkHours\HolidayVirtualHoursResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use App\State\WorkHourDayContextProvider;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

/**
 * @internal
 */
final class WorkHourDayContextProviderTest extends TestCase
{
    private Security $security;
    private EmployeeScopedRepositoryInterface $employeeRepository;
    private AbsenceReadRepositoryInterface $absenceRepository;
    private FormationReadRepositoryInterface $formationRepository;
    private RequestStack $requestStack;

    protected function setUp(): void
    {
        $this->security            = $this->createStub(Security::class);
        $this->employeeRepository  = $this->createStub(EmployeeScopedRepositoryInterface::class);
        $this->absenceRepository   = $this->createStub(AbsenceReadRepositoryInterface::class);
        $this->formationRepository = $this->createStub(FormationReadRepositoryInterface::class);
        $this->formationRepository->method('findByDateAndEmployees')->willReturn([]);
        $this->requestStack = new RequestStack();
    }

    public function testThrowsWhenAnonymous(): void
    {
        $this->security->method('getUser')->willReturn(null);

        $provider = new WorkHourDayContextProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->absenceRepository,
            $this->formationRepository,
            $this->buildResolverStub(),
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildHolidayResolver(),
        );

        $this->expectException(AccessDeniedHttpException::class);
        $provider->provide(new Get());
    }

    public function testThrowsWhenDateFormatInvalid(): void
    {
        $this->requestStack->push(new Request(query: ['workDate' => '16-02-2026']));
        $this->security->method('getUser')->willReturn(new User());

        $provider = new WorkHourDayContextProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->absenceRepository,
            $this->formationRepository,
            $this->buildResolverStub(),
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildHolidayResolver(),
        );

        $this->expectException(UnprocessableEntityHttpException::class);
        $provider->provide(new Get());
    }

    public function testBuildsRowsWithAbsenceCredits(): void
    {
        $user     = new User();
        $employee = $this->buildEmployee(1, Contract::TRACKING_TIME, 35);
        $absence  = $this->buildAbsence($employee, '2026-02-16', '2026-02-16', true);

        $this->requestStack->push(new Request(query: ['workDate' => '2026-02-16']));
        $this->security->method('getUser')->willReturn($user);
        $this->employeeRepository->method('findScoped')->with($user)->willReturn([$employee]);
        $this->absenceRepository->method('findByDateAndEmployees')->willReturn([$absence]);

        $provider = new WorkHourDayContextProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->absenceRepository,
            $this->formationRepository,
            $this->buildResolverStub(),
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildHolidayResolver(),
        );

        $result = $provider->provide(new Get());

        self::assertSame('2026-02-16', $result->workDate);
        self::assertCount(1, $result->rows);
        self::assertSame(1, $result->rows[0]['employeeId']);
        self::assertSame('Maladie', $result->rows[0]['absenceLabel']);
        self::assertSame('AM', $result->rows[0]['absenceHalf']);
        self::assertSame(210, $result->rows[0]['creditedMinutes']);
    }

    public function testRowCarriesContractAtRequestedDate(): void
    {
        $user = new User();

        $timeContract = new Contract()
            ->setName('Contrat')
            ->setTrackingMode(Contract::TRACKING_TIME)
            ->setWeeklyHours(39)
        ;
        $forfaitContract = new Contract()
            ->setName('Forfait')
            ->setTrackingMode(Contract::TRACKING_PRESENCE)
            ->setWeeklyHours(null)
        ;
        $employee = new Employee()
            ->setFirstName('Jean')
            ->setLastName('Test')
            ->setContract($forfaitContract)
        ;
        $this->setEntityId($employee, 1);

        // Resolver renvoie le contrat 39h avant 2026-03-01, le forfait à partir de cette date.
        $resolver = $this->createStub(EmployeeContractResolver::class);
        $resolver->method('resolveForEmployeeAndDate')->willReturnCallback(
            static fn (Employee $e, DateTimeImmutable $d): ?Contract => $d < new DateTimeImmutable('2026-03-01') ? $timeContract : $forfaitContract
        );
        $resolver->method('resolveNatureForEmployeeAndDate')->willReturn(ContractNature::CDI);

        $this->requestStack->push(new Request(query: ['workDate' => '2026-02-16']));
        $this->security->method('getUser')->willReturn($user);
        $this->employeeRepository->method('findScoped')->with($user)->willReturn([$employee]);
        $this->absenceRepository->method('findByDateAndEmployees')->willReturn([]);

        $provider = new WorkHourDayContextProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->absenceRepository,
            $this->formationRepository,
            $resolver,
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildHolidayResolver(),
        );

        $row = $provider->provide(new Get())->rows[0];

        self::assertSame('TIME', $row['trackingMode']);
        self::assertSame(39, $row['weeklyHours']);
        self::assertSame('39H', $row['contractType']);
        self::assertSame('Contrat', $row['contractName']);
    }

    public function testRowCarriesWorkDaysHoursForCustomContract(): void
    {
        $user     = new User();
        $employee = $this->buildEmployee(1, Contract::TRACKING_TIME, 4);

        $resolver = $this->createStub(EmployeeContractResolver::class);
        $resolver->method('resolveForEmployeeAndDate')->willReturn($employee->getContract());
        $resolver->method('resolveNatureForEmployeeAndDate')->willReturn(ContractNature::CDI);
        // Contrat 4h travaillé le lundi et le vendredi (120 min chacun).
        $resolver->method('resolveWorkDaysMinutesForEmployeeAndDate')->willReturn([1 => 120, 5 => 120]);

        $this->requestStack->push(new Request(query: ['workDate' => '2026-02-16']));
        $this->security->method('getUser')->willReturn($user);
        $this->employeeRepository->method('findScoped')->with($user)->willReturn([$employee]);
        $this->absenceRepository->method('findByDateAndEmployees')->willReturn([]);

        $provider = new WorkHourDayContextProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->absenceRepository,
            $this->formationRepository,
            $resolver,
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildHolidayResolver(),
        );

        $row = $provider->provide(new Get())->rows[0];

        self::assertSame([1 => 120, 5 => 120], $row['workDaysHours']);
    }

    public function testRowHasNullWorkDaysHoursForStandardContract(): void
    {
        $user     = new User();
        $employee = $this->buildEmployee(1, Contract::TRACKING_TIME, 35);

        // buildResolverStub ne stube pas resolveWorkDaysMinutesForEmployeeAndDate → null (35h n'a pas de planning).
        $this->requestStack->push(new Request(query: ['workDate' => '2026-02-16']));
        $this->security->method('getUser')->willReturn($user);
        $this->employeeRepository->method('findScoped')->with($user)->willReturn([$employee]);
        $this->absenceRepository->method('findByDateAndEmployees')->willReturn([]);

        $provider = new WorkHourDayContextProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->absenceRepository,
            $this->formationRepository,
            $this->buildResolverStub(),
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildHolidayResolver(),
        );

        $row = $provider->provide(new Get())->rows[0];

        self::assertNull($row['workDaysHours']);
    }

    private function buildEmployee(int $id, string $trackingMode, ?int $weeklyHours): Employee
    {
        $contract = new Contract()
            ->setName('Contrat')
            ->setTrackingMode($trackingMode)
            ->setWeeklyHours($weeklyHours)
        ;
        $employee = new Employee()
            ->setFirstName('Jean')
            ->setLastName('Test')
            ->setContract($contract)
        ;
        $this->setEntityId($employee, $id);

        return $employee;
    }

    private function buildAbsence(Employee $employee, string $startDate, string $endDate, bool $countAsWorked): Absence
    {
        $type = new AbsenceType()
            ->setCode('MAL')
            ->setLabel('Maladie')
            ->setColor('#f00')
            ->setCountAsWorkedHours($countAsWorked)
        ;

        return new Absence()
            ->setEmployee($employee)
            ->setType($type)
            ->setStartDate(new DateTime($startDate))
            ->setEndDate(new DateTime($endDate))
            ->setStartHalf(HalfDay::AM)
            ->setEndHalf(HalfDay::AM)
        ;
    }

    private function setEntityId(object $entity, int $id): void
    {
        $reflection = new ReflectionObject($entity);
        $property   = $reflection->getProperty('id');
        $property->setAccessible(true);
        $property->setValue($entity, $id);
    }

    private function buildResolverStub(): EmployeeContractResolver
    {
        $resolver = $this->createStub(EmployeeContractResolver::class);
        $resolver
            ->method('resolveForEmployeeAndDate')
            ->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract())
        ;
        $resolver
            ->method('resolveNatureForEmployeeAndDate')
            ->willReturn(ContractNature::CDI)
        ;

        return $resolver;
    }

    private function buildHolidayResolver(): HolidayVirtualHoursResolver
    {
        $service = $this->createStub(PublicHolidayServiceInterface::class);
        $service->method('getHolidaysDayByYears')->willReturn([]);

        return new HolidayVirtualHoursResolver(
            new DailyReferenceMinutesResolver(),
            $service,
            $this->createStub(EmployeeContractResolver::class),
        );
    }
}
