<?php

declare(strict_types=1);

namespace App\Tests\Functional\Command;

use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Infrastructure\Command\AccrueLeaveCommand;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsenceBalanceRepository;
use App\Module\Core\Domain\Entity\User;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

/**
 * Covers the period roll-over: when a new reference period opens, the previous
 * period's "en cours d'acquisition" (N) becomes the new "acquired" (N-1), but
 * only for the days that were not already taken.
 *
 * @internal
 */
class AccrueLeaveCommandTest extends KernelTestCase
{
    private EntityManagerInterface $em;
    private DoctrineAbsenceBalanceRepository $balances;

    protected function setUp(): void
    {
        self::bootKernel();
        $this->em       = self::getContainer()->get(EntityManagerInterface::class);
        $this->balances = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class);
    }

    /**
     * Tristan's real case: 9.75 accruing, 1 day taken, nothing previously
     * acquired → the day taken eats into the carry-over, so 8.75 rolls over
     * (not 9.75).
     */
    public function testCarryOverDeductsTakenDays(): void
    {
        $user = $this->createEmployee();
        $this->seedPreviousBalance($user, acquired: 0.0, acquiring: 9.75, taken: 1.0);

        $this->runForJune2026();

        $rolled = $this->balances->findOneForPeriod($user, AbsenceType::PaidLeave, '2026-2027');
        self::assertNotNull($rolled);
        self::assertEqualsWithDelta(8.75, $rolled->getAcquired(), 0.0001);
    }

    /**
     * Leave is charged oldest-first: the 3 days taken come out of the expiring
     * N-2 "acquired" bucket (5), so the full 10 accruing days carry over intact.
     */
    public function testCarryOverChargesOldestBucketFirst(): void
    {
        $user = $this->createEmployee();
        $this->seedPreviousBalance($user, acquired: 5.0, acquiring: 10.0, taken: 3.0);

        $this->runForJune2026();

        $rolled = $this->balances->findOneForPeriod($user, AbsenceType::PaidLeave, '2026-2027');
        self::assertNotNull($rolled);
        self::assertEqualsWithDelta(10.0, $rolled->getAcquired(), 0.0001);
    }

    /** No day taken → the whole accruing bucket carries over. */
    public function testFullCarryOverWhenNothingTaken(): void
    {
        $user = $this->createEmployee();
        $this->seedPreviousBalance($user, acquired: 0.0, acquiring: 10.0, taken: 0.0);

        $this->runForJune2026();

        $rolled = $this->balances->findOneForPeriod($user, AbsenceType::PaidLeave, '2026-2027');
        self::assertNotNull($rolled);
        self::assertEqualsWithDelta(10.0, $rolled->getAcquired(), 0.0001);
    }

    private function createEmployee(): User
    {
        $user = new User();
        $user->setUsername('accrue-test-'.uniqid());
        $user->setPassword('x');
        $user->setRoles(['ROLE_USER']);
        $user->setIsEmployee(true);
        $user->setHireDate(new DateTimeImmutable('2024-01-01'));
        $user->setReferencePeriodStart('06-01');
        $user->setAnnualLeaveDays(25.0);
        $user->setWorkTimeRatio(1.0);
        $user->setInitialLeaveBalance(0.0);
        $this->em->persist($user);
        $this->em->flush();

        return $user;
    }

    private function seedPreviousBalance(User $user, float $acquired, float $acquiring, float $taken): void
    {
        $balance = new AbsenceBalance();
        $balance->setUser($user);
        $balance->setType(AbsenceType::PaidLeave);
        $balance->setPeriod('2025-2026');
        $balance->setAcquired($acquired);
        $balance->setAcquiring($acquiring);
        $balance->setTaken($taken);
        $this->em->persist($balance);
        $this->em->flush();
    }

    private function runForJune2026(): void
    {
        $command = self::getContainer()->get(AccrueLeaveCommand::class);
        $tester  = new CommandTester($command);
        $tester->execute(['--month' => '2026-06']);
        self::assertSame(0, $tester->getStatusCode());
    }
}
