<?php

namespace Velis\Test\Db;

use InvalidArgumentException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use Velis\Cache\CacheInterface;
use Velis\Db\Db;
use Velis\Db\EntityCache;
use Velis\Db\EntityRepository;
use Velis\Label;
use Velis\User;

class EntityCacheTest extends TestCase
{
    private EntityCache $entityCache;

    private EntityRepository&MockObject $repositoryMock;
    private CacheInterface&MockObject $cacheMock;

    /**
     * @throws ReflectionException
     */
    protected function setUp(): void
    {
        $this->repositoryMock = $this->createMock(EntityRepository::class);

        $dbMock = $this->createMock(Db::class);
        $dbMock->method('getRepository')
            ->willReturn($this->repositoryMock);

        $entityClass = Label::class;

        $this->cacheMock = $this->createMock(CacheInterface::class);

        $this->entityCache = new EntityCache($dbMock, $entityClass, $this->cacheMock);
    }

    /**
     * @throws ReflectionException
     */
    public function testConstructWhenEntityIsNotCacheable(): void
    {
        $this->expectException(InvalidArgumentException::class);

        new EntityCache($this->createMock(Db::class), User::class, $this->cacheMock);
    }

    /**
     * @throws ReflectionException
     */
    public function testGetCachedListWhenCacheEntryExists(): void
    {
        $entityMock = $this->createMock(Label::class);

        $this->cacheMock->expects($this->once())
            ->method('has')
            ->willReturn(true)
        ;

        $this->cacheMock->expects($this->once())
            ->method('get')
            ->willReturn([1 => $entityMock])
        ;

        $this->cacheMock->expects($this->never())
            ->method('set');

        $this->repositoryMock->expects($this->never())
            ->method('findAll');

        $list = $this->entityCache->getCachedList();

        $this->assertIsArray($list);
        $this->assertNotEmpty($list);
        $this->assertSame($entityMock, reset($list));
    }

    /**
     * @throws ReflectionException
     */
    public function testGetCachedListWhenCacheEntryDoesNotExist(): void
    {
        $entityMock = $this->createMock(Label::class);

        $this->cacheMock->expects($this->once())
            ->method('has')
            ->willReturn(false)
        ;

        $this->cacheMock->expects($this->never())
            ->method('get');

        $this->cacheMock->expects($this->once())
            ->method('set')
            ->with('entities_' . str_replace('\\', '_', Label::class), [1 => $entityMock])
        ;

        $this->repositoryMock->expects($this->once())
            ->method('findAll')
            ->willReturn([1 => $entityMock])
        ;

        $list = $this->entityCache->getCachedList();

        $this->assertIsArray($list);
        $this->assertNotEmpty($list);
        $this->assertSame($entityMock, reset($list));
    }

    /**
     * @throws ReflectionException
     */
    public function testGetWhenCacheEntryExists(): void
    {
        $entityMock = $this->createMock(Label::class);

        $this->cacheMock->expects($this->once())
            ->method('has')
            ->willReturn(true)
        ;

        $this->cacheMock->expects($this->once())
            ->method('get')
            ->willReturn([1 => $entityMock])
        ;

        $this->cacheMock->expects($this->never())
            ->method('set');

        $this->repositoryMock->expects($this->never())
            ->method('findAll');

        $entity = $this->entityCache->get(1);

        $this->assertInstanceOf(Label::class, $entity);
        $this->assertSame($entityMock, $entity);
    }

    /**
     * @throws ReflectionException
     */
    public function testGetWhenCacheEntryDoesNotExist(): void
    {
        $entityMock = $this->createMock(Label::class);

        $this->cacheMock->expects($this->once())
            ->method('has')
            ->willReturn(false)
        ;

        $this->cacheMock->expects($this->never())
            ->method('get');

        $this->cacheMock->expects($this->once())
            ->method('set')
            ->with('entities_' . str_replace('\\', '_', Label::class), [1 => $entityMock])
        ;

        $this->repositoryMock->expects($this->once())
            ->method('findAll')
            ->willReturn([1 => $entityMock])
        ;

        $entity = $this->entityCache->get(1);

        $this->assertInstanceOf(Label::class, $entity);
        $this->assertSame($entityMock, $entity);
    }

    /**
     * @throws ReflectionException
     */
    public function testGetWhenEntryDoesNotExistInDb(): void
    {
        $this->cacheMock->expects($this->once())
            ->method('has')
            ->willReturn(false)
        ;

        $this->cacheMock->expects($this->never())
            ->method('get');

        $this->cacheMock->expects($this->once())
            ->method('set');

        $this->repositoryMock->expects($this->once())
            ->method('findAll')
            ->willReturn([])
        ;

        $entity = $this->entityCache->get(-1);

        $this->assertNull($entity);
    }

    public function testUnset(): void
    {
        $this->cacheMock->expects($this->once())
            ->method('delete');

        $this->entityCache->unset();
    }
}
