<?php

declare(strict_types=1);

namespace App\Tests\Functional\Mcp;

use App\Module\Absence\Application\Service\AbsenceBalanceService;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Entity\AbsencePolicy;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Domain\Service\AbsenceDayCalculator;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsenceBalanceRepository;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsencePolicyRepository;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsenceRequestRepository;
use App\Module\Absence\Infrastructure\Mcp\Tool\CancelAbsenceRequestTool;
use App\Module\Absence\Infrastructure\Mcp\Tool\CreateAbsenceRequestTool;
use App\Module\Absence\Infrastructure\Mcp\Tool\ReviewAbsenceRequestTool;
use App\Module\Core\Domain\Entity\User;
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;

/**
 * @internal
 */
class AbsenceRequestLifecycleTest extends KernelTestCase
{
    private EntityManagerInterface $em;
    private User $employee;
    private User $admin;

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

        // Employé dédié au test.
        $this->employee = new User();
        $this->employee->setUsername('mcp-test-employee-'.uniqid());
        $this->employee->setPassword('x');
        $this->employee->setRoles(['ROLE_USER']);
        $this->employee->setIsEmployee(true);
        $this->employee->setReferencePeriodStart('06-01');
        $this->em->persist($this->employee);

        $this->admin = new User();
        $this->admin->setUsername('mcp-test-admin-'.uniqid());
        $this->admin->setPassword('x');
        $this->admin->setRoles(['ROLE_ADMIN']);
        $this->em->persist($this->admin);

        // Policy CP active (créée si absente — findOneByType peut déjà exister via fixtures).
        $policy = $this->em->getRepository(AbsencePolicy::class)->findOneBy(['type' => AbsenceType::PaidLeave]);
        if (null === $policy) {
            $policy = new AbsencePolicy();
            $policy->setType(AbsenceType::PaidLeave);
            $policy->setCountWorkingDaysOnly(true);
            $policy->setActive(true);
            $this->em->persist($policy);
        } else {
            $policy->setActive(true);
        }

        // Entitlement so CP requests can be approved without breaching the
        // no-negative-balance guard (period of the June 2026 test requests).
        $balance = new AbsenceBalance();
        $balance->setUser($this->employee);
        $balance->setType(AbsenceType::PaidLeave);
        $balance->setPeriod('2026-2027');
        $balance->setAcquired(25.0);
        $this->em->persist($balance);

        $this->em->flush();
    }

    public function testCreateReservesPendingDays(): void
    {
        $tool = $this->createTool($this->admin);

        // Lundi 2026-06-01 → vendredi 2026-06-05 = 5 jours ouvrés.
        $json = ($tool)(
            $this->employee->getId(),
            'cp',
            '2026-06-01',
            '2026-06-05',
        );
        $data = json_decode($json, true);

        self::assertSame('pending', $data['status']);
        // JSON ne préserve pas le type float pour un entier (5.0 -> 5) : on compare la valeur.
        self::assertSame(5.0, (float) $data['countedDays']);
        self::assertSame($this->employee->getId(), $data['user']['id']);

        $balance = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class)
            ->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
        ;
        self::assertNotNull($balance);
        self::assertSame(5.0, $balance->getPending());
    }

    public function testApproveMovesPendingToTaken(): void
    {
        $created = json_decode(
            ($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
            true,
        );

        $json = ($this->reviewTool($this->admin))($created['id'], 'approve');
        $data = json_decode($json, true);

        self::assertSame('approved', $data['status']);
        self::assertSame($this->admin->getId(), $data['reviewedBy']['id']);

        $balance = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class)
            ->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
        ;
        self::assertSame(0.0, $balance->getPending());
        self::assertSame(5.0, $balance->getTaken());
    }

    public function testApproveBeyondAvailableBalanceIsBlocked(): void
    {
        $created = json_decode(
            ($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
            true,
        );

        // Shrink the entitlement below the 5 requested days.
        $balance = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class)
            ->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
        ;
        $balance->setAcquired(2.0);
        $balance->setAcquiring(0.0);
        $this->em->flush();

        try {
            ($this->reviewTool($this->admin))($created['id'], 'approve');
            self::fail('Expected approval to be blocked when it would breach the balance.');
        } catch (InvalidArgumentException $e) {
            self::assertStringContainsString('below zero', $e->getMessage());
        }

        // Approval bailed out before mutating: nothing moved to taken, days stay reserved.
        self::assertSame(0.0, $balance->getTaken());
        self::assertSame(5.0, $balance->getPending());
    }

    public function testAdminCancelApprovedReleasesTaken(): void
    {
        $created = json_decode(
            ($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
            true,
        );
        ($this->reviewTool($this->admin))($created['id'], 'approve');

        $data = json_decode(($this->cancelTool($this->admin))($created['id']), true);
        self::assertSame('cancelled', $data['status']);

        $balance = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class)
            ->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
        ;
        self::assertSame(0.0, $balance->getTaken());
        self::assertSame(0.0, $balance->getPending());
    }

    public function testBereavementRequiresReasonAndDoesNotTouchBalance(): void
    {
        // Ensure an active bereavement policy exists (no fixed entitlement).
        $policy = $this->em->getRepository(AbsencePolicy::class)->findOneBy(['type' => AbsenceType::Bereavement]);
        if (null === $policy) {
            $policy = new AbsencePolicy();
            $policy->setType(AbsenceType::Bereavement);
            $policy->setCountWorkingDaysOnly(true);
            $this->em->persist($policy);
        }
        $policy->setActive(true);
        $this->em->flush();

        $tool = $this->createTool($this->admin);

        // Without a reason → rejected.
        try {
            ($tool)($this->employee->getId(), 'deces', '2026-06-10', '2026-06-12');
            self::fail('Expected an exception for bereavement without a reason.');
        } catch (InvalidArgumentException $e) {
            self::assertStringContainsString('reason', $e->getMessage());
        }

        // With a reason → accepted, and no balance row is created (per-event right).
        $data = json_decode(
            ($tool)($this->employee->getId(), 'deces', '2026-06-10', '2026-06-12', null, null, 'Décès grand-parent'),
            true,
        );
        self::assertSame('pending', $data['status']);

        $balance = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class)
            ->findOneForPeriod($this->employee, AbsenceType::Bereavement, '2026')
        ;
        self::assertNull($balance, 'Bereavement must not create or touch any balance.');
    }

    private function securityFor(User $user): Security
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->willReturn(true);
        $security->method('getUser')->willReturn($user);

        return $security;
    }

    private function createTool(User $actor): CreateAbsenceRequestTool
    {
        $c = self::getContainer();

        return new CreateAbsenceRequestTool(
            $c->get(EntityManagerInterface::class),
            $c->get(DoctrineUserRepository::class),
            $c->get(DoctrineAbsencePolicyRepository::class),
            $c->get(DoctrineAbsenceRequestRepository::class),
            $c->get(AbsenceDayCalculator::class),
            $c->get(AbsenceBalanceService::class),
            $this->securityFor($actor),
        );
    }

    private function reviewTool(User $actor): ReviewAbsenceRequestTool
    {
        $c = self::getContainer();

        return new ReviewAbsenceRequestTool(
            $c->get(EntityManagerInterface::class),
            $c->get(DoctrineAbsenceRequestRepository::class),
            $c->get(AbsenceBalanceService::class),
            $this->securityFor($actor),
        );
    }

    private function cancelTool(User $actor): CancelAbsenceRequestTool
    {
        $c = self::getContainer();

        return new CancelAbsenceRequestTool(
            $c->get(EntityManagerInterface::class),
            $c->get(DoctrineAbsenceRequestRepository::class),
            $c->get(AbsenceBalanceService::class),
            $this->securityFor($actor),
        );
    }
}
