<?php

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

use Combodo\iTop\Test\UnitTest\ItopDataTestCase;

class DBUnionSearchTest extends ItopDataTestCase
{
	/** @var array<string, int>|null */
	private ?array $aUnionSearchFixtureIds = null;

	/**
	 * @dataProvider UnionSearchProvider
	 *
	 * @param $sOQL
	 *
	 * @return void
	 * @throws \CoreException
	 * @throws \OQLException
	 */
	public function testUnionSearch($sOQL)
	{
		$sOQL = $this->ResolveUnionSearchFixtureOql($sOQL);
		$oSearch = DBSearch::FromOQL($sOQL);

		$oSet = new DBObjectSet($oSearch);
		if ($oSet->Count() > 0) {
			$aSelectedAliases = array_keys($oSearch->GetSelectedClasses());
			$aFirstRow = $oSet->FetchAssoc();
			$aAliases = array_keys($aFirstRow);
			$this->assertEquals($aSelectedAliases, $aAliases);
		}

		$aSelectedClasses = $oSearch->GetSelectedClasses();
		foreach ($aSelectedClasses as $sSelectedAlias => $sSelectedClass) {
			$oSearchTest = $oSearch->DeepClone();
			$oSearchTest->SetSelectedClasses([$sSelectedAlias]);
			$oSet = new DBObjectSet($oSearchTest);
			if ($oSet->Count() > 0) {
				$aSelectedAliases = array_keys($oSearchTest->GetSelectedClasses());
				$aFirstRow = $oSet->FetchAssoc();
				$aAliases = array_keys($aFirstRow);
				$this->assertEquals($aSelectedAliases, $aAliases);
			}
		}
	}

	public function UnionSearchProvider()
	{
		return [
			'Same class' => ["SELECT Server UNION SELECT Server"],
			'different class same alias' => ['SELECT Server AS fci UNION SELECT VirtualMachine AS fci'],
			'different class no alias' => ['SELECT Server UNION SELECT VirtualMachine'],
			'multiple classes same alias' => [
				'SELECT `L`, `P` FROM Location AS `L` JOIN Person AS `P` ON `P`.location_id = `L`.id WHERE (`L`.`org_id` = {{ORG1_ID}}) UNION SELECT `L`, `P` FROM Location AS `L` JOIN Person AS `P` ON `P`.location_id = `L`.id WHERE (`L`.`org_id` = {{ORG2_ID}})',
			],
			'multiple classes' => [
				'SELECT `L`, `P` FROM Location AS `L` JOIN Person AS `P` ON `P`.location_id = `L`.id WHERE (`L`.`org_id` = {{ORG1_ID}}) UNION SELECT `L1`, `P1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE (`P1`.`org_id` = {{ORG2_ID}})',
			],
		];
	}

	private function ResolveUnionSearchFixtureOql(string $sOQL): string
	{
		if (strpos($sOQL, '{{ORG') === false) {
			return $sOQL;
		}

		$aFixtures = $this->GetUnionSearchFixtureIds();

		return strtr($sOQL, [
			'{{ORG1_ID}}' => (string) $aFixtures['org1_id'],
			'{{ORG2_ID}}' => (string) $aFixtures['org2_id'],
		]);
	}

	/**
	 * Create the minimum fixture set for UNION tests once per test instance.
	 */
	private function GetUnionSearchFixtureIds(): array
	{
		if ($this->aUnionSearchFixtureIds !== null) {
			return $this->aUnionSearchFixtureIds;
		}

		$sSuffix = uniqid('dbunion_', false);
		$iOrg1 = (int) $this->GivenObjectInDB('Organization', ['name' => 'Union Org 1 '.$sSuffix]);
		$iOrg2 = (int) $this->GivenObjectInDB('Organization', ['name' => 'Union Org 2 '.$sSuffix]);

		$iLocation1 = (int) $this->GivenObjectInDB('Location', [
			'name' => 'Union Location 1 '.$sSuffix,
			'org_id' => $iOrg1,
		]);
		$iLocation2 = (int) $this->GivenObjectInDB('Location', [
			'name' => 'Union Location 2 '.$sSuffix,
			'org_id' => $iOrg2,
		]);

		$this->GivenObjectInDB('Person', [
			'name' => 'Union Person 1 '.$sSuffix,
			'first_name' => 'Test',
			'org_id' => $iOrg1,
			'location_id' => $iLocation1,
		]);
		$this->GivenObjectInDB('Person', [
			'name' => 'Union Person 2 '.$sSuffix,
			'first_name' => 'Test',
			'org_id' => $iOrg2,
			'location_id' => $iLocation2,
		]);

		$this->aUnionSearchFixtureIds = [
			'org1_id' => $iOrg1,
			'org2_id' => $iOrg2,
		];

		return $this->aUnionSearchFixtureIds;
	}

	public function testFilterOnFirstSelectedClass()
	{
		$sSourceOQL = 'SELECT `Person`, `Location` FROM Person AS `Person` JOIN Location AS `Location` ON `Person`.location_id = `Location`.id WHERE (`Location`.`id` = 1)';
		$oSearch = DBSearch::FromOQL($sSourceOQL);

		$sFilterOQL = 'SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`id` = 2) UNION SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`id` = 3)';
		$oVisibleObjects = DBSearch::FromOQL($sFilterOQL);
		$sClassAlias = 'Person';
		$oVisibleObjects->AllowAllData();
		$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);
		$this->InvokeNonPublicMethod(get_class($oSearch), 'SetDataFiltered', $oSearch, []);

		$this->debug($oSearch->ToOQL());

		$oSet = new DBObjectSet($oSearch);
		$oSet->Count();

		$this->assertTrue(true);
	}

	/**
	 * Ignored test (provokes PHP Error)
	 *
	 * @dataProvider FilterOnSecondSelectedClassProvider
	 * @return void
	 * @throws \CoreException
	 * @throws \MissingQueryArgument
	 * @throws \MySQLException
	 * @throws \MySQLHasGoneAwayException
	 * @throws \OQLException
	 */
	public function testFilterOnSecondSelectedClass($sSourceOQL, $sClassAlias, $sFilterOQL, $sExpected)
	{
		$oSearch = DBSearch::FromOQL($sSourceOQL);

		$oVisibleObjects = DBSearch::FromOQL($sFilterOQL);
		$oVisibleObjects->AllowAllData();
		$oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects);

		$sResult = $oSearch->ToOQL();
		$this->debug($sResult);

		$this->assertEquals($sExpected, $sResult);

		$oSet = new DBObjectSet($oSearch);
		$oSet->CountWithLimit(1);

		$this->assertTrue(true);
	}

	public function FilterOnSecondSelectedClassProvider()
	{
		return [
			[
				'sSourceOQL'  => "SELECT P1, L1 FROM Person AS P1 JOIN Location AS L1 ON P1.location_id = L1.id WHERE L1.id = 1",
				'sClassAlias' => "L1",
				'sFilterOQL'  => "SELECT Location AS L2 WHERE L2.id = 2 UNION SELECT Location AS L3 WHERE L3.id = 3",
				'sExpected'   => "SELECT `P1`, `L1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 2)) UNION SELECT `P1`, `L1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 3))",
			],
			[
				'sSourceOQL'  => 'SELECT L1, P1 FROM Person AS P1 JOIN Location AS L1 ON P1.location_id = L1.id WHERE L1.id = 1',
				'sClassAlias' => 'L1',
				'sFilterOQL'  => 'SELECT Location AS L2 WHERE L2.id = 2 UNION SELECT Location AS L3 WHERE L3.id = 3',
				'sExpected'   => 'SELECT `L1`, `P1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 2)) UNION SELECT `L1`, `P1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 3))',
			],
			[
				'sSourceOQL'  => "SELECT L1,O1 FROM Location AS L1 JOIN Organization AS O1 ON L1.org_id=O1.id JOIN Organization AS O2 ON O1.parent_id = O2.id WHERE L1.name != 'l1-name' AND O1.name != 'o1-name' AND ISNULL(O2.name) != 0",
				'sClassAlias' => "O1",
				'sFilterOQL'  => "SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3",
				'sExpected'   => "SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O1`.`id` = 2)) UNION SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O1`.`id` = 3))",
			],
			[
				'sSourceOQL'  => "SELECT L1,O2 FROM Location AS L1 JOIN Organization AS O1 ON L1.org_id=O1.id JOIN Organization AS O2 ON O1.parent_id = O2.id WHERE L1.name != 'l1-name' AND O1.name != 'o1-name' AND ISNULL(O2.name) != 0",
				'sClassAlias' => 'O2',
				'sFilterOQL'  => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3',
				'sExpected'   => "SELECT `L1`, `O2` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 2)) UNION SELECT `L1`, `O2` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 3))",
			],
			[
				'sSourceOQL'  => "SELECT L1,O1 FROM Location AS L1 JOIN Organization AS O1 ON L1.org_id=O1.id JOIN Organization AS O2 ON O1.parent_id = O2.id WHERE L1.name != 'l1-name' AND O1.name != 'o1-name' AND ISNULL(O2.name) != 0",
				'sClassAlias' => 'O2',
				'sFilterOQL'  => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3',
				// This is another problem, we should not be able to filter on not selected classes
				'sExpected'   => "SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 2)) UNION SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 3))",
			],
			[
				'sSourceOQL'  => "SELECT P1, O1 FROM Person AS P1 JOIN Organization AS O1 ON P1.org_id = O1.id JOIN Location AS L1 ON P1.location_id = L1.id JOIN Organization AS O2 ON L1.org_id = O2.id WHERE L1.name != '' AND P1.name != '' AND O1.name != '' AND O2.name != ''",
				'sClassAlias' => 'O1',
				'sFilterOQL'  => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3',
				'sExpected'   => "SELECT `P1`, `O1` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O1`.`id` = 2)) UNION SELECT `P1`, `O1` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O1`.`id` = 3))",
			],
			[
				'sSourceOQL'  => "SELECT P1, O2 FROM Person AS P1 JOIN Organization AS O1 ON P1.org_id = O1.id JOIN Location AS L1 ON P1.location_id = L1.id JOIN Organization AS O2 ON L1.org_id = O2.id WHERE L1.name != '' AND P1.name != '' AND O1.name != '' AND O2.name != ''",
				'sClassAlias' => 'O2',
				'sFilterOQL'  => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3',
				'sExpected'   => "SELECT `P1`, `O2` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O2`.`id` = 2)) UNION SELECT `P1`, `O2` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O2`.`id` = 3))",
			],
		];
	}

	/**
	 * @dataProvider SetSelectedClassesProvider
	 * @param $sOQL
	 * @param $sSelectedClass
	 *
	 * @return void
	 * @throws \CoreException
	 * @throws \OQLException
	 */
	public function testSetSelectedClasses(string $sOQL, array $aSelectedClasses, string $sExpectedOQL)
	{
		$oSearch = DBSearch::FromOQL($sOQL);
		$this->debug($oSearch->ToOQL());
		$this->debug("Set selected classes to [ ".implode(" ,", $aSelectedClasses)." ]");
		$oSearch->SetSelectedClasses($aSelectedClasses);
		$this->debug($oSearch->ToOQL());
		$this->assertEquals($sExpectedOQL, $oSearch->ToOQL());
	}

	public function SetSelectedClassesProvider()
	{
		return [
			'N°6151' => [
				'OQL'             => "SELECT P FROM Person AS P JOIN User AS U ON U.contactid = P.id UNION SELECT P FROM Person AS P JOIN User AS U ON U.contactid = P.id",
				'SelectedClasses' => ['U'],
				'Expected OQL'    => "SELECT `U` FROM Person AS `P` JOIN User AS `U` ON `U`.contactid = `P`.id WHERE 1 UNION SELECT `U` FROM Person AS `P` JOIN User AS `U` ON `U`.contactid = `P`.id WHERE 1",
			],
		];
	}

	public function testAllowAllDataOnUnions()
	{
		$oSearch = \DBObjectSearch::FromOQL('SELECT Server UNION SELECT VirtualMachine');
		$oSearch->AllowAllData(false);
		self::assertFalse($oSearch->IsAllDataAllowed(), 'DBUnionSearch AllowData value');
		$oSearch->AllowAllData(true);
		self::assertTrue($oSearch->IsAllDataAllowed(), 'DBUnionSearch AllowData value');
	}

}
