<?php

namespace Velis\Test\Http\Response;

use DateTime;
use Exception;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Velis\App;
use Velis\Filesystem\FilesystemInterface;
use Velis\Http\Response;
use Velis\Http\Response\CachedFileResponseBuilder;
use Velis\Mvc\Controller\Exception\BadRequestException;

/**
 * @author Jan Małysiak <jan.malysiak@velistech.com>
 * @author Maciej Borowiec <maciej.borowiec@singu.com>
 */
class CachedFileResponseBuilderTest extends TestCase
{
    private CachedFileResponseBuilder $builder;

    /**
     * @var FilesystemInterface|MockObject
     */
    private $filesystemMock;

    protected function setUp(): void
    {
        App::$cache->clear();
        $response = new Response();
        $this->filesystemMock = $this->createMock(FilesystemInterface::class);

        $this->builder = new CachedFileResponseBuilder($response, $this->filesystemMock);
    }

    /**
     * @param int $filesystemMtime
     * @param string $ifModifiedSince
     * @param string $expireAfter
     * @param DateTime $expectedExpires
     * @return void
     * @throws Exception
     *
     * @dataProvider provideTestDataForGetResponseWithNotModifiedData
     */
    public function testGetResponseNotModified(int $filesystemMtime, string $ifModifiedSince, string $expireAfter, DateTime $expectedExpires): void
    {
        $this->filesystemMock->method('mtime')
            ->willReturn($filesystemMtime);

        $response = $this->builder->getResponse('path/123.jpg', true, $ifModifiedSince, $expireAfter);

        self::assertEquals(304, $response->getStatusCode());
        self::assertEmpty($response->getContent());
        $actualExpires = new DateTime($response->getHeaders()->get('Expires'));
        // delta argument allows to include the interval between execution of data provider and actual test
        self::assertEqualsWithDelta($expectedExpires->getTimestamp(), $actualExpires->getTimestamp(), 300);
    }

    /**
     * @return array
     */
    public function provideTestDataForGetResponseWithNotModifiedData(): array
    {
        $now = new DateTime();

        $mtime = clone $now;
        $mtime->modify('-2 weeks');

        $ifModifiedSince = clone $now;
        $ifModifiedSince->modify('-1 day');

        $yearLater = clone $now;
        $yearLater->modify('1 year');

        $monthLater = clone $now;
        $monthLater->modify('+1 month');

        $ifModifiedSinceEarlierThanMtime = clone $now;
        $ifModifiedSinceEarlierThanMtime->modify('-3 weeks');

        return [
            [$mtime->getTimestamp(), $ifModifiedSince->format('r'), '1 year', $yearLater],
            [$mtime->getTimestamp(), $ifModifiedSince->format('r'), '1 month', $monthLater],
        ];
    }

    /**
     * @param int $filesystemMtime
     * @param string|null $ifModifiedSince
     * @param array $expectedHeaders
     * @param string $expectedContent
     * @return void
     * @throws Exception
     *
     * @dataProvider provideTestDataForGetResponseWithContentUsingCache
     */
    public function testGetResponseWithContentUsingCache(int $filesystemMtime, ?string $ifModifiedSince, array $expectedHeaders, string $expectedContent): void
    {
        $this->filesystemMock->method('mtime')
            ->willReturn($filesystemMtime);

        $this->filesystemMock->method('mimeType')
            ->willReturn($expectedHeaders['Content-Type']);

        $this->filesystemMock->method('read')
            ->willReturn($expectedContent);

        $response = $this->builder->getResponse('path/123.jpg', true, $ifModifiedSince);

        self::assertEquals($expectedContent, $response->getContent());

        $actualHeaders = $response->getHeaders();

        foreach ($expectedHeaders as $headerName => $expectedValue) {
            self::assertTrue($actualHeaders->has($headerName));

            if ($expectedValue instanceof DateTime) {
                $actualValue = new DateTime($actualHeaders->get($headerName));
                self::assertEqualsWithDelta($expectedValue->getTimestamp(), $actualValue->getTimestamp(), 300);
            } else {
                self::assertEquals($expectedValue, $actualHeaders->get($headerName));
            }
        }
    }

    /**
     * @return array
     * @throws Exception
     */
    public function provideTestDataForGetResponseWithContentUsingCache(): array
    {
        $now = new DateTime();

        $mtime = clone $now;
        $mtime->modify('-3 weeks');

        $yearLater = clone $now;
        $yearLater->modify('+1 year');

        $ifModifiedSince = clone $now;
        $ifModifiedSince->modify('-2 months');

        return [
            [
                $mtime->getTimestamp(),
                null,
                [
                    'Content-Type' => 'image/jpeg',
                    'Last-Modified' => $mtime,
                    'Expires' => $yearLater,
                ],
                bin2hex(random_bytes(200)),
            ],
            [
                $mtime->getTimestamp(),
                $ifModifiedSince->format('r'),
                [
                    'Content-Type' => 'image/jpeg',
                    'Last-Modified' => $mtime,
                    'Expires' => $yearLater,
                ],
                bin2hex(random_bytes(200)),
            ],
        ];
    }

    /**
     * @param string $datetime
     * @param string $expectedException
     * @return void
     * @throws Exception
     * @dataProvider provideTestDataForGetResponseWithInvalidArguments
     */
    public function testGetResponseWithInvalidArguments(string $datetime, string $expectedException): void
    {
        $this->expectException($expectedException);

        $this->builder->getResponse('path/123.jpg', true, $datetime);
    }

    /**
     * @return array
     */
    public function provideTestDataForGetResponseWithInvalidArguments(): array
    {
        return [
            [
                'someStringWithInvalidDate',
                BadRequestException::class,
            ],
        ];
    }

    /**
     * @param int $filesystemMtime
     * @param string $expectedMimeType
     * @param string $expectedContent
     * @return void
     * @throws Exception
     *
     * @dataProvider provideTestDataForGetResponseWithContentWithCacheDisabled
     */
    public function testGetResponseWithContentWithCacheDisabled(int $filesystemMtime, string $expectedMimeType, string $expectedContent): void
    {
        $this->filesystemMock->method('mtime')
            ->willReturn($filesystemMtime);

        $this->filesystemMock->method('mimeType')
            ->willReturn($expectedMimeType);

        $this->filesystemMock->method('read')
            ->willReturn($expectedContent);

        $response = $this->builder->getResponse('path/abcd.txt', false);

        self::assertEquals($expectedContent, $response->getContent());

        $headers = $response->getHeaders();

        self::assertTrue($headers->has('Content-Type'));
        self::assertEquals($expectedMimeType, $headers->get('Content-Type'));

        self::assertFalse($headers->has('Last-Modified'));
        self::assertFalse($headers->has('Expires'));
    }

    /**
     * @return array
     * @throws Exception
     */
    public function provideTestDataForGetResponseWithContentWithCacheDisabled(): array
    {
        $mtime = new DateTime();
        $mtime->modify('-3 days');

        return [
            [$mtime->getTimestamp(), 'text/plain', bin2hex(random_bytes(100))],
        ];
    }
}
