<?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\Controller\WebdavController;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Ramsey\Uuid\Uuid;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;

/**
 * @internal
 *
 * @coversNothing
 */
class WebdavControllerTest extends KernelTestCase
{
    use ProphecyTrait;

    private \Twig\Environment $engine;

    protected function setUp(): void
    {
        self::bootKernel();

        $this->engine = self::getContainer()->get(\Twig\Environment::class);
    }

    private function buildController(?EntityManagerInterface $entityManager = null, ?StoredObjectManagerInterface $storedObjectManager = null): WebdavController
    {
        if (null === $storedObjectManager) {
            $storedObjectManager = new MockedStoredObjectManager();
        }

        if (null === $entityManager) {
            $entityManager = $this->createMock(EntityManagerInterface::class);
        }

        $security = $this->prophesize(Security::class);
        $security->isGranted(Argument::in(['SEE_AND_EDIT', 'SEE']), Argument::type(StoredObject::class))
            ->willReturn(true);

        return new WebdavController($this->engine, $storedObjectManager, $security->reveal(), $entityManager);
    }

    private function buildDocument(): StoredObject
    {
        $object = (new StoredObject())
            ->registerVersion(type: 'application/vnd.oasis.opendocument.text')
            ->getStoredObject();

        $reflectionObject = new \ReflectionClass($object);
        $reflectionObjectUuid = $reflectionObject->getProperty('uuid');

        $reflectionObjectUuid->setValue($object, Uuid::fromString('716e6688-4579-4938-acf3-c4ab5856803b'));

        return $object;
    }

    public function testGet(): void
    {
        $controller = $this->buildController();

        $response = $controller->getDocument($this->buildDocument());

        self::assertEquals(200, $response->getStatusCode());
        self::assertEquals('abcde', $response->getContent());
        self::assertContains('etag', $response->headers->keys());
        self::assertStringContainsString('ab56b4', $response->headers->get('etag'));
    }

    public function testOptionsOnDocument(): void
    {
        $controller = $this->buildController();

        $response = $controller->optionsDocument($this->buildDocument());

        self::assertEquals(200, $response->getStatusCode());
        self::assertContains('allow', $response->headers->keys());

        foreach (explode(',', 'OPTIONS,GET,HEAD,PROPFIND') as $method) {
            self::assertStringContainsString($method, $response->headers->get('allow'));
        }

        self::assertContains('dav', $response->headers->keys());
        self::assertStringContainsString('1', $response->headers->get('dav'));
    }

    public function testOptionsOnDirectory(): void
    {
        $controller = $this->buildController();

        $response = $controller->optionsDirectory($this->buildDocument());

        self::assertEquals(200, $response->getStatusCode());
        self::assertContains('allow', $response->headers->keys());

        foreach (explode(',', 'OPTIONS,GET,HEAD,PROPFIND') as $method) {
            self::assertStringContainsString($method, $response->headers->get('allow'));
        }

        self::assertContains('dav', $response->headers->keys());
        self::assertStringContainsString('1', $response->headers->get('dav'));
    }

    /**
     * @dataProvider generateDataPropfindDocument
     */
    public function testPropfindDocument(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
    {
        $controller = $this->buildController();

        $request = new Request([], [], [], [], [], [], $requestContent);
        $request->setMethod('PROPFIND');
        $response = $controller->propfindDocument($this->buildDocument(), '1234', $request);

        self::assertEquals($expectedStatusCode, $response->getStatusCode());
        self::assertContains('content-type', $response->headers->keys());
        self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
        self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
        self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
    }

    public static function generateDataPropfindDocument(): iterable
    {
        $content =
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
            <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
            XML;

        $response =
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
            <d:multistatus xmlns:d="DAV:" >
            <d:response>
              <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
              <d:propstat>
                <d:prop>
                  <d:resourcetype/>
                  <d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
                </d:prop>
                <d:status>HTTP/1.1 200 OK</d:status>
              </d:propstat>
              <d:propstat>
                <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
                  <ns0:IsReadOnly/>
                </d:prop>
                <d:status>HTTP/1.1 404 Not Found</d:status>
              </d:propstat>
            </d:response>
            </d:multistatus>
            XML;

        yield [$content, 207, $response, 'get IsReadOnly and contenttype from server'];

        $content =
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
              <propfind xmlns="DAV:">
                <prop>
                  <IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/>
                </prop>
              </propfind>
            XML;

        $response =
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
              <d:multistatus xmlns:d="DAV:">
                <d:response>
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
                  <d:propstat>
                    <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
                      <ns0:IsReadOnly/>
                    </d:prop>
                    <d:status>HTTP/1.1 404 Not Found</d:status>
                  </d:propstat>
                </d:response>
              </d:multistatus>
            XML;

        yield [$content, 207, $response, 'get property IsReadOnly'];

        yield [
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
              <propfind xmlns="DAV:">
                <prop>
                  <BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/>
                </prop>
              </propfind>
            XML,
            207,
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
              <d:multistatus xmlns:d="DAV:">
                <d:response>
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
                  <d:propstat>
                    <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
                      <ns0:BaseURI/>
                    </d:prop>
                    <d:status>HTTP/1.1 404 Not Found</d:status>
                  </d:propstat>
                </d:response>
              </d:multistatus>
            XML,
            'Test requesting an unknow property',
        ];

        yield [
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
              <propfind xmlns="DAV:">
                <prop>
                  <getlastmodified xmlns="DAV:"/>
                </prop>
              </propfind>
            XML,
            207,
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
              <d:multistatus xmlns:d="DAV:">
                <d:response>
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
                  <d:propstat>
                    <d:prop>
                    <!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT -->
                      <d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
                    </d:prop>
                    <d:status>HTTP/1.1 200 OK</d:status>
                  </d:propstat>
                </d:response>
              </d:multistatus>
            XML,
            'test getting the last modified date',
        ];

        yield [
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
              <propfind xmlns="DAV:">
                <propname/>
              </propfind>
            XML,
            207,
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
              <d:multistatus xmlns:d="DAV:">
                <d:response>
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
                  <d:propstat>
                    <d:prop>
                      <d:resourcetype/>
                      <d:creationdate/>
                      <d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
                      <!-- <d:getcontentlength/> -->
                      <d:getcontentlength>5</d:getcontentlength>
                      <!-- <d:getlastmodified/> -->
                      <d:getetag>"ab56b4d92b40713acc5af89985d4b786"</d:getetag>
                      <!--
                      <d:supportedlock/>
                      <d:lockdiscovery/>
                      -->
                     <!-- <d:getcontenttype/> -->
                      <d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
                    </d:prop>
                    <d:status>HTTP/1.1 200 OK</d:status>
                  </d:propstat>
                </d:response>
              </d:multistatus>
            XML,
            'test finding all properties',
        ];
    }

    /**
     * @dataProvider generateDataPropfindDirectory
     */
    public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
    {
        $controller = $this->buildController();

        $request = new Request([], [], [], [], [], [], $requestContent);
        $request->setMethod('PROPFIND');
        $request->headers->add(['Depth' => '0']);
        $response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);

        self::assertEquals($expectedStatusCode, $response->getStatusCode());
        self::assertContains('content-type', $response->headers->keys());
        self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
        self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
        self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
    }

    public static function generateDataPropfindDirectory(): iterable
    {
        yield [
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
            <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
            XML,
            207,
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
            <d:multistatus xmlns:d="DAV:">
                <d:response>
                    <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
                    <d:propstat>
                        <d:prop>
                            <d:resourcetype><d:collection/></d:resourcetype>
                            <d:getcontenttype>httpd/unix-directory</d:getcontenttype>
                            <!--
                            <d:supportedlock>
                                <d:lockentry>
                                    <d:lockscope><d:exclusive/></d:lockscope>
                                    <d:locktype><d:write/></d:locktype>
                                </d:lockentry>
                                <d:lockentry>
                                    <d:lockscope><d:shared/></d:lockscope>
                                    <d:locktype><d:write/></d:locktype>
                                </d:lockentry>
                            </d:supportedlock>
                            -->
                        </d:prop>
                        <d:status>HTTP/1.1 200 OK</d:status>
                    </d:propstat>
                    <d:propstat>
                        <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
                            <ns0:IsReadOnly/>
                        </d:prop>
                        <d:status>HTTP/1.1 404 Not Found</d:status>
                    </d:propstat>
                </d:response>
            </d:multistatus>
            XML,
            'test resourceType and IsReadOnly ',
        ];

        yield [
            <<<'XML'
            <?xml version="1.0" encoding="UTF-8"?>
            <propfind xmlns="DAV:"><prop><CreatableContentsInfo xmlns="http://ucb.openoffice.org/dav/props/"/></prop></propfind>
            XML,
            207,
            <<<'XML'
            <?xml version="1.0" encoding="utf-8"?>
            <d:multistatus xmlns:d="DAV:">
                <d:response>
                    <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
                    <d:propstat>
                        <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/" >
                            <ns0:CreatableContentsInfo/>
                        </d:prop>
                        <d:status>HTTP/1.1 404 Not Found</d:status>
                    </d:propstat>
                </d:response>
            </d:multistatus>
            XML,
            'test creatableContentsInfo',
        ];
    }

    public function testHeadDocument(): void
    {
        $controller = $this->buildController();
        $response = $controller->headDocument($this->buildDocument());

        self::assertEquals(200, $response->getStatusCode());
        self::assertContains('content-length', $response->headers->keys());
        self::assertContains('content-type', $response->headers->keys());
        self::assertContains('etag', $response->headers->keys());
        self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
        self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
        self::assertEquals(5, $response->headers->get('content-length'));
    }

    public function testPutDocument(): void
    {
        $document = $this->buildDocument();
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);

        // entity manager must be flushed
        $entityManager->expects($this->once())
            ->method('flush');

        // object must be written by StoredObjectManager
        $storedObjectManager->expects($this->once())
            ->method('write')
            ->with($this->identicalTo($document), $this->identicalTo('1234'));

        $controller = $this->buildController($entityManager, $storedObjectManager);

        $request = new Request(content: '1234');
        $response = $controller->putDocument($document, $request);

        self::assertEquals(204, $response->getStatusCode());
        self::assertEquals('', $response->getContent());
    }
}

class MockedStoredObjectManager implements StoredObjectManagerInterface
{
    public function getLastModified(StoredObject|StoredObjectVersion $document): \DateTimeInterface
    {
        return new \DateTimeImmutable('2023-09-13T14:15', new \DateTimeZone('+02:00'));
    }

    public function getContentLength(StoredObject|StoredObjectVersion $document): int
    {
        return 5;
    }

    public function read(StoredObject|StoredObjectVersion $document): string
    {
        return 'abcde';
    }

    public function write(StoredObject $document, string $clearContent, ?string $contentType = null): StoredObjectVersion
    {
        return $document->registerVersion();
    }

    public function etag(StoredObject|StoredObjectVersion $document): string
    {
        return 'ab56b4d92b40713acc5af89985d4b786';
    }

    public function clearCache(): void {}

    public function delete(StoredObjectVersion $storedObjectVersion): void {}

    public function exists(StoredObject|StoredObjectVersion $document): bool
    {
        return true;
    }
}
