<?php

namespace App\Tests\Controller;

use App\Controller\DevisProcessController;
use App\Entity\Customer;
use App\Entity\Devis;
use App\Entity\OrderNumber;
use App\Service\DocuSealService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;

class DevisProcessControllerTest extends TestCase
{
    private const HMAC_SECRET = 'test-secret';
    private const DOCUSEAL_URL = 'https://docuseal.test';

    /** Build a real Devis so its HMAC is consistent. */
    private function createDevis(?string $state = Devis::STATE_SEND): Devis
    {
        $orderNumber = $this->createStub(OrderNumber::class);

        // Use a real Devis constructed with the correct secret so getHmac() works
        $devis = new Devis($orderNumber, self::HMAC_SECRET);
        $devis->setState($state);

        return $devis;
    }

    private function hmacFor(Devis $devis): string
    {
        return $devis->getHmac();
    }

    /** Creates a stub EM (no expectations). Use createEmWithExpectations() when flush assertions are needed. */
    private function createEmMock(?Devis $devis): EntityManagerInterface
    {
        $repo = $this->createStub(EntityRepository::class);
        $repo->method('find')->willReturn($devis);

        $em = $this->createStub(EntityManagerInterface::class);
        $em->method('getRepository')->willReturn($repo);

        return $em;
    }

    /** Creates a real mock EM that allows flush expectations. */
    private function createEmWithExpectations(?Devis $devis): EntityManagerInterface
    {
        $repo = $this->createStub(EntityRepository::class);
        $repo->method('find')->willReturn($devis);

        $em = $this->createMock(EntityManagerInterface::class);
        $em->method('getRepository')->willReturn($repo);

        return $em;
    }

    private function createDocuSealStub(): DocuSealService
    {
        return $this->createStub(DocuSealService::class);
    }

    private function createContainer(): ContainerInterface
    {
        $session = new Session(new MockArraySessionStorage());
        $stack = $this->createStub(RequestStack::class);
        $stack->method('getSession')->willReturn($session);

        $twig = $this->createStub(Environment::class);
        $twig->method('render')->willReturn('<html></html>');

        $router = $this->createStub(RouterInterface::class);
        $router->method('generate')->willReturn('/some/path');

        $container = $this->createStub(ContainerInterface::class);
        $container->method('has')->willReturn(true);
        $container->method('get')->willReturnMap([
            ['twig', $twig],
            ['router', $router],
            ['request_stack', $stack],
        ]);

        return $container;
    }

    private function makeController(EntityManagerInterface $em, ?DocuSealService $docuSeal = null): DevisProcessController
    {
        $controller = new DevisProcessController(
            $em,
            $docuSeal ?? $this->createDocuSealStub(),
            self::DOCUSEAL_URL
        );
        $controller->setContainer($this->createContainer());

        return $controller;
    }

    // -------------------------------------------------------------------------
    // loadAndCheck – devis not found
    // -------------------------------------------------------------------------

    public function testShowDevisNotFoundThrows404(): void
    {
        $em = $this->createEmMock(null);
        $controller = $this->makeController($em);

        $this->expectException(NotFoundHttpException::class);
        $controller->show(99, 'wrong-hmac');
    }

    public function testSignDevisNotFoundThrows404(): void
    {
        $em = $this->createEmMock(null);
        $controller = $this->makeController($em);

        $this->expectException(NotFoundHttpException::class);
        $controller->sign(99, 'wrong-hmac');
    }

    public function testSignedDevisNotFoundThrows404(): void
    {
        $em = $this->createEmMock(null);
        $controller = $this->makeController($em);

        $this->expectException(NotFoundHttpException::class);
        $controller->signed(99, 'wrong-hmac');
    }

    public function testRefuseDevisNotFoundThrows404(): void
    {
        $em = $this->createEmMock(null);
        $controller = $this->makeController($em);

        $this->expectException(NotFoundHttpException::class);
        $controller->refuse(99, 'wrong-hmac', new Request());
    }

    // -------------------------------------------------------------------------
    // loadAndCheck – HMAC mismatch
    // -------------------------------------------------------------------------

    public function testShowHmacMismatchThrows403(): void
    {
        $devis = $this->createDevis();
        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em);

        $this->expectException(AccessDeniedException::class);
        $controller->show(1, 'bad-hmac');
    }

    public function testSignHmacMismatchThrows403(): void
    {
        $devis = $this->createDevis();
        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em);

        $this->expectException(AccessDeniedException::class);
        $controller->sign(1, 'bad-hmac');
    }

    public function testSignedHmacMismatchThrows403(): void
    {
        $devis = $this->createDevis();
        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em);

        $this->expectException(AccessDeniedException::class);
        $controller->signed(1, 'bad-hmac');
    }

    public function testRefuseHmacMismatchThrows403(): void
    {
        $devis = $this->createDevis();
        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em);

        $this->expectException(AccessDeniedException::class);
        $controller->refuse(1, 'bad-hmac', new Request());
    }

    // -------------------------------------------------------------------------
    // show – state-based rendering
    // -------------------------------------------------------------------------

    public function testShowStateAcceptedRendersSignedTwig(): void
    {
        $devis = $this->createDevis(Devis::STATE_ACCEPTED);
        $em = $this->createEmMock($devis);

        $twig = $this->createMock(Environment::class);
        $twig->expects($this->once())
            ->method('render')
            ->with($this->stringContains('signed.html.twig'), $this->anything())
            ->willReturn('<html></html>');

        $router = $this->createStub(RouterInterface::class);
        $stack = $this->createStub(RequestStack::class);
        $stack->method('getSession')->willReturn(new Session(new MockArraySessionStorage()));

        $container = $this->createStub(ContainerInterface::class);
        $container->method('has')->willReturn(true);
        $container->method('get')->willReturnMap([
            ['twig', $twig],
            ['router', $router],
            ['request_stack', $stack],
        ]);

        $controller = new DevisProcessController($em, $this->createDocuSealStub(), self::DOCUSEAL_URL);
        $controller->setContainer($container);

        $response = $controller->show(1, $this->hmacFor($devis));
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testShowStateRefusedRendersRefusedTwig(): void
    {
        $devis = $this->createDevis(Devis::STATE_REFUSED);
        $em = $this->createEmMock($devis);

        $twig = $this->createMock(Environment::class);
        $twig->expects($this->once())
            ->method('render')
            ->with($this->stringContains('refused.html.twig'), $this->anything())
            ->willReturn('<html></html>');

        $router = $this->createStub(RouterInterface::class);
        $stack = $this->createStub(RequestStack::class);
        $stack->method('getSession')->willReturn(new Session(new MockArraySessionStorage()));

        $container = $this->createStub(ContainerInterface::class);
        $container->method('has')->willReturn(true);
        $container->method('get')->willReturnMap([
            ['twig', $twig],
            ['router', $router],
            ['request_stack', $stack],
        ]);

        $controller = new DevisProcessController($em, $this->createDocuSealStub(), self::DOCUSEAL_URL);
        $controller->setContainer($container);

        $response = $controller->show(1, $this->hmacFor($devis));
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testShowStateSendRendersProcessTwig(): void
    {
        $devis = $this->createDevis(Devis::STATE_SEND);
        $em = $this->createEmMock($devis);

        $twig = $this->createMock(Environment::class);
        $twig->expects($this->once())
            ->method('render')
            ->with($this->stringContains('process.html.twig'), $this->anything())
            ->willReturn('<html></html>');

        $router = $this->createStub(RouterInterface::class);
        $stack = $this->createStub(RequestStack::class);
        $stack->method('getSession')->willReturn(new Session(new MockArraySessionStorage()));

        $container = $this->createStub(ContainerInterface::class);
        $container->method('has')->willReturn(true);
        $container->method('get')->willReturnMap([
            ['twig', $twig],
            ['router', $router],
            ['request_stack', $stack],
        ]);

        $controller = new DevisProcessController($em, $this->createDocuSealStub(), self::DOCUSEAL_URL);
        $controller->setContainer($container);

        $response = $controller->show(1, $this->hmacFor($devis));
        $this->assertSame(200, $response->getStatusCode());
    }

    // -------------------------------------------------------------------------
    // sign
    // -------------------------------------------------------------------------

    public function testSignNoSubmissionIdThrows404(): void
    {
        $devis = $this->createDevis();
        // submissionId is null by default → cast to int gives 0
        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em);

        $this->expectException(NotFoundHttpException::class);
        $controller->sign(1, $this->hmacFor($devis));
    }

    public function testSignSlugNotFoundThrows404(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('42');

        $docuSeal = $this->createStub(DocuSealService::class);
        $docuSeal->method('getSubmitterSlug')->willReturn(null);

        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em, $docuSeal);

        $this->expectException(NotFoundHttpException::class);
        $controller->sign(1, $this->hmacFor($devis));
    }

    public function testSignSuccessRedirectsToDocuSeal(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('42');

        $docuSeal = $this->createStub(DocuSealService::class);
        $docuSeal->method('getSubmitterSlug')->willReturn('abc123');

        $em = $this->createEmMock($devis);
        $controller = $this->makeController($em, $docuSeal);

        $response = $controller->sign(1, $this->hmacFor($devis));

        $this->assertSame(302, $response->getStatusCode());
        $this->assertStringContainsString('/s/abc123', $response->getTargetUrl());
        $this->assertStringContainsString(self::DOCUSEAL_URL, $response->getTargetUrl());
    }

    public function testSignDocuSealUrlTrailingSlashHandled(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('7');

        $docuSeal = $this->createStub(DocuSealService::class);
        $docuSeal->method('getSubmitterSlug')->willReturn('xyz');

        $repo = $this->createStub(EntityRepository::class);
        $repo->method('find')->willReturn($devis);
        $em = $this->createStub(EntityManagerInterface::class);
        $em->method('getRepository')->willReturn($repo);

        $controller = new DevisProcessController($em, $docuSeal, 'https://docuseal.test/');
        $controller->setContainer($this->createContainer());

        $response = $controller->sign(1, $this->hmacFor($devis));
        $this->assertSame(302, $response->getStatusCode());
        // Must not produce double slash before /s/
        $this->assertStringNotContainsString('//s/', $response->getTargetUrl());
        $this->assertStringContainsString('/s/xyz', $response->getTargetUrl());
    }

    // -------------------------------------------------------------------------
    // signed
    // -------------------------------------------------------------------------

    public function testSignedAlreadyAcceptedDoesNotFlush(): void
    {
        $devis = $this->createDevis(Devis::STATE_ACCEPTED);
        $em = $this->createEmWithExpectations($devis);
        $em->expects($this->never())->method('flush');

        $controller = $this->makeController($em);
        $response = $controller->signed(1, $this->hmacFor($devis));

        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame(Devis::STATE_ACCEPTED, $devis->getState());
    }

    public function testSignedNotYetAcceptedSetsStateAndFlushes(): void
    {
        $devis = $this->createDevis(Devis::STATE_SEND);
        $em = $this->createEmWithExpectations($devis);
        $em->expects($this->once())->method('flush');

        $controller = $this->makeController($em);
        $response = $controller->signed(1, $this->hmacFor($devis));

        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame(Devis::STATE_ACCEPTED, $devis->getState());
    }

    // -------------------------------------------------------------------------
    // refuse
    // -------------------------------------------------------------------------

    public function testRefuseWithReasonSetsRaisonMessage(): void
    {
        $devis = $this->createDevis();
        $em = $this->createEmMock($devis);
        $em->method('flush');

        $controller = $this->makeController($em);

        $request = new Request([], ['reason' => 'Trop cher']);
        $response = $controller->refuse(1, $this->hmacFor($devis), $request);

        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame(Devis::STATE_REFUSED, $devis->getState());
        $this->assertSame('Trop cher', $devis->getRaisonMessage());
    }

    public function testRefuseWithoutReasonDoesNotSetRaisonMessage(): void
    {
        $devis = $this->createDevis();
        $em = $this->createEmMock($devis);
        $em->method('flush');

        $controller = $this->makeController($em);

        $request = new Request([], ['reason' => '']);
        $response = $controller->refuse(1, $this->hmacFor($devis), $request);

        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame(Devis::STATE_REFUSED, $devis->getState());
        $this->assertNull($devis->getRaisonMessage());
    }

    public function testRefuseWithSubmitterIdArchivesDocuSeal(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('5');

        $docuSeal = $this->createMock(DocuSealService::class);
        $docuSeal->expects($this->once())
            ->method('getSubmitterData')
            ->with(5)
            ->willReturn(['submission_id' => 10]);
        $docuSeal->expects($this->once())
            ->method('archiveSubmission')
            ->with(10);

        $em = $this->createEmMock($devis);
        $em->method('flush');

        $controller = $this->makeController($em, $docuSeal);

        $response = $controller->refuse(1, $this->hmacFor($devis), new Request());
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testRefuseWithSubmitterIdGetSubmitterDataReturnsNull(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('5');

        $docuSeal = $this->createMock(DocuSealService::class);
        $docuSeal->method('getSubmitterData')->willReturn(null);
        $docuSeal->expects($this->never())->method('archiveSubmission');

        $em = $this->createEmMock($devis);
        $em->method('flush');

        $controller = $this->makeController($em, $docuSeal);

        $response = $controller->refuse(1, $this->hmacFor($devis), new Request());
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testRefuseWithZeroSubmitterIdSkipsDocuSeal(): void
    {
        $devis = $this->createDevis();
        // submissionId stays null → (int)'0' = 0

        $docuSeal = $this->createMock(DocuSealService::class);
        $docuSeal->expects($this->never())->method('getSubmitterData');
        $docuSeal->expects($this->never())->method('archiveSubmission');

        $em = $this->createEmMock($devis);
        $em->method('flush');

        $controller = $this->makeController($em, $docuSeal);

        $response = $controller->refuse(1, $this->hmacFor($devis), new Request());
        $this->assertSame(200, $response->getStatusCode());
    }

    public function testRefuseDocuSealThrowsSilentlyCaught(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('5');

        $docuSeal = $this->createStub(DocuSealService::class);
        $docuSeal->method('getSubmitterData')->willThrowException(new \RuntimeException('API error'));

        $em = $this->createEmWithExpectations($devis);
        $em->expects($this->once())->method('flush');

        $controller = $this->makeController($em, $docuSeal);

        // Must not throw; flush should still be called
        $response = $controller->refuse(1, $this->hmacFor($devis), new Request());
        $this->assertSame(200, $response->getStatusCode());
        $this->assertSame(Devis::STATE_REFUSED, $devis->getState());
    }

    public function testRefuseWithSubmitterIdButNoSubmissionIdInData(): void
    {
        $devis = $this->createDevis();
        $devis->setSubmissionId('5');

        $docuSeal = $this->createMock(DocuSealService::class);
        $docuSeal->method('getSubmitterData')->willReturn(['other_key' => 99]);
        $docuSeal->expects($this->never())->method('archiveSubmission');

        $em = $this->createEmMock($devis);
        $em->method('flush');

        $controller = $this->makeController($em, $docuSeal);

        $response = $controller->refuse(1, $this->hmacFor($devis), new Request());
        $this->assertSame(200, $response->getStatusCode());
    }
}
