<?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\Entity\WorkHour;
use App\Enum\HalfDay;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Repository\EmployeeWeekCommentRepository;
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\NightHoursCalculator;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
use App\State\WorkHourWeeklySummaryProvider;
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;

/**
 * @internal
 */
final class WorkHourWeeklySummaryProviderTest extends TestCase
{
    private Security $security;
    private EmployeeScopedRepositoryInterface $employeeRepository;
    private WorkHourReadRepositoryInterface $workHourRepository;
    private AbsenceReadRepositoryInterface $absenceRepository;
    private RequestStack $requestStack;

    protected function setUp(): void
    {
        $this->security           = $this->createStub(Security::class);
        $this->employeeRepository = $this->createStub(EmployeeScopedRepositoryInterface::class);
        $this->workHourRepository = $this->createStub(WorkHourReadRepositoryInterface::class);
        $this->absenceRepository  = $this->createStub(AbsenceReadRepositoryInterface::class);
        $this->requestStack       = new RequestStack();
    }

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

        $provider = new WorkHourWeeklySummaryProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->workHourRepository,
            $this->absenceRepository,
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildResolverStub(),
            new DailyReferenceMinutesResolver(),
            $this->buildHolidayResolver(),
            $this->buildHolidayService(),
            $this->buildWeekCommentRepoStub(),
            new NightHoursCalculator(),
        );

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

    public function testBuildsWeeklyRowsWithOvertimeAndPresence(): void
    {
        $user             = new User();
        $timeEmployee     = $this->buildEmployee(1, 'TIME', 35, 'Alice');
        $presenceEmployee = $this->buildEmployee(2, 'PRESENCE', null, 'Bob');
        $interimEmployee  = $this->buildEmployee(3, 'TIME', 35, 'Charly', 'Interim');
        $employees        = [$timeEmployee, $presenceEmployee, $interimEmployee];

        $workHours = [];
        foreach (['2026-02-16', '2026-02-17', '2026-02-18', '2026-02-19', '2026-02-20'] as $date) {
            $workHours[] = new WorkHour()
                ->setEmployee($timeEmployee)
                ->setWorkDate(new DateTimeImmutable($date))
                ->setMorningFrom('09:00')
                ->setMorningTo('19:00')
            ;
            $workHours[] = new WorkHour()
                ->setEmployee($interimEmployee)
                ->setWorkDate(new DateTimeImmutable($date))
                ->setMorningFrom('09:00')
                ->setMorningTo('19:00')
            ;
        }

        $absenceType = new AbsenceType()
            ->setCode('CP')
            ->setLabel('Congé')
            ->setColor('#000')
            ->setCountAsWorkedHours(true)
        ;
        $presenceAbsence = new Absence()
            ->setEmployee($presenceEmployee)
            ->setType($absenceType)
            ->setStartDate(new DateTime('2026-02-16'))
            ->setEndDate(new DateTime('2026-02-16'))
            ->setStartHalf(HalfDay::AM)
            ->setEndHalf(HalfDay::PM)
        ;

        $this->requestStack->push(new Request(query: ['weekStart' => '2026-02-16']));
        $this->security->method('getUser')->willReturn($user);
        $this->employeeRepository->method('findScoped')->with($user)->willReturn($employees);
        $this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn($workHours);
        $this->absenceRepository->method('findForPrint')->willReturn([$presenceAbsence]);

        $provider = new WorkHourWeeklySummaryProvider(
            $this->security,
            $this->requestStack,
            $this->employeeRepository,
            $this->workHourRepository,
            $this->absenceRepository,
            new AbsenceSegmentsResolver(),
            new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
            $this->buildWeeklyResolverStub($employees),
            new DailyReferenceMinutesResolver(),
            $this->buildHolidayResolver(),
            $this->buildHolidayService(),
            $this->buildWeekCommentRepoStub(),
            new NightHoursCalculator(),
        );

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

        self::assertSame('2026-02-16', $result->weekStart);
        self::assertSame('2026-02-22', $result->weekEnd);
        self::assertCount(3, $result->rows);

        self::assertSame(3000, $result->rows[0]->weeklyTotalMinutes);
        self::assertSame(900, $result->rows[0]->weeklyOvertimeTotalMinutes);
        self::assertSame(120, $result->rows[0]->weeklyOvertime25Minutes);
        self::assertSame(210, $result->rows[0]->weeklyOvertime50Minutes);
        self::assertSame(1230, $result->rows[0]->weeklyRecoveryMinutes);

        self::assertSame(0.0, $result->rows[1]->weeklyPresenceCount);
        self::assertTrue($result->rows[1]->daily[0]->hasAbsence);
        self::assertSame('Congé', $result->rows[1]->daily[0]->absenceLabel);
        self::assertSame('#000', $result->rows[1]->daily[0]->absenceColor);
        self::assertSame(0, $result->rows[1]->weeklyOvertimeTotalMinutes);
        self::assertSame(0, $result->rows[2]->weeklyOvertime25Minutes);
        self::assertSame(0, $result->rows[2]->weeklyOvertime50Minutes);
        self::assertSame(0, $result->rows[2]->weeklyRecoveryMinutes);
        self::assertSame(900, $result->rows[2]->weeklyOvertimeTotalMinutes);
    }

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

        return $employee;
    }

    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 buildWeekCommentRepoStub(): EmployeeWeekCommentRepository
    {
        $r = $this->createStub(EmployeeWeekCommentRepository::class);
        $r->method('findByWeekAndEmployees')->willReturn([]);

        return $r;
    }

    private function buildHolidayResolver(array $holidayMap = []): HolidayVirtualHoursResolver
    {
        return new HolidayVirtualHoursResolver(
            new DailyReferenceMinutesResolver(),
            $this->buildHolidayService($holidayMap),
            $this->createStub(EmployeeContractResolver::class),
        );
    }

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

        return $service;
    }

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

        return $resolver;
    }

    /**
     * @param list<Employee> $employees
     */
    private function buildWeeklyResolverStub(array $employees): EmployeeContractResolver
    {
        $resolver = $this->createStub(EmployeeContractResolver::class);
        $resolver
            ->method('resolveForEmployeeAndDate')
            ->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract())
        ;
        $resolver
            ->method('resolveForEmployeesAndDays')
            ->willReturnCallback(static function (array $scopedEmployees, array $days): array {
                $map = [];
                foreach ($scopedEmployees as $employee) {
                    $employeeId = $employee->getId();
                    if (!$employeeId) {
                        continue;
                    }
                    foreach ($days as $day) {
                        $map[$employeeId][$day] = $employee->getContract();
                    }
                }

                return $map;
            })
        ;

        return $resolver;
    }
}
