<?php

namespace App\Tests\Command;

use App\Command\PaymentReminderCommand;
use App\Entity\Advert;
use App\Entity\Customer;
use App\Entity\OrderNumber;
use App\Entity\PaymentReminder;
use App\Service\ActionService;
use App\Service\MailerService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Twig\Environment;

class PaymentReminderCommandTest extends TestCase
{
    private EntityManagerInterface $em;
    private MailerService $mailer;
    private Environment $twig;
    private LoggerInterface $logger;
    private ActionService $actionService;

    protected function setUp(): void
    {
        $this->em = $this->createMock(EntityManagerInterface::class);
        $this->mailer = $this->createMock(MailerService::class);
        $this->twig = $this->createStub(Environment::class);
        $this->logger = $this->createMock(LoggerInterface::class);
        $this->actionService = $this->createMock(ActionService::class);
    }

    private function makeCommand(): PaymentReminderCommand
    {
        return new PaymentReminderCommand(
            $this->em,
            $this->mailer,
            $this->twig,
            $this->logger,
            $this->actionService,
        );
    }

    private function makeAdvert(string $numOrder, ?string $email = 'client@example.com', int $daysAgo = 20): Advert
    {
        $orderNumber = new OrderNumber($numOrder);
        $customer = $this->createStub(Customer::class);
        $customer->method('getEmail')->willReturn($email);
        $customer->method('getFullName')->willReturn('Jean Dupont');

        $advert = $this->createMock(Advert::class);
        $advert->method('getOrderNumber')->willReturn($orderNumber);
        $advert->method('getCustomer')->willReturn($customer);
        $advert->method('getState')->willReturn(Advert::STATE_SEND);

        $updatedAt = new \DateTimeImmutable('-'.$daysAgo.' days');
        $advert->method('getUpdatedAt')->willReturn($updatedAt);
        $advert->method('getCreatedAt')->willReturn($updatedAt);

        return $advert;
    }

    /**
     * Stub the PaymentReminder QueryBuilder so getNextStep returns a specific list of already-done steps.
     * Also stubs the Advert repository findBy to return the provided adverts.
     *
     * @param array<Advert> $adverts
     */
    private function stubReminderRepo(array $existingSteps, array $adverts = []): void
    {
        $query = $this->createStub(Query::class);
        $query->method('getSingleColumnResult')->willReturn($existingSteps);

        $qb = $this->createStub(QueryBuilder::class);
        $qb->method('select')->willReturnSelf();
        $qb->method('where')->willReturnSelf();
        $qb->method('setParameter')->willReturnSelf();
        $qb->method('getQuery')->willReturn($query);

        $reminderRepo = $this->createStub(EntityRepository::class);
        $reminderRepo->method('createQueryBuilder')->willReturn($qb);

        $advertRepo = $this->createStub(EntityRepository::class);
        $advertRepo->method('findBy')->willReturn($adverts);

        $this->em->method('getRepository')->willReturnCallback(
            fn (string $class) => match ($class) {
                PaymentReminder::class => $reminderRepo,
                default => $advertRepo,
            }
        );
    }

    public function testNoAdvertsReturnsSuccess(): void
    {
        $repo = $this->createStub(EntityRepository::class);
        $repo->method('findBy')->willReturn([]);

        $this->em->method('getRepository')->with(Advert::class)->willReturn($repo);

        $this->mailer->expects($this->never())->method('sendEmail');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('Aucun avis de paiement en attente', $tester->getDisplay());
    }

    public function testAdvertWithNoCustomerIsSkipped(): void
    {
        $advert = $this->createStub(Advert::class);
        $advert->method('getCustomer')->willReturn(null);

        $advertRepo = $this->createStub(EntityRepository::class);
        $advertRepo->method('findBy')->willReturn([$advert]);

        $this->em->method('getRepository')->willReturn($advertRepo);

        $this->mailer->expects($this->never())->method('sendEmail');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testAdvertWithNoEmailIsSkipped(): void
    {
        $advert = $this->makeAdvert('04/2026-00001', null, 20);

        $advertRepo = $this->createStub(EntityRepository::class);
        $advertRepo->method('findBy')->willReturn([$advert]);

        $this->em->method('getRepository')->willReturn($advertRepo);

        $this->mailer->expects($this->never())->method('sendEmail');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testAdvertWithNoEligibleStepIsSkipped(): void
    {
        // Only 5 days old — no step requires less than 15 days
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 5);

        $this->stubReminderRepo([], [$advert]);

        $this->mailer->expects($this->never())->method('sendEmail');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('0 relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testAdvertWithAllStepsDoneIsSkipped(): void
    {
        // 30 days old — many steps eligible, but all already done
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 30);

        // All steps already done
        $allSteps = array_keys(PaymentReminder::STEPS_CONFIG);
        $this->stubReminderRepo($allSteps, [$advert]);

        $this->mailer->expects($this->never())->method('sendEmail');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('0 relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testSendsFirstReminderAt15Days(): void
    {
        // 16 days old, no steps done yet -> should trigger reminder_15
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 16);

        $this->stubReminderRepo([], [$advert]);

        $this->twig->method('render')->willReturn('<p>Email content</p>');

        // Expect 2 emails: client + admin notification
        $this->mailer->expects($this->exactly(2))->method('sendEmail');

        $this->em->method('persist');
        $this->em->method('flush');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('1 relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testSuspensionStepCallsSuspendCustomer(): void
    {
        // 30 days old, all steps before suspension already done
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 30);

        $customer = $advert->getCustomer();

        // All steps before STEP_SUSPENSION_1 are done; days=30 >= 29 for suspension_1
        $doneSteps = [
            PaymentReminder::STEP_REMINDER_15,
            PaymentReminder::STEP_WARNING_10,
            PaymentReminder::STEP_SUSPENSION_WARNING_5,
            PaymentReminder::STEP_FINAL_REMINDER_3,
        ];
        $this->stubReminderRepo($doneSteps, [$advert]);

        $this->twig->method('render')->willReturn('<p>Email</p>');
        $this->mailer->method('sendEmail');
        $this->em->method('persist');
        $this->em->method('flush');

        $this->actionService->expects($this->once())
            ->method('suspendCustomer')
            ->with($customer, $this->stringContains('Impaye'));

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('1 relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testTerminationWarningStepCallsDisableCustomer(): void
    {
        // 46 days old -> termination_warning step (>= 45 days)
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 46);

        $customer = $advert->getCustomer();

        // All earlier steps done
        $doneSteps = [
            PaymentReminder::STEP_REMINDER_15,
            PaymentReminder::STEP_WARNING_10,
            PaymentReminder::STEP_SUSPENSION_WARNING_5,
            PaymentReminder::STEP_FINAL_REMINDER_3,
            PaymentReminder::STEP_SUSPENSION_1,
            PaymentReminder::STEP_FORMAL_NOTICE,
        ];
        $this->stubReminderRepo($doneSteps, [$advert]);

        $this->twig->method('render')->willReturn('<p>Email</p>');
        $this->mailer->method('sendEmail');
        $this->em->method('persist');
        $this->em->method('flush');

        $this->actionService->expects($this->once())
            ->method('disableCustomer')
            ->with($customer, $this->stringContains('Pre-resiliation'));

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function testTerminationStepCallsMarkForDeletion(): void
    {
        // 61 days old -> termination_30 step (>= 60 days)
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 61);

        $customer = $advert->getCustomer();

        // All earlier steps done
        $doneSteps = [
            PaymentReminder::STEP_REMINDER_15,
            PaymentReminder::STEP_WARNING_10,
            PaymentReminder::STEP_SUSPENSION_WARNING_5,
            PaymentReminder::STEP_FINAL_REMINDER_3,
            PaymentReminder::STEP_SUSPENSION_1,
            PaymentReminder::STEP_FORMAL_NOTICE,
            PaymentReminder::STEP_TERMINATION_WARNING,
        ];
        $this->stubReminderRepo($doneSteps, [$advert]);

        $this->twig->method('render')->willReturn('<p>Email</p>');
        $this->mailer->method('sendEmail');
        $this->em->method('persist');
        $this->em->expects($this->atLeastOnce())->method('flush');

        $this->actionService->expects($this->once())
            ->method('markForDeletion')
            ->with($customer, $this->stringContains('Resiliation'));

        // setState(STATE_CANCEL) should be called on the advert
        $advert->expects($this->once())->method('setState')->with(Advert::STATE_CANCEL);

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
    }

    public function testFormalNoticeStepSendsEmailOnly(): void
    {
        // 32 days old -> formal_notice step (>= 31 days), all earlier steps done
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 32);

        $doneSteps = [
            PaymentReminder::STEP_REMINDER_15,
            PaymentReminder::STEP_WARNING_10,
            PaymentReminder::STEP_SUSPENSION_WARNING_5,
            PaymentReminder::STEP_FINAL_REMINDER_3,
            PaymentReminder::STEP_SUSPENSION_1,
        ];
        $this->stubReminderRepo($doneSteps, [$advert]);

        $this->twig->method('render')->willReturn('<p>Email</p>');
        $this->em->method('persist');
        $this->em->method('flush');

        // Expect 2 emails: client (mise en demeure) + admin notification
        $this->mailer->expects($this->exactly(2))->method('sendEmail');

        // No ActionService calls expected for formal_notice
        $this->actionService->expects($this->never())->method('suspendCustomer');
        $this->actionService->expects($this->never())->method('disableCustomer');
        $this->actionService->expects($this->never())->method('markForDeletion');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('1 relance(s) envoyee(s)', $tester->getDisplay());
    }

    public function testExceptionInStepIsLoggedAndContinues(): void
    {
        $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 20);

        $this->stubReminderRepo([], [$advert]);

        // Twig throws an exception when rendering
        $this->twig->method('render')->willThrowException(new \RuntimeException('Twig error'));

        $this->logger->expects($this->once())->method('error');

        $tester = new CommandTester($this->makeCommand());
        $tester->execute([]);

        $this->assertSame(0, $tester->getStatusCode());
        $this->assertStringContainsString('0 relance(s) envoyee(s)', $tester->getDisplay());
        $this->assertStringContainsString('Erreur', $tester->getDisplay());
    }
}
