<?php

namespace App\Tests\Service;

use App\Entity\OrderNumber;
use App\Repository\OrderNumberRepository;
use App\Service\OrderNumberService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;

class OrderNumberServiceTest extends TestCase
{
    private function createQueryBuilder(?OrderNumber $lastOrder = null): QueryBuilder
    {
        $query = $this->createStub(Query::class);
        $query->method('getOneOrNullResult')->willReturn($lastOrder);

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

        return $qb;
    }

    public function testGenerateFirstOfMonth(): void
    {
        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder(null));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->generate();

        $now = new \DateTimeImmutable();
        $expected = $now->format('m/Y').'-00001';
        $this->assertSame($expected, $result->getNumOrder());
    }

    public function testGenerateIncrementsCounter(): void
    {
        $now = new \DateTimeImmutable();
        $lastOrder = new OrderNumber($now->format('m/Y').'-00042');

        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder($lastOrder));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->generate();

        $expected = $now->format('m/Y').'-00042';
        $this->assertSame($expected, $result->getNumOrder());
    }

    public function testGenerateAndUse(): void
    {
        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder(null));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->generateAndUse();

        $this->assertTrue($result->isUsed());
    }

    public function testPreviewFirstOfMonth(): void
    {
        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder(null));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->preview();

        $now = new \DateTimeImmutable();
        $this->assertSame($now->format('m/Y').'-00001', $result);
    }

    public function testPreviewIncrementsCounter(): void
    {
        $now = new \DateTimeImmutable();
        $lastOrder = new OrderNumber($now->format('m/Y').'-00010');

        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder($lastOrder));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->preview();

        $this->assertSame($now->format('m/Y').'-00010', $result);
    }

    public function testPreviewReturnsUnusedOrderNumber(): void
    {
        $now = new \DateTimeImmutable();
        // An existing unused order number
        $unused = new OrderNumber($now->format('m/Y').'-00005');
        // unused query returns it; lastOrder query also returns it (only first call matters)
        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder($unused));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->preview();

        // When unused order exists, preview returns its numOrder
        $this->assertSame($now->format('m/Y').'-00005', $result);
    }

    public function testGenerateReturnsUnusedOrderNumberWhenExists(): void
    {
        $now = new \DateTimeImmutable();
        $unused = new OrderNumber($now->format('m/Y').'-00003');

        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder($unused));

        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->generate();

        $this->assertSame($unused, $result);
    }

    /**
     * Creates a repo stub that returns different QueryBuilders per call.
     * First call (unused lookup) returns null, second call (last order lookup) returns $lastOrder.
     */
    private function createSequentialRepo(?OrderNumber $lastOrder = null): OrderNumberRepository
    {
        $queryNull = $this->createStub(Query::class);
        $queryNull->method('getOneOrNullResult')->willReturn(null);

        $queryLast = $this->createStub(Query::class);
        $queryLast->method('getOneOrNullResult')->willReturn($lastOrder);

        $qbNull = $this->createStub(QueryBuilder::class);
        $qbNull->method('where')->willReturnSelf();
        $qbNull->method('setParameter')->willReturnSelf();
        $qbNull->method('orderBy')->willReturnSelf();
        $qbNull->method('setMaxResults')->willReturnSelf();
        $qbNull->method('getQuery')->willReturn($queryNull);

        $qbLast = $this->createStub(QueryBuilder::class);
        $qbLast->method('where')->willReturnSelf();
        $qbLast->method('setParameter')->willReturnSelf();
        $qbLast->method('orderBy')->willReturnSelf();
        $qbLast->method('setMaxResults')->willReturnSelf();
        $qbLast->method('getQuery')->willReturn($queryLast);

        $callCount = 0;
        $repo = $this->createStub(OrderNumberRepository::class);
        $repo->method('createQueryBuilder')->willReturnCallback(
            function () use ($qbNull, $qbLast, &$callCount) {
                return 0 === $callCount++ ? $qbNull : $qbLast;
            }
        );

        return $repo;
    }

    public function testGenerateCreatesNewWhenNoUnusedAndNoLastOrder(): void
    {
        $repo = $this->createSequentialRepo(null);
        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->generate();

        $now = new \DateTimeImmutable();
        $this->assertSame($now->format('m/Y').'-00001', $result->getNumOrder());
        $this->assertFalse($result->isUsed());
    }

    public function testGenerateCreatesNewIncrementingFromLastOrder(): void
    {
        $now = new \DateTimeImmutable();
        $lastOrder = new OrderNumber($now->format('m/Y').'-00042');

        $repo = $this->createSequentialRepo($lastOrder);
        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->generate();

        $this->assertSame($now->format('m/Y').'-00043', $result->getNumOrder());
    }

    public function testPreviewCreatesNewWhenNoUnusedAndNoLastOrder(): void
    {
        $repo = $this->createSequentialRepo(null);
        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->preview();

        $now = new \DateTimeImmutable();
        $this->assertSame($now->format('m/Y').'-00001', $result);
    }

    public function testPreviewIncrementingFromLastOrder(): void
    {
        $now = new \DateTimeImmutable();
        $lastOrder = new OrderNumber($now->format('m/Y').'-00010');

        $repo = $this->createSequentialRepo($lastOrder);
        $em = $this->createStub(EntityManagerInterface::class);

        $service = new OrderNumberService($repo, $em);
        $result = $service->preview();

        $this->assertSame($now->format('m/Y').'-00011', $result);
    }
}
