<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Locking;

use DateTime;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Version;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;

use function date;
use function strtotime;

class OptimisticTest extends OrmFunctionalTestCase
{
    private Connection $_conn;

    protected function setUp(): void
    {
        parent::setUp();

        $this->_conn = $this->_em->getConnection();
    }

    private function createSchema(): void
    {
        $this->createSchemaForModels(
            OptimisticJoinedParent::class,
            OptimisticJoinedChild::class,
            OptimisticStandard::class,
            OptimisticTimestamp::class,
        );
    }

    public function testJoinedChildInsertSetsInitialVersionValue(): OptimisticJoinedChild
    {
        $this->createSchema();
        $test = new OptimisticJoinedChild();

        $test->name     = 'child';
        $test->whatever = 'whatever';

        $this->_em->persist($test);
        $this->_em->flush();

        self::assertEquals(1, $test->version);

        return $test;
    }

    #[Depends('testJoinedChildInsertSetsInitialVersionValue')]
    public function testJoinedChildFailureThrowsException(OptimisticJoinedChild $child): void
    {
        $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Functional\Locking\OptimisticJoinedChild t WHERE t.id = :id');

        $q->setParameter('id', $child->id);

        $test = $q->getSingleResult();

        // Manually update/increment the version so we can try and save the same
        // $test and make sure the exception is thrown saying the record was
        // changed or updated since you read it
        $this->_conn->executeQuery('UPDATE optimistic_joined_parent SET version = ? WHERE id = ?', [2, $test->id]);

        // Now lets change a property and try and save it again
        $test->whatever = 'ok';

        try {
            $this->_em->flush();
        } catch (OptimisticLockException $e) {
            self::assertSame($test, $e->getEntity());
        }
    }

    public function testJoinedParentInsertSetsInitialVersionValue(): OptimisticJoinedParent
    {
        $this->createSchema();
        $test = new OptimisticJoinedParent();

        $test->name = 'parent';

        $this->_em->persist($test);
        $this->_em->flush();

        self::assertEquals(1, $test->version);

        return $test;
    }

    #[Depends('testJoinedParentInsertSetsInitialVersionValue')]
    public function testJoinedParentFailureThrowsException(OptimisticJoinedParent $parent): void
    {
        $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Functional\Locking\OptimisticJoinedParent t WHERE t.id = :id');

        $q->setParameter('id', $parent->id);

        $test = $q->getSingleResult();

        // Manually update/increment the version so we can try and save the same
        // $test and make sure the exception is thrown saying the record was
        // changed or updated since you read it
        $this->_conn->executeQuery('UPDATE optimistic_joined_parent SET version = ? WHERE id = ?', [2, $test->id]);

        // Now lets change a property and try and save it again
        $test->name = 'WHATT???';

        try {
            $this->_em->flush();
        } catch (OptimisticLockException $e) {
            self::assertSame($test, $e->getEntity());
        }
    }

    public function testMultipleFlushesDoIncrementalUpdates(): void
    {
        $this->createSchema();

        $test = new OptimisticStandard();

        for ($i = 0; $i < 5; $i++) {
            $test->name = 'test' . $i;

            $this->_em->persist($test);
            $this->_em->flush();

            self::assertIsInt($test->getVersion());
            self::assertEquals($i + 1, $test->getVersion());
        }
    }

    public function testStandardInsertSetsInitialVersionValue(): OptimisticStandard
    {
        $this->createSchema();

        $test = new OptimisticStandard();

        $test->name = 'test';

        $this->_em->persist($test);
        $this->_em->flush();

        self::assertIsInt($test->getVersion());
        self::assertEquals(1, $test->getVersion());

        return $test;
    }

    #[Depends('testStandardInsertSetsInitialVersionValue')]
    public function testStandardFailureThrowsException(OptimisticStandard $entity): void
    {
        $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Functional\Locking\OptimisticStandard t WHERE t.id = :id');

        $q->setParameter('id', $entity->id);

        $test = $q->getSingleResult();

        // Manually update/increment the version so we can try and save the same
        // $test and make sure the exception is thrown saying the record was
        // changed or updated since you read it
        $this->_conn->executeQuery('UPDATE optimistic_standard SET version = ? WHERE id = ?', [2, $test->id]);

        // Now lets change a property and try and save it again
        $test->name = 'WHATT???';

        try {
            $this->_em->flush();
        } catch (OptimisticLockException $e) {
            self::assertSame($test, $e->getEntity());
        }
    }

    #[DoesNotPerformAssertions]
    public function testLockWorksWithProxy(): void
    {
        $this->createSchema();
        $test       = new OptimisticStandard();
        $test->name = 'test';

        $this->_em->persist($test);
        $this->_em->flush();
        $this->_em->clear();

        $proxy = $this->_em->getReference(OptimisticStandard::class, $test->id);

        $this->_em->lock($proxy, LockMode::OPTIMISTIC, 1);
    }

    public function testOptimisticTimestampSetsDefaultValue(): OptimisticTimestamp
    {
        $this->createSchema();
        $test = new OptimisticTimestamp();

        $test->name = 'Testing';

        self::assertNull($test->version, 'Pre-Condition');

        $this->_em->persist($test);
        $this->_em->flush();

        self::assertInstanceOf(DateTime::class, $test->version);

        return $test;
    }

    #[Depends('testOptimisticTimestampSetsDefaultValue')]
    public function testOptimisticTimestampFailureThrowsException(OptimisticTimestamp $entity): void
    {
        $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Functional\Locking\OptimisticTimestamp t WHERE t.id = :id');

        $q->setParameter('id', $entity->id);

        $test = $q->getSingleResult();

        self::assertInstanceOf(DateTime::class, $test->version);

        // Manually increment the version datetime column
        $format = $this->_em->getConnection()->getDatabasePlatform()->getDateTimeFormatString();

        $this->_conn->executeQuery('UPDATE optimistic_timestamp SET version = ? WHERE id = ?', [date($format, strtotime($test->version->format($format)) + 3600), $test->id]);

        // Try and update the record and it should throw an exception
        $caughtException = null;
        $test->name      = 'Testing again';

        try {
            $this->_em->flush();
        } catch (OptimisticLockException $e) {
            $caughtException = $e;
        }

        self::assertNotNull($caughtException, 'No OptimisticLockingException was thrown');
        self::assertSame($test, $caughtException->getEntity());
    }

    #[Depends('testOptimisticTimestampSetsDefaultValue')]
    public function testOptimisticTimestampLockFailureThrowsException(OptimisticTimestamp $entity): void
    {
        $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Functional\Locking\OptimisticTimestamp t WHERE t.id = :id');

        $q->setParameter('id', $entity->id);

        $test = $q->getSingleResult();

        self::assertInstanceOf(DateTime::class, $test->version);

        // Try to lock the record with an older timestamp and it should throw an exception
        $caughtException = null;

        try {
            $expectedVersionExpired = DateTime::createFromFormat(
                'U',
                (string) ($test->version->getTimestamp() - 3600),
            );

            $this->_em->lock($test, LockMode::OPTIMISTIC, $expectedVersionExpired);
        } catch (OptimisticLockException $e) {
            $caughtException = $e;
        }

        self::assertNotNull($caughtException, 'No OptimisticLockingException was thrown');
        self::assertSame($test, $caughtException->getEntity());
    }
}

#[Table(name: 'optimistic_joined_parent')]
#[Entity]
#[InheritanceType('JOINED')]
#[DiscriminatorColumn(name: 'discr', type: 'string', length: 255)]
#[DiscriminatorMap(['parent' => 'OptimisticJoinedParent', 'child' => 'OptimisticJoinedChild'])]
class OptimisticJoinedParent
{
    /** @var int */
    #[Id]
    #[Column(type: 'integer')]
    #[GeneratedValue(strategy: 'AUTO')]
    public $id;

    /** @var string */
    #[Column(type: 'string', length: 255)]
    public $name;

    /** @var int */
    #[Version]
    #[Column(type: 'integer')]
    public $version;
}


#[Table(name: 'optimistic_joined_child')]
#[Entity]
class OptimisticJoinedChild extends OptimisticJoinedParent
{
    /** @var string */
    #[Column(type: 'string', length: 255)]
    public $whatever;
}

#[Table(name: 'optimistic_standard')]
#[Entity]
class OptimisticStandard
{
    /** @var int */
    #[Id]
    #[Column(type: 'integer')]
    #[GeneratedValue(strategy: 'AUTO')]
    public $id;

    /** @var string */
    #[Column(type: 'string', length: 255)]
    public $name;

    #[Version]
    #[Column(type: 'integer')]
    private int $version;

    public function getVersion(): int
    {
        return $this->version;
    }
}

#[Table(name: 'optimistic_timestamp')]
#[Entity]
class OptimisticTimestamp
{
    /** @var int */
    #[Id]
    #[Column(type: 'integer')]
    #[GeneratedValue(strategy: 'AUTO')]
    public $id;

    /** @var string */
    #[Column(type: 'string', length: 255)]
    public $name;

    /** @var DateTime */
    #[Version]
    #[Column(type: 'datetime')]
    public $version;
}
