<?php

declare(strict_types=1);

namespace NoahVet\Reef\Test\A_Unit\Plugin\Cache;

use Http\Client\Common\Deferred;
use Http\Promise\Promise;
use NoahVet\Reef\Plugin\Cache\HttpResponseCache;
use NoahVet\Reef\Plugin\Cache\Model\CacheableResponse;
use NoahVet\Reef\Plugin\Cache\Model\Factory\CacheableResponseFactory;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

class HttpResponseCacheTest extends TestCase
{
    private CacheableResponseFactory $cacheableResponseFactory;

    private RequestInterface $request;

    private ResponseInterface $response;

    private StreamInterface $body;

    private Deferred $deferedSuccess;

    private Deferred $deferedFailure;

    private ClientExceptionInterface $exception;

    protected function setUp(): void
    {
        $this->cacheableResponseFactory = $this->createMock(CacheableResponseFactory::class);

        $this->request = $this->createMock(RequestInterface::class);
        $this->response = $this->createMock(ResponseInterface::class);
        $this->body = $this->createMock(StreamInterface::class);

        $this->request->method('getMethod')->willReturn('GET');

        $this->exception = $this->createMock(ClientExceptionInterface::class);

        $this->deferedSuccess = new Deferred(fn () => $this->deferedSuccess->resolve($this->response));
        $this->deferedFailure = new Deferred(fn () => $this->deferedFailure->reject($this->exception));
    }

    public function testCacheResponseWithNonGetRequest(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('POST');

        $this->assertSame(
            $this->response,
            $httpResponseCache->cacheResponse($this->request, $this->response),
        );
    }

    public function testCacheResponseWithResponseTooLarge(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->body->method('getSize')->willReturn(HttpResponseCache::MAX_RESPONSE_CACHE_SIZE + 1);
        $this->response->method('getBody')->willReturn($this->body);

        $this->assertSame(
            $this->response,
            $httpResponseCache->cacheResponse($this->request, $this->response),
        );
    }

    public function testCacheResponseWithNoCacheHeader(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->body->method('getSize')->willReturn(100);
        $this->response->method('getHeaderLine')->with('Cache-Control')->willReturn('no-cache');
        $this->response->method('getBody')->willReturn($this->body);

        $this->assertSame(
            $this->response,
            $httpResponseCache->cacheResponse($this->request, $this->response),
        );
    }

    public function testCacheResponseWithSuccessfulCacheSave(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            new CacheableResponseFactory(),
        );

        $this->body->method('getSize')->willReturn(100);
        $this->response->method('getStatusCode')->willReturn(200);
        $this->response->method('getBody')->willReturn($this->body);
        $this->response->method('getHeaderLine')->willReturnCallback(fn (string $header) => match ($header) {
            'Cache-Control' => 'max-age=3600',
            'Date' => (new \DateTimeImmutable())->format(\DATE_RFC2822),
            default => '',
        });

        $cachedResponse = $httpResponseCache->cacheResponse($this->request, $this->response);

        $this->assertSame(
            $this->response->getStatusCode(),
            $cachedResponse->getStatusCode(),
        );
        $this->assertSame(
            $this->response->getBody(),
            $cachedResponse->getBody(),
        );
    }

    public function testCacheResponseWithUnsuccessfulCacheSave(): void
    {
        $httpResponseCache = new HttpResponseCache(
            $this->createFailingCache(),
            $this->cacheableResponseFactory,
        );

        $this->body->method('getSize')->willReturn(100);
        $this->response->method('getBody')->willReturn($this->body);
        $this->response->method('getHeaderLine')->willReturnCallback(fn (string $header) => match ($header) {
            'Cache-Control' => 'max-age=3600',
            'Date' => (new \DateTimeImmutable())->format(\DATE_RFC2822),
            default => '',
        });

        $this->assertSame(
            $this->response,
            $httpResponseCache->cacheResponse($this->request, $this->response),
        );
    }

    public function testGetCachedResponseWithNonGetRequest(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('POST');

        $this->assertNull($httpResponseCache->getCachedResponse($this->request));
    }

    public function testGetCachedResponseWithCacheMiss(): void
    {
        $this->request
            ->method('getMethod')
            ->willReturn('GET')
        ;

        $cache = $this->createMock(CacheItemPoolInterface::class);
        $cacheItem = $this->createMock(CacheItemInterface::class);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(false);

        $httpResponseCache = new HttpResponseCache(
            $cache,
            $this->cacheableResponseFactory,
        );

        $this->assertNull($httpResponseCache->getCachedResponse($this->request));
    }

    public function testGetCachedResponseWithCacheHit(): void
    {
        $this->request
            ->method('getMethod')
            ->willReturn('GET')
        ;

        $cache = $this->createMock(CacheItemPoolInterface::class);
        $cacheItem = $this->createMock(CacheItemInterface::class);

        $cachedResponse = $this->createMock(CacheableResponse::class);
        $cachedResponse->method('getStatusCode')->willReturn(200);
        $cachedResponse->method('getBody')->willReturn($this->body);

        $cache->method('getItem')->willReturn($cacheItem);
        $cacheItem->method('isHit')->willReturn(true);
        $cacheItem->method('get')->willReturn($cachedResponse);

        $httpResponseCache = new HttpResponseCache(
            $cache,
            $this->cacheableResponseFactory,
        );

        $cachedResponseReturned = $httpResponseCache->getCachedResponse($this->request);

        $this->assertSame(
            $cachedResponse->getStatusCode(),
            $cachedResponseReturned->getStatusCode(),
        );
        $this->assertSame(
            $cachedResponse->getBody(),
            $cachedResponseReturned->getBody(),
        );
    }

    public function testCacheResponsePromiseWithNonGetRequest(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('POST');
        $responsePromise = $this->createMock(Promise::class);

        $resultPromise = $httpResponseCache->cacheResponsePromise($this->request, $responsePromise);

        $this->assertInstanceOf(Promise::class, $resultPromise);
    }

    public function testCacheResponsePromiseWithResponseTooLarge(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('GET');

        $this->body->method('getSize')->willReturn(HttpResponseCache::MAX_RESPONSE_CACHE_SIZE + 1);
        $this->response->method('getBody')->willReturn($this->body);

        $resultPromise = $httpResponseCache->cacheResponsePromise($this->request, $this->deferedSuccess);

        $response = $resultPromise->wait();
        $this->assertSame($this->response, $response);
    }

    public function testCacheResponsePromiseWithResponseNoCacheHeader(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('GET');

        $this->body->method('getSize')->willReturn(100);
        $this->response->method('getHeaderLine')->with('Cache-Control')->willReturn('no-cache');
        $this->response->method('getBody')->willReturn($this->body);

        $resultPromise = $httpResponseCache->cacheResponsePromise($this->request, $this->deferedSuccess);

        $response = $resultPromise->wait();
        $this->assertSame($this->response, $response);
    }

    public function testCacheResponsePromiseWithSuccessfulCacheSave(): void
    {
        $httpResponseCache = new HttpResponseCache(
            new ArrayAdapter(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('GET');

        $this->body->method('getSize')->willReturn(100);
        $this->response->method('getBody')->willReturn($this->body);
        $this->response->method('getHeaderLine')->willReturnCallback(fn (string $header) => match ($header) {
            'Cache-Control' => 'max-age=3600',
            'Date' => (new \DateTimeImmutable())->format(\DATE_RFC2822),
            default => '',
        });

        $cacheableResponse = $this->createMock(CacheableResponse::class);
        $cacheableResponse->method('getStatusCode')->willReturn(200);
        $cacheableResponse->method('getBody')->willReturn($this->body);
        $this->cacheableResponseFactory->method('create')->willReturn($cacheableResponse);

        $resultPromise = $httpResponseCache->cacheResponsePromise($this->request, $this->deferedSuccess);
        $response = $resultPromise->wait();

        $this->assertSame($cacheableResponse->getStatusCode(), $response->getStatusCode());
        $this->assertSame($cacheableResponse->getBody(), $response->getBody());
    }

    public function testCacheResponsePromiseWithUnsuccessfulCacheSave(): void
    {
        $httpResponseCache = new HttpResponseCache(
            $this->createFailingCache(),
            $this->cacheableResponseFactory,
        );

        $this->request->method('getMethod')->willReturn('GET');

        $this->body->method('getSize')->willReturn(100);
        $this->response->method('getBody')->willReturn($this->body);
        $this->response->method('getHeaderLine')->willReturnCallback(fn (string $header) => match ($header) {
            'Cache-Control' => 'max-age=3600',
            'Date' => (new \DateTimeImmutable())->format(\DATE_RFC2822),
            default => '',
        });

        $resultPromise = $httpResponseCache->cacheResponsePromise($this->request, $this->deferedSuccess);
        $response = $resultPromise->wait();

        $this->assertSame($this->response, $response);
    }

    private function createFailingCache(): CacheItemPoolInterface
    {
        return new class extends ArrayAdapter {
            public function save(CacheItemInterface $item): bool
            {
                return false;
            }
        };
    }
}
