<?php
/**
 * @copyright   Copyright (C) 2010-2024 Combodo SAS
 * @license     http://opensource.org/licenses/AGPL-3.0
 */

namespace Combodo\iTop\Test\UnitTest\Core;

use CMDBSource;
use Combodo\iTop\Core\DbConnectionWrapper;
use Combodo\iTop\Test\UnitTest\ItopTestCase;
use Exception;
use MetaModel;
use MySQLTransactionNotClosedException;

/**
 * @group itopRequestMgmt
 * @group specificOrgInSampleData
 * Class TransactionsTest
 *
 * @package Combodo\iTop\Test\UnitTest\Core
 */
class TransactionsTest extends ItopTestCase
{
	/** @var DeadLockInjection */
	private $oMySQLiMock;

	protected function setUp(): void
	{
		parent::setUp();
		require_once('DeadLockInjection.php');
		$this->RequireOnceItopFile('/core/cmdbsource.class.inc.php');
		$sEnv = 'production';
		$sConfigFile = APPCONF.$sEnv.'/config-itop.php';

		MetaModel::Startup($sConfigFile, false, true, false, $sEnv);

		$oInitialMysqli = CMDBSource::GetMysqli();
		$this->oMySQLiMock = new DeadLockInjection();

		$oMockMysqli = $this->getMockBuilder('mysqli')
			->setMethods(['query'])
			->getMock();
		$oMockMysqli->expects($this->any())
			->method('query')
			->will($this->returnCallback(
				function ($sSql) use ($oInitialMysqli) {
					$this->oMySQLiMock->query($sSql);
					return $oInitialMysqli->query($sSql);
				}
			));

		DbConnectionWrapper::SetDbConnectionMockForQuery($oMockMysqli);
	}

	/**
	 * Test DBInsertNoReload database transaction by provoking deadlock exceptions
	 *
	 * @dataProvider DBInsertProvider
	 *
	 * @param int $iFailAt  Specify the request occurrence that fails
	 * @param bool $bIsInDB Indicates if the object must have been created or not
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreCannotSaveObjectException
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \DeleteException
	 * @throws \MySQLException
	 * @throws \MySQLHasGoneAwayException
	 * @throws \OQLException
	 */
	public function testDBInsert($iFailAt, $bIsInDB)
	{
		// Create a UserRequest with 2 contacts
		$oTicket = MetaModel::NewObject('UserRequest', [
			'ref' => 'Test Ticket',
			'title' => 'Create OK',
			'description' => 'Create OK',
			'caller_id' => 15,
			'org_id' => 3,
		]);
		$oLinkSet = $oTicket->Get('contacts_list');
		$oLinkSet->AddItem(MetaModel::NewObject('lnkContactToTicket', ['contact_id' => 6]));
		$oLinkSet->AddItem(MetaModel::NewObject('lnkContactToTicket', ['contact_id' => 7]));
		$oTicket->Set('contacts_list', $oLinkSet);

		$this->oMySQLiMock->SetFailAt($iFailAt);
		$this->debug("---> DBInsert()");
		try {
			$oTicket->DBWrite();
		}
		catch (Exception $e) {
			// If an exception occurs must be a deadlock
			$this->assertTrue(CMDBSource::IsDeadlockException($e), $e->getMessage());
		}

		// Verify if the ticket is considered as saved in the database
		$this->assertEquals($bIsInDB, !$oTicket->IsNew(), " The ticket should be persisted in the DB");

		if (!$oTicket->IsNew()) {
			$this->oMySQLiMock->SetShowRequest(false);
			// Delete created objects
			$oTicket->DBDelete();
		}
	}

	/**
	 * This test case was originaly in DBInsertProvider
	 * @runInSeparateProcess Failing when run in the same process as other...
	 */
	public function testDBInsertCaseHistory38()
	{
		$this->testDBInsert(40, false);
	}

	public function DBInsertProvider()
	{
		return [
			"Normal case" => ['iFailAt' => -1, 'bIsInDB' => true],
			"ticket" => ['iFailAt' => 1, 'bIsInDB' => false],
			"ticket_request" => ['iFailAt' => 2, 'bIsInDB' => false],
			"priv_change" => ['iFailAt' => 3, 'bIsInDB' => false],
			"priv_changeop" => ['iFailAt' => 4, 'bIsInDB' => false],
			"priv_changeop_create" => ['iFailAt' => 5, 'bIsInDB' => false],
			"History 4" => ['iFailAt' => 6, 'bIsInDB' => false],
			"History 5" => ['iFailAt' => 7, 'bIsInDB' => false],
			"History 6" => ['iFailAt' => 8, 'bIsInDB' => false],
			"History 7" => ['iFailAt' => 9, 'bIsInDB' => false],
			"History 8" => ['iFailAt' => 10, 'bIsInDB' => false],
			"History 9" => ['iFailAt' => 11, 'bIsInDB' => false],
			"History 10" => ['iFailAt' => 12, 'bIsInDB' => false],
			"History 11" => ['iFailAt' => 13, 'bIsInDB' => false],
			"History 12" => ['iFailAt' => 14, 'bIsInDB' => false],
			"History 13" => ['iFailAt' => 15, 'bIsInDB' => false],
			"History 14" => ['iFailAt' => 16, 'bIsInDB' => false],
			"History 15" => ['iFailAt' => 17, 'bIsInDB' => false],
			"History 16" => ['iFailAt' => 18, 'bIsInDB' => false],
			"History 17" => ['iFailAt' => 19, 'bIsInDB' => false],
			"History 18" => ['iFailAt' => 20, 'bIsInDB' => false],
			"History 19" => ['iFailAt' => 21, 'bIsInDB' => false],
			"History 20" => ['iFailAt' => 22, 'bIsInDB' => false],
			"History 21" => ['iFailAt' => 23, 'bIsInDB' => false],
			"History 22" => ['iFailAt' => 24, 'bIsInDB' => false],
			"History 23" => ['iFailAt' => 25, 'bIsInDB' => false],
			"History 24" => ['iFailAt' => 26, 'bIsInDB' => false],
			"History 25" => ['iFailAt' => 27, 'bIsInDB' => false],
			"History 26" => ['iFailAt' => 28, 'bIsInDB' => false],
			"History 27" => ['iFailAt' => 29, 'bIsInDB' => false],
			"History 28" => ['iFailAt' => 30, 'bIsInDB' => false],
			"History 29" => ['iFailAt' => 31, 'bIsInDB' => false],
			"History 30" => ['iFailAt' => 32, 'bIsInDB' => false],
			"History 31" => ['iFailAt' => 33, 'bIsInDB' => false],
			"History 32" => ['iFailAt' => 34, 'bIsInDB' => false],
			"History 33" => ['iFailAt' => 35, 'bIsInDB' => false],
			"History 34" => ['iFailAt' => 36, 'bIsInDB' => false],
			"History 35" => ['iFailAt' => 37, 'bIsInDB' => false],
			"History 36" => ['iFailAt' => 38, 'bIsInDB' => false],
			"History 37" => ['iFailAt' => 39, 'bIsInDB' => false],
		];
	}

	/**
 	 * Test DBUpdate database transaction by provoking deadlock exceptions
	 *
	 * @dataProvider DBUpdateProvider
	 * @param $iFailAt
	 *
	 * @throws \ArchivedObjectException
	 * @throws \CoreCannotSaveObjectException
	 * @throws \CoreException
	 * @throws \CoreUnexpectedValue
	 * @throws \DeleteException
	 * @throws \MySQLException
	 * @throws \MySQLHasGoneAwayException
	 * @throws \OQLException
	 */
	public function testDBUpdate($iFailAt, $bIsModified)
	{
		// Create a UserRequest into the database with 2 contacts
		$oTicket = MetaModel::NewObject('UserRequest', [
			'ref' => 'Test Ticket',
			'title' => 'Create OK',
			'description' => 'Create OK',
			'solution' => 'Create OK',
			'caller_id' => 15,
			'org_id' => 3,
		]);
		$oLinkSet = $oTicket->Get('contacts_list');
		$oLinkSet->AddItem(MetaModel::NewObject('lnkContactToTicket', ['contact_id' => 6]));
		$oLinkSet->AddItem(MetaModel::NewObject('lnkContactToTicket', ['contact_id' => 7]));
		$oTicket->Set('contacts_list', $oLinkSet);

		$this->oMySQLiMock->SetShowRequest(false);
		$oTicket->DBWrite();

		// Verify that the object is considered as saved in the database
		$this->assertEquals(true, !$oTicket->IsNew());

		// Reload from db
		$oTicket = MetaModel::GetObject('UserRequest', $oTicket->GetKey());
		$oTicket->Set('description', 'Update OK');
		$oTicket->Set('solution', 'Test OK');
		$oLinkSet = $oTicket->Get('contacts_list');
		$oLinkSet->AddItem(MetaModel::NewObject('lnkContactToTicket', ['contact_id' => 8]));
		$oTicket->Set('contacts_list', $oLinkSet);

		// Provoke an error during the update
		$this->oMySQLiMock->SetFailAt($iFailAt);
		$this->debug("---> DBUpdate()");
		try {
			$oTicket->DBWrite();
		}
		catch (Exception $e) {
			// If an exception occurs must be a deadlock
			$this->assertTrue(CMDBSource::IsDeadlockException($e));
		}

		// Verify if the ticket is considered as saved in the database
		$this->assertEquals($bIsModified, $oTicket->IsModified());

		// Reload from db after the update to check the value present in the database
		$oTicket = MetaModel::GetObject('UserRequest', $oTicket->GetKey());
		if ($bIsModified) {
			$this->assertEquals('<p>Create OK</p>', $oTicket->Get('solution'));
		} else {
			$this->assertEquals('<p>Test OK</p>', $oTicket->Get('solution'));
		}

		if (!$oTicket->IsNew()) {
			$this->oMySQLiMock->SetShowRequest(false);
			// Delete created objects
			$oTicket->DBDelete();
		}
	}

	public function DBUpdateProvider()
	{
		return [
			"Normal case"        => ['iFailAt' => -1, 'bIsModified' => false],
			"ticket_request"     => ['iFailAt' => 1, 'bIsModified' => true],
			"lnkcontacttoticket" => ['iFailAt' => 2, 'bIsModified' => true],
			"History 1"          => ['iFailAt' => 3, 'bIsModified' => true],
			"History 2"          => ['iFailAt' => 4, 'bIsModified' => true],
			"History 3"          => ['iFailAt' => 5, 'bIsModified' => true],
			"History 4"          => ['iFailAt' => 6, 'bIsModified' => true],
			"History 5"          => ['iFailAt' => 7, 'bIsModified' => true],
			"History 6"          => ['iFailAt' => 8, 'bIsModified' => true],
			"History 7"          => ['iFailAt' => 9, 'bIsModified' => true],
			"History 8"          => ['iFailAt' => 10, 'bIsModified' => true],
			"History 9"          => ['iFailAt' => 11, 'bIsModified' => true],
			"History 10"         => ['iFailAt' => 12, 'bIsModified' => true],
			"History 11"         => ['iFailAt' => 13, 'bIsModified' => true],
			"History 12"         => ['iFailAt' => 14, 'bIsModified' => true],
			"History 13"         => ['iFailAt' => 15, 'bIsModified' => true],
		];
	}

	/**
	 * @return void
	 */
	public function testIsInsideTransaction()
	{
		static::assertFalse(CMDBSource::IsInsideTransaction(), 'Should not be already inside a transaction');

		// First, with a transaction ended by a "COMMIT" statement
		CMDBSource::Query('START TRANSACTION;');
		static::assertTrue(CMDBSource::IsInsideTransaction(), 'Should be inside a translation');
		CMDBSource::Query('COMMIT;');
		static::assertFalse(CMDBSource::IsInsideTransaction(), 'Should not be inside a transaction anymore');

		// Second, with a transaction ended by a "ROLLBACK" statement
		CMDBSource::Query('START TRANSACTION;');
		static::assertTrue(CMDBSource::IsInsideTransaction(), 'Should be inside a translation (again)');
		CMDBSource::Query('ROLLBACK;');
		static::assertFalse(CMDBSource::IsInsideTransaction(), 'Should not be inside a transaction anymore');
	}

	protected function tearDown(): void
	{
		try {
			DbConnectionWrapper::SetDbConnectionMockForQuery();
			parent::tearDown();
		}
		catch (MySQLTransactionNotClosedException $e) {
			if ($this->getName() === 'testTransactionOpenedNotClosed') {
				$this->debug('Executing the testTransactionOpenNoClose method throws a '.MySQLTransactionNotClosedException::class.' exception in tearDown');
			}
		}
	}
}