<?php

namespace App\Tests\Command;

use App\Command\StripeSyncCommand;
use App\Entity\BilletBuyer;
use App\Entity\Event;
use App\Entity\User;
use App\Service\AuditService;
use App\Service\BilletOrderService;
use App\Service\MailerService;
use App\Service\StripeService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;

class StripeSyncCommandTest extends TestCase
{
    private StripeService $stripeService;
    private BilletOrderService $billetOrderService;
    private MailerService $mailerService;
    private AuditService $audit;
    private EntityManagerInterface $em;
    private EntityRepository $userRepo;
    private EntityRepository $buyerRepo;

    protected function setUp(): void
    {
        $this->stripeService = $this->createMock(StripeService::class);
        $this->billetOrderService = $this->createMock(BilletOrderService::class);
        $this->mailerService = $this->createMock(MailerService::class);
        $this->audit = $this->createMock(AuditService::class);

        $this->userRepo = $this->createMock(EntityRepository::class);
        $this->buyerRepo = $this->createMock(EntityRepository::class);
        $this->buyerRepo->method('findBy')->willReturn([]);

        $this->em = $this->createMock(EntityManagerInterface::class);
        $this->em->method('getRepository')->willReturnCallback(function (string $class) {
            return match ($class) {
                User::class => $this->userRepo,
                BilletBuyer::class => $this->buyerRepo,
                default => $this->createMock(EntityRepository::class),
            };
        });
    }

    private function createOrganizer(string $stripeId, bool $charges = false, bool $payouts = false): User
    {
        $user = new User();
        $user->setEmail('orga-'.uniqid().'@test.fr');
        $user->setFirstName('Test');
        $user->setLastName('Orga');
        $user->setPassword('hashed');
        $user->setRoles(['ROLE_ORGANIZER']);
        $user->setStripeAccountId($stripeId);
        $user->setStripeChargesEnabled($charges);
        $user->setStripePayoutsEnabled($payouts);
        $user->setCompanyName('Asso Test');

        return $user;
    }

    private function createPendingOrder(?string $paymentIntentId = null): BilletBuyer
    {
        $event = $this->createMock(Event::class);
        $event->method('getTitle')->willReturn('Test Event');
        $event->method('getAccount')->willReturn($this->createOrganizer('acct_org'));

        $order = new BilletBuyer();
        $order->setStatus(BilletBuyer::STATUS_PENDING);
        $order->setStripeSessionId($paymentIntentId);
        $order->setEmail('buyer@test.fr');
        $order->setEvent($event);

        return $order;
    }

    private function createCommandTester(): CommandTester
    {
        $command = new StripeSyncCommand(
            $this->em,
            $this->stripeService,
            $this->billetOrderService,
            $this->mailerService,
            $this->audit,
        );

        $app = new Application();
        $app->addCommand($command);

        return new CommandTester($app->find('app:stripe:sync'));
    }

    private function setBuyerRepo(array $orders): void
    {
        $this->buyerRepo = $this->createMock(EntityRepository::class);
        $this->buyerRepo->method('findBy')->willReturn($orders);

        $this->em = $this->createMock(EntityManagerInterface::class);
        $this->em->method('getRepository')->willReturnCallback(function (string $class) {
            return match ($class) {
                User::class => $this->userRepo,
                BilletBuyer::class => $this->buyerRepo,
                default => $this->createMock(EntityRepository::class),
            };
        });
    }

    // --- Account sync tests ---

    public function testSyncUpdatesStripeStatus(): void
    {
        $user = $this->createOrganizer('acct_123');
        $this->userRepo->method('findAll')->willReturn([$user]);

        $this->stripeService->method('retrieveAccountStatus')
            ->with('acct_123')
            ->willReturn(['charges_enabled' => true, 'payouts_enabled' => true]);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertTrue($user->isStripeChargesEnabled());
        self::assertTrue($user->isStripePayoutsEnabled());
        self::assertStringContainsString('1 synced', $tester->getDisplay());
        self::assertSame(0, $tester->getStatusCode());
    }

    public function testSyncDetectsChanges(): void
    {
        $user = $this->createOrganizer('acct_456', true, true);
        $this->userRepo->method('findAll')->willReturn([$user]);

        $this->stripeService->method('retrieveAccountStatus')
            ->willReturn(['charges_enabled' => true, 'payouts_enabled' => false]);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertTrue($user->isStripeChargesEnabled());
        self::assertFalse($user->isStripePayoutsEnabled());
        self::assertStringContainsString('UPDATED', $tester->getDisplay());
    }

    public function testSyncWithNoOrganizers(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('No organizers', $tester->getDisplay());
        self::assertSame(0, $tester->getStatusCode());
    }

    public function testSyncHandlesStripeError(): void
    {
        $user = $this->createOrganizer('acct_bad');
        $this->userRepo->method('findAll')->willReturn([$user]);

        $this->stripeService->method('retrieveAccountStatus')
            ->willThrowException(new \RuntimeException('Account not found'));

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('1 error', $tester->getDisplay());
        self::assertSame(1, $tester->getStatusCode());
    }

    public function testSyncSkipsOrganizersWithoutStripeAccount(): void
    {
        $userWithStripe = $this->createOrganizer('acct_ok');

        $userWithoutStripe = new User();
        $userWithoutStripe->setEmail('no-stripe@test.fr');
        $userWithoutStripe->setFirstName('No');
        $userWithoutStripe->setLastName('Stripe');
        $userWithoutStripe->setPassword('hashed');
        $userWithoutStripe->setRoles(['ROLE_ORGANIZER']);

        $this->userRepo->method('findAll')->willReturn([$userWithStripe, $userWithoutStripe]);

        $this->stripeService->method('retrieveAccountStatus')
            ->willReturn(['charges_enabled' => true, 'payouts_enabled' => false]);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('Syncing 1 organizer', $tester->getDisplay());
        self::assertStringContainsString('1 synced', $tester->getDisplay());
        self::assertTrue($userWithStripe->isStripeChargesEnabled());
        self::assertFalse($userWithStripe->isStripePayoutsEnabled());
    }

    // --- Pending orders sync tests ---

    public function testNoPendingOrders(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('No pending orders', $tester->getDisplay());
    }

    public function testPendingOrderSucceeded(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $order = $this->createPendingOrder('pi_succeeded');
        $this->setBuyerRepo([$order]);

        $paymentIntent = new \stdClass();
        $paymentIntent->status = 'succeeded';
        $paymentIntent->amount = 5000;
        $paymentIntent->metadata = (object) [];

        $this->stripeService->method('retrievePaymentIntent')
            ->with('pi_succeeded')
            ->willReturn($paymentIntent);

        $this->billetOrderService->expects(self::once())->method('generateOrderTickets')->with($order);
        $this->billetOrderService->expects(self::once())->method('generateAndSendTickets')->with($order);
        $this->billetOrderService->expects(self::once())->method('notifyOrganizer')->with($order);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('PAID', $tester->getDisplay());
    }

    public function testPendingOrderSucceededWithDebtOrganizer(): void
    {
        $organizer = $this->createOrganizer('acct_debt');
        $this->userRepo->method('findAll')->willReturn([]);
        $this->userRepo->method('find')->with(42)->willReturn($organizer);

        $order = $this->createPendingOrder('pi_debt');
        $this->setBuyerRepo([$order]);

        $paymentIntent = new \stdClass();
        $paymentIntent->status = 'succeeded';
        $paymentIntent->amount = 3000;
        $paymentIntent->metadata = (object) ['debt_organizer_id' => '42'];

        $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('PAID', $tester->getDisplay());
    }

    public function testPendingOrderCanceled(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $order = $this->createPendingOrder('pi_canceled');
        $this->setBuyerRepo([$order]);

        $paymentIntent = new \stdClass();
        $paymentIntent->status = 'canceled';

        $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent);

        $this->audit->expects(self::once())->method('log')
            ->with('payment_cancelled_sync', 'BilletBuyer', self::anything(), self::anything());

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertSame(BilletBuyer::STATUS_CANCELLED, $order->getStatus());
        self::assertStringContainsString('CANCELLED', $tester->getDisplay());
    }

    public function testPendingOrderFailed(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $order = $this->createPendingOrder('pi_failed');
        $this->setBuyerRepo([$order]);

        $paymentIntent = new \stdClass();
        $paymentIntent->status = 'requires_payment_method';
        $paymentIntent->last_payment_error = (object) ['message' => 'Card declined'];

        $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent);

        $this->audit->expects(self::once())->method('log')
            ->with('payment_failed_sync', 'BilletBuyer', self::anything(), self::anything());
        $this->mailerService->expects(self::once())->method('sendEmail');

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertSame(BilletBuyer::STATUS_CANCELLED, $order->getStatus());
        self::assertStringContainsString('FAILED', $tester->getDisplay());
    }

    public function testPendingOrderSkippedWithoutPaymentIntentId(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $order = $this->createPendingOrder(null);
        $this->setBuyerRepo([$order]);

        $this->stripeService->expects(self::never())->method('retrievePaymentIntent');

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('SKIP', $tester->getDisplay());
    }

    public function testPendingOrderStillPending(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $order = $this->createPendingOrder('pi_processing');
        $this->setBuyerRepo([$order]);

        $paymentIntent = new \stdClass();
        $paymentIntent->status = 'processing';

        $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent);

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertSame(BilletBuyer::STATUS_PENDING, $order->getStatus());
        self::assertStringContainsString('PENDING', $tester->getDisplay());
    }

    public function testPendingOrderStripeApiError(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $order = $this->createPendingOrder('pi_error');
        $this->setBuyerRepo([$order]);

        $this->stripeService->method('retrievePaymentIntent')
            ->willThrowException(new \RuntimeException('Stripe API error'));

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertStringContainsString('ERROR', $tester->getDisplay());
        self::assertSame(1, $tester->getStatusCode());
    }

    public function testPendingOrderFailedWithoutEmail(): void
    {
        $this->userRepo->method('findAll')->willReturn([]);

        $event = $this->createMock(Event::class);
        $event->method('getTitle')->willReturn('Test Event');

        $order = new BilletBuyer();
        $order->setStatus(BilletBuyer::STATUS_PENDING);
        $order->setStripeSessionId('pi_no_email');
        $order->setEvent($event);

        $this->setBuyerRepo([$order]);

        $paymentIntent = new \stdClass();
        $paymentIntent->status = 'requires_payment_method';
        $paymentIntent->last_payment_error = null;

        $this->stripeService->method('retrievePaymentIntent')->willReturn($paymentIntent);

        $this->mailerService->expects(self::never())->method('sendEmail');

        $tester = $this->createCommandTester();
        $tester->execute([]);

        self::assertSame(BilletBuyer::STATUS_CANCELLED, $order->getStatus());
        self::assertStringContainsString('FAILED', $tester->getDisplay());
    }
}
