<?php

namespace Combodo\iTop\Test\UnitTest\Webservices;

use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use MetaModel;
use utils;

/**
 * @group itopRequestMgmt
 * @group restApi
 * @group defaultProfiles
 */
class RestTest extends ItopDataTestCase
{
	public const USE_TRANSACTION = false;
	public const CREATE_TEST_ORG = false;

	private static $sLogin;
	private static $sPassword = "Iuytrez9876543ç_è-(";

	/**
	 * This method is called before the first test of this test class is run (in the current process).
	 */
	public static function setUpBeforeClass(): void
	{
		parent::setUpBeforeClass();
	}

	/**
	 * This method is called after the last test of this test class is run (in the current process).
	 */
	public static function tearDownAfterClass(): void
	{
		parent::tearDownAfterClass();
	}

	/**
	 * @throws Exception
	 */
	protected function setUp(): void
	{
		parent::setUp();

		static::$sLogin = "rest-user-".date('dmYHis');

		$this->CreateTestOrganization();

		$oRestProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'REST Services User'], true);
		$oAdminProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'Administrator'], true);

		if (is_object($oRestProfile) && is_object($oAdminProfile)) {
			$oUser = $this->CreateUser(static::$sLogin, $oRestProfile->GetKey(), static::$sPassword);
			$this->AddProfileToUser($oUser, $oAdminProfile->GetKey());
		}
	}

	public function testListOperationsAndJSONPCallback()
	{
		$sCallbackName = 'fooCallback';
		$sJsonData = '{"operation": "list_operations"}';

		// Test regular JSON result
		$sJSONResult = $this->CallRestApi_HTTP($sJsonData);
		// - Try to decode JSON to array to check if it is well-formed
		$aJSONResultAsArray = json_decode($sJSONResult, true);
		$this->assertArrayHasKey('version', $aJSONResultAsArray);
		$this->assertArrayHasKey('operations', $aJSONResultAsArray);
		$this->assertArrayHasKey('code', $aJSONResultAsArray);
		$this->assertArrayHasKey('message', $aJSONResultAsArray);
		$this->assertEquals(0, $aJSONResultAsArray['code']);
		$this->assertTrue(count($aJSONResultAsArray['operations']) >= 7, 'Expecting at least 7 operations from Core Services');
		foreach ($aJSONResultAsArray['operations'] as $aOperationData) {
			$this->assertCount(3, $aOperationData);
			$this->assertArrayHasKey('verb', $aOperationData);
			$this->assertArrayHasKey('description', $aOperationData);
			$this->assertArrayHasKey('extension', $aOperationData);
			$this->assertNotEmpty($aOperationData['verb']);
		}

		// Test JSONP with callback by checking that it is the same as the regular JSON but within the JS callback
		$sJSONPResult =  $this->CallRestApi_HTTP($sJsonData, $sCallbackName);
		$this->assertEquals($sCallbackName.'('.$sJSONResult.');', $sJSONPResult, 'JSONP response callback does not match expected result');
	}

	public function testMissingJSONData()
	{
		$sOutputJson = $this->CallRestApi_HTTP();
		$aJson = json_decode($sOutputJson, true);
		$this->assertEquals(3, $aJson['code'], $sOutputJson);
		$this->assertEquals("Error: Missing parameter 'json_data'", $aJson['message'], $sOutputJson);
	}

	public function testPostJSONDataAsCurlFile()
	{
		$sJsonData = '{"operation": "list_operations"}';

		// Test regular JSON result
		$sJSONResult = $this->CallRestApi_HTTP($sJsonData, null, true);
		$aJSONResultAsArray = json_decode($sJSONResult, true);
		$this->assertEquals(0, $aJSONResultAsArray['code'], $sJSONResult);
	}

	public function testCoreApiGet()
	{
		// Create ticket
		$description = date('dmY H:i:s');
		$oTicket = $this->CreateSampleTicket($description);
		$iId = $oTicket->GetKey();

		$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{
   "operation": "core/get",
   "class": "UserRequest",
   "key": "SELECT UserRequest WHERE id=$iId",
   "output_fields": "id, description"
}
JSON);

		$sExpectedJsonOuput = <<<JSON
{
    "code": 0,
    "message": "Found: 1",
    "objects": {
        "UserRequest::$iId": {
            "class": "UserRequest",
            "code": 0,
            "fields": {
                "description": "<p>$description</p>",
                "id": "$iId"
            },
            "key": "$iId",
            "message": ""
        }
    }
}
JSON;
		$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
	}

	public function testCoreApiGet_Select2SubClasses()
	{
		// Create ticket
		$description = date('dmY H:i:s');
		$iIdCaller = $this->CreatePerson(1)->GetKey();
		$oUserRequest = $this->CreateSampleTicket($description, 'UserRequest', $iIdCaller);
		$oChange = $this->CreateSampleTicket($description, 'Change', $iIdCaller);
		$iIdUserRequest = $oUserRequest->GetKey();
		$iIdChange = $oChange->GetKey();

		$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{
   "operation": "core/get",
   "class": "UserRequest, Change",
   "key": "SELECT UserRequest WHERE id=$iIdUserRequest UNION SELECT Change WHERE id=$iIdChange",
   "output_fields": "id, description, outage"
}
JSON);

		$sExpectedJsonOuput = <<<JSON
{
    "code": 0,
    "message": "Found: 2",
    "objects": {
        "UserRequest::$iIdUserRequest": {
            "class": "UserRequest",
            "code": 0,
            "fields": {
                "description": "<p>$description</p>",
                "id": "$iIdUserRequest"
            },
            "key": "$iIdUserRequest",
            "message": ""
        },       
         "Change::$iIdChange": {
            "class": "Change",
            "code": 0,
            "fields": {
                "description": "<p>$description</p>",
                "id": "$iIdChange",
                "outage": "no"
            },
            "key": "$iIdChange",
            "message": ""
        }
    }
}
JSON;
		$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
	}

	public function testCoreApiGet_SelectTicketAndPerson()
	{
		// Create ticket
		$description = date('dmY H:i:s');
		$iIdCaller = $this->CreatePerson(1)->GetKey();
		$oUserRequest = $this->CreateSampleTicket($description, 'UserRequest', $iIdCaller);
		$iIdUserRequest = $oUserRequest->GetKey();

		$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{
   "operation": "core/get",
   "class": "UserRequest, Change",
   "key": "SELECT UR, P FROM UserRequest AS UR JOIN Person AS P ON UR.caller_id = P.id WHERE UR.id=$iIdUserRequest ",
   "output_fields": "id, title, description, name, email"
}
JSON);

		$sExpectedJsonOuput = <<<JSON
{
    "code": 0,
    "message": "Found: 1",
    "objects": [{
	        "UR": {
	            "class": "UserRequest",
	            "code": 0,
	            "fields": {
	                "description": "<p>$description</p>",
	                "id": "$iIdUserRequest",                
	                "title": "Houston, got a problem"
	            },
	            "key": "$iIdUserRequest",
	            "message": ""
	        },       
            "P": {
                "class": "Person",
                "code": 0,
                "fields": {
                    "email": "",
                    "id": "$iIdCaller",
                    "name": "Person_1"
                },
            "key": "$iIdCaller",
            "message": ""
        }
    }]
}
JSON;
		$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
	}

	public function testCoreApiGetWithUnionAndDifferentOutputFields()
	{
		// Create ticket
		$description = date('dmY H:i:s');
		$oUserRequest = $this->CreateSampleTicket($description);
		$oChange = $this->CreateSampleTicket($description, 'Change');
		$iUserRequestId = $oUserRequest->GetKey();
		$sUserRequestRef = $oUserRequest->Get('ref');
		$iChangeId = $oChange->GetKey();
		$sChangeRef = $oChange->Get('ref');

		$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{
   "operation": "core/get",
   "class": "Ticket",
   "key": "SELECT UserRequest WHERE id=$iUserRequestId UNION SELECT Change WHERE id=$iChangeId",
   "output_fields": "Ticket:ref;UserRequest:ref,status,origin;Change:ref,status,outage"
}
JSON);

		$sExpectedJsonOuput = <<<JSON
{
    "code": 0,
    "message": "Found: 2",
    "objects": {
    	"Change::$iChangeId": {
            "class": "Change",
            "code": 0,
            "fields": {
            	"outage": "no",
                "ref": "$sChangeRef",
                "status": "new"
            },
            "key": "$iChangeId",
            "message": ""
        },
        "UserRequest::$iUserRequestId": {
            "class": "UserRequest",
            "code": 0,
            "fields": {
            	"origin": "phone",
                "ref": "$sUserRequestRef",
                "status": "new"
            },
            "key": "$iUserRequestId",
            "message": ""
        }
    }
}
JSON;
		$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
	}
	public function testCoreApiCreate()
	{
		// Create ticket
		$description = date('dmY H:i:s');
		$sOutputJson = $this->CallCoreRestApi_Internally(<<<JSON
{
   "operation": "core/create",
   "comment": "test",
   "class": "UserRequest",
   "output_fields": "id",
   "fields":
   {
      "org_id": "SELECT Organization WHERE name = \"Demo\"",
      
      "title": "Houston, got a problem",
      "description": "$description"
   }
}
JSON);

		$this->debug("Output: '$sOutputJson'");
		$aJson = json_decode($sOutputJson, true);
		$this->assertNotNull($aJson, "Cannot decode returned JSON : $sOutputJson");

		$this->assertStringContainsString("0", "".$aJson['code'], $sOutputJson);
		$sUserRequestKey = $this->array_key_first($aJson['objects']);
		$this->assertStringContainsString('UserRequest::', $sUserRequestKey);
		$iId = $aJson['objects'][$sUserRequestKey]['key'];

		$sExpectedJsonOuput = <<<JSON
{"objects":{"UserRequest::$iId":{"code":0,"message":"created","class":"UserRequest","key":"$iId","fields":{"id":"$iId"}}},"code":0,"message":null}
JSON;
		$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sOutputJson);

		$oObject = MetaModel::GetObject('UserRequest', $iId, false, true);
		$this->assertIsObject($oObject, "Object UserRequest::$iId not present in the database");
		$this->assertSame("<p>$description</p>", $oObject->Get('description'));

		$this->assertDBChangeOpCount('UserRequest', $iId, 1);

		// Delete ticket
		$oObject->DBDelete();
	}

	/**
	 * array_key_first comes with PHP7.3
	 * itop should also work with previous PHP versions
	 */
	private function array_key_first($aTab)
	{
		if (!is_array($aTab) || empty($aTab)) {
			return false;
		}

		foreach ($aTab as $sKey => $sVal) {
			return $sKey;
		}
	}

	public function testCoreApiUpdate()
	{
		//create ticket
		$description = date('dmY H:i:s');
		$oTicket = $this->CreateSampleTicket($description);
		$iId = $oTicket->GetKey();

		// Update ticket
		$description = 'Update to '.date('Ymd H:i:s');
		$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{"operation": "core/update","comment": "test","class": "UserRequest","key":"$iId","output_fields": "description","fields":{"description": "$description"}}
JSON);

		$sExpectedJsonOuput = <<<JSON
{"objects":{"UserRequest::$iId":{"code":0,"message":"updated","class":"UserRequest","key":"$iId","fields":{"description":"<p>$description<\/p>"}}},"code":0,"message":null}
JSON;
		$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);

		$this->assertDBChangeOpCount('UserRequest', $iId, 2);
	}

	public function testCoreApiDelete()
	{
		// Create ticket
		$description = date('dmY H:i:s');
		$oTicket = $this->CreateSampleTicket($description);
		$iId = $oTicket->GetKey();

		// Delete ticket
		$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{
   "operation": "core/delete",
   "comment": "Cleanup",
   "class": "UserRequest",
   "key":$iId,
   "simulate": false
}
JSON);
		$sExpectedJsonOuput = <<<JSON
"objects":{"UserRequest::$iId"
JSON;
		$this->assertStringContainsString($sExpectedJsonOuput, $sJSONOutput);

		$oObject = MetaModel::GetObject('UserRequest', $iId, false, true);
		$this->assertSame(null, $oObject, "Object UserRequest::$iId still present in the database");
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Helpers
	//
	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	private function CreateSampleTicket($description, $sType = 'UserRequest', $iIdCaller = null)
	{
		$oTicket = $this->createObject($sType, [
			'org_id' => $this->getTestOrgId(),
			"title" => "Houston, got a problem",
			"description" => $description,
			"caller_id" => $iIdCaller,
		]);
		return $oTicket;
	}

	/**
	 * @param string|null $sJsonDataContent If null, then no data is posted and the service should reply with an error
	 * @param string|null $sCallbackName JSONP callback
	 * @param bool $bJSONDataAsFile Set to true to test with the data provided to curl as a file
	 *
	 * @return bool|string
	 */
	private function CallRestApi_HTTP(?string $sJsonDataContent = null, ?string $sCallbackName = null, bool $bJSONDataAsFile = false)
	{
		$ch = curl_init();
		$aPostFields = [
			'version' => '1.3',
			'auth_user' => static::$sLogin,
			'auth_pwd' => static::$sPassword,
		];

		$sTmpFile = null;
		if (!is_null($sJsonDataContent)) {
			if ($bJSONDataAsFile) {
				$sTmpFile = tempnam(sys_get_temp_dir(), 'jsondata_');
				file_put_contents($sTmpFile, $sJsonDataContent);

				$oCurlFile = curl_file_create($sTmpFile);
				$aPostFields['json_data'] = $oCurlFile;
			} else {
				$aPostFields['json_data'] = $sJsonDataContent;
			}
		}

		if (utils::IsNotNullOrEmptyString($sCallbackName)) {
			$aPostFields['callback'] = $sCallbackName;
		}

		$sJson = $this->CallItopUri('webservices/rest.php', $aPostFields);

		if (!is_null($sTmpFile)) {
			unlink($sTmpFile);
		}

		return $sJson;
	}

	private function CallCoreRestApi_Internally(string $sJsonDataContent)
	{
		$oJsonData = json_decode($sJsonDataContent);
		$sOperation = $oJsonData->operation;

		//\UserRights::Login(static::$sLogin);
		\CMDBObject::SetTrackOrigin('webservice-rest');
		\CMDBObject::SetTrackInfo('test');

		$oRestSP = new \CoreServices();
		$oResult = $oRestSP->ExecOperation('1.3', $sOperation, $oJsonData);

		//\UserRights::Logoff();

		return json_encode($oResult);
	}
}
