<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\DocStoreBundle\Tests\Controller;

use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Controller\AsyncUploadController;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;

/**
 * @internal
 *
 * @coversNothing
 */
class AsyncUploadControllerTest extends TestCase
{
    use ProphecyTrait;

    public function testGetSignedUrlPost(): void
    {
        $storedObject = new StoredObject();
        $security = $this->prophesize(Security::class);
        $security->isGranted('SEE_AND_EDIT', $storedObject)->willReturn(true)->shouldBeCalled();

        $controller = new AsyncUploadController(
            $this->buildTempUrlGenerator(),
            $this->buildSerializer(),
            $security->reveal(),
            new NullLogger(),
        );

        $actual = $controller->getSignedUrlPost(new Request(query: ['expires_delay' => 10, 'submit_delay' => 1800]), $storedObject);

        $decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);

        self::assertArrayHasKey('method', $decodedActual);
        self::assertEquals('POST', $decodedActual['method']);
    }

    public function testGetSignedUrlGetSimpleScenarioHappy(): void
    {
        $storedObject = new StoredObject();
        $storedObject->registerVersion();
        $security = $this->prophesize(Security::class);
        $security->isGranted('SEE', $storedObject)->willReturn(true)->shouldBeCalled();
        $security->getUser()->willReturn(new User());

        $controller = new AsyncUploadController(
            $this->buildTempUrlGenerator(),
            $this->buildSerializer(),
            $security->reveal(),
            new NullLogger(),
        );

        $actual = $controller->getSignedUrlGet(new Request(query: ['expires_delay' => 10, 'submit_delay' => 1800]), $storedObject, 'get');

        $decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);

        self::assertArrayHasKey('method', $decodedActual);
        self::assertEquals('GET', $decodedActual['method']);
    }

    public function testGetSignedUrlGetSimpleScenarioNotAuthorized(): void
    {
        $this->expectException(AccessDeniedHttpException::class);

        $storedObject = new StoredObject();
        $storedObject->registerVersion();
        $security = $this->prophesize(Security::class);
        $security->isGranted('SEE', $storedObject)->willReturn(false)->shouldBeCalled();

        $controller = new AsyncUploadController(
            $this->buildTempUrlGenerator(),
            $this->buildSerializer(),
            $security->reveal(),
            new NullLogger(),
        );

        $controller->getSignedUrlGet(new Request(query: ['expires_delay' => 10, 'submit_delay' => 1800]), $storedObject, 'get');
    }

    public function testGetSignedUrlGetForSpecificVersionOfTheStoredObjectStoredInDatabase(): void
    {
        $storedObject = new StoredObject();
        $version = $storedObject->registerVersion();
        // we add a version to be sure that the we do not get the last one
        $storedObject->registerVersion();

        $security = $this->prophesize(Security::class);
        $security->isGranted('SEE', $storedObject)->willReturn(true)->shouldBeCalled();
        $security->getUser()->willReturn(new User());

        $controller = new AsyncUploadController(
            $this->buildTempUrlGenerator(),
            $this->buildSerializer(),
            $security->reveal(),
            new NullLogger(),
        );

        $actual = $controller->getSignedUrlGet(
            new Request(query: ['expires_delay' => 10, 'submit_delay' => 1800, 'version' => $version->getFilename()]),
            $storedObject,
            'get'
        );

        $decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);

        self::assertArrayHasKey('method', $decodedActual);
        self::assertEquals('GET', $decodedActual['method']);
        self::assertArrayHasKey('object_name', $decodedActual);
        self::assertEquals($version->getFilename(), $decodedActual['object_name']);
    }

    public function testGetSignedUrlGetForSpecificVersionOfTheStoredObjectNotYetStoredInDatabase(): void
    {
        $storedObject = new StoredObject();
        $storedObject->registerVersion();
        // we generate a valid name
        $version = $storedObject->getPrefix().'/some-version';

        $security = $this->prophesize(Security::class);
        $security->isGranted('SEE', $storedObject)->willReturn(true)->shouldBeCalled();
        $security->getUser()->willReturn(new User());

        $controller = new AsyncUploadController(
            $this->buildTempUrlGenerator(),
            $this->buildSerializer(),
            $security->reveal(),
            new NullLogger(),
        );

        $actual = $controller->getSignedUrlGet(
            new Request(query: ['expires_delay' => 10, 'submit_delay' => 1800, 'version' => $version]),
            $storedObject,
            'get'
        );

        $decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);

        self::assertArrayHasKey('method', $decodedActual);
        self::assertEquals('GET', $decodedActual['method']);
        self::assertArrayHasKey('object_name', $decodedActual);
        self::assertEquals($version, $decodedActual['object_name']);
    }

    public function testGetSignedUrlGetForSpecificVersionNotBelongingToTheStoreObject(): void
    {
        $this->expectException(AccessDeniedHttpException::class);

        $storedObject = new StoredObject();
        $storedObject->registerVersion();
        // we generate a random version
        $version = 'something/else';

        $security = $this->prophesize(Security::class);
        $security->isGranted('SEE', $storedObject)->willReturn(true)->shouldBeCalled();

        $controller = new AsyncUploadController(
            $this->buildTempUrlGenerator(),
            $this->buildSerializer(),
            $security->reveal(),
            new NullLogger(),
        );

        $controller->getSignedUrlGet(
            new Request(query: ['expires_delay' => 10, 'submit_delay' => 1800, 'version' => $version]),
            $storedObject,
            'get'
        );
    }

    public function buildTempUrlGenerator(): TempUrlGeneratorInterface
    {
        return new class () implements TempUrlGeneratorInterface {
            public function generatePost(?int $expire_delay = null, ?int $submit_delay = null, int $max_file_count = 1, ?string $object_name = null): SignedUrlPost
            {
                return new SignedUrlPost(
                    'https://object.store.example',
                    new \DateTimeImmutable('1 hour'),
                    $object_name ?? 'abc',
                    150,
                    1,
                    1800,
                    '',
                    'abc',
                    'abc'
                );
            }

            public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl
            {
                return new SignedUrl(
                    strtoupper($method),
                    'https://object.store.example',
                    new \DateTimeImmutable('1 hour'),
                    $object_name
                );
            }
        };
    }

    private function buildSerializer(): SerializerInterface
    {
        $serializer = $this->prophesize(SerializerInterface::class);
        $serializer->serialize(Argument::type(SignedUrl::class), 'json', Argument::type('array'))
            ->will(fn (array $args): string => json_encode(
                ['method' => $args[0]->method, 'object_name' => $args[0]->object_name],
                JSON_THROW_ON_ERROR,
                3
            ));

        return $serializer->reveal();
    }
}
