<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/


require_once __DIR__.'/../include/CAPITest.php';

/**
 * @onBefore prepareTestData
 *
 * @onAfter clearData
 */
class testProxy extends CAPITest {

	/**
	 * Non-existent ID, type, status etc.
	 */
	private const INVALID_NUMBER = 999999;

	/**
	 * @var array
	 */
	private static array $data = [
		'proxyids' => [],
		'groupids' => [],
		'hostids' => [],
		'actionids' => [],
		'druleids' => [],

		// Created proxies during proxy.create test (deleted at the end).
		'created' => []
	];

	/**
	 * Prepare data for tests. Create proxies, host groups, hosts, actions, discovery rules.
	 */
	public function prepareTestData(): void {
		$this->prepareTestDataProxies();
		$this->prepareTestDataHostGroups();
		$this->prepareTestDataHosts();
		$this->prepareTestDataActions();
		$this->prepareTestDataDiscoveryRules();
	}

	/**
	 * Create proxies.
	 */
	private function prepareTestDataProxies(): void {
		$proxies = [
			'get_active_defaults' => [
				'name' => 'API test proxy.get - active',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'get_passive_defaults' => [
				'name' => 'API test proxy.get - passive',
				'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
				'address' => '127.0.0.1',
				'port' => '10050'
			],
			'get_version_undefined' => [
				'name' => 'API test proxy.get for filter - version undefined',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'get_version_current' => [
				'name' => 'API test proxy.get for filter - version current',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'get_version_outdated' => [
				'name' => 'API test proxy.get for filter - version outdated',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'get_version_unsupported' => [
				'name' => 'API test proxy.get for filter - version unsupported',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'update_active_defaults' => [
				'name' => 'API test proxy.update - active defaults',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'update_passive_defaults' => [
				'name' => 'API test proxy.update - passive defaults',
				'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
				'address' => '127.0.0.1',
				'port' => '10050'
			],
			'update_active_psk' => [
				'name' => 'API test proxy.update - active with PSK-based connections from proxy',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
				'tls_accept' => HOST_ENCRYPTION_PSK,
				'tls_psk_identity' => 'Test PSK',
				'tls_psk' => '9b8eafedfaae00cece62e85d5f4792c7d9c9bcc851b23216a1d300311cc4f7cb'
			],
			'update_active_cert' => [
				'name' => 'API test proxy.update - active with certificate-based connections from proxy',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
				'tls_accept' => HOST_ENCRYPTION_CERTIFICATE
			],
			'update_active_any' => [
				'name' => 'API test proxy.update - active with any connections from proxy',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
				'tls_accept' => HOST_ENCRYPTION_NONE + HOST_ENCRYPTION_PSK + HOST_ENCRYPTION_CERTIFICATE,
				'tls_psk_identity' => 'Test PSK',
				'tls_psk' => '9b8eafedfaae00cece62e85d5f4792c7d9c9bcc851b23216a1d300311cc4f7cb'
			],
			'update_passive_dns' => [
				'name' => 'API test proxy.update - passive with DNS name',
				'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
				'address' => 'localhost',
				'port' => '10050'
			],
			'update_passive_ip' => [
				'name' => 'API test proxy.update - passive with IP address',
				'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
				'address' => '127.0.0.1',
				'port' => '10050'
			],
			'update_passive_psk' => [
				'name' => 'API test proxy.update - passive with PSK-based connections to proxy',
				'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
				'address' => '127.0.0.1',
				'port' => '10050',
				'tls_connect' => HOST_ENCRYPTION_PSK
			],
			'update_passive_cert' => [
				'name' => 'API test proxy.update - passive with certificate-based connections to proxy',
				'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
				'address' => '127.0.0.1',
				'port' => '10050',
				'tls_connect' => HOST_ENCRYPTION_CERTIFICATE
			],
			'update_hosts' => [
				'name' => 'API test proxy.update - hosts',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'delete_single' => [
				'name' => 'API test proxy.delete - single',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'delete_multiple_1' => [
				'name' => 'API test proxy.delete - multiple 1',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'delete_multiple_2' => [
				'name' => 'API test proxy.delete - multiple 2',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'delete_used_in_host' => [
				'name' => 'API test proxy.delete - used in hosts',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'delete_used_in_action' => [
				'name' => 'API test proxy.delete - used in actions',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'delete_used_in_discovery' => [
				'name' => 'API test proxy.delete - used in discovery rules',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			],
			'select_hosts_extend' => [
				'name' => 'API test proxy - verify fields returned with selectHosts extend',
				'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
			]
		];
		$db_proxies = CDataHelper::call('proxy.create', array_values($proxies));
		$this->assertArrayHasKey('proxyids', $db_proxies, __FUNCTION__.'() failed: Could not create proxies.');

		self::$data['proxyids'] = array_combine(array_keys($proxies), $db_proxies['proxyids']);

		// Manually update "proxy_rtdata" table.
		$proxy_rtdata = [
			'get_version_current' => [
				'lastaccess' => 1662034530,
				'version' => 60400,
				'compatibility' => ZBX_PROXY_VERSION_CURRENT
			],
			'get_version_outdated' => [
				'lastaccess' => 1662034225,
				'version' => 60200,
				'compatibility' => ZBX_PROXY_VERSION_OUTDATED
			],
			'get_version_unsupported' => [
				'lastaccess' => 1651407015,
				'version' => 50401,
				'compatibility' => ZBX_PROXY_VERSION_UNSUPPORTED
			]
		];

		$upd_proxy_rtdata = [];

		foreach ($proxy_rtdata as $id_placeholder => $rtdata) {
			$upd_proxy_rtdata[] = [
				'values' => [
					'lastaccess' => $rtdata['lastaccess'],
					'version' => $rtdata['version'],
					'compatibility' => $rtdata['compatibility']
				],
				'where' => ['proxyid' => self::$data['proxyids'][$id_placeholder]]
			];
		}

		DB::update('proxy_rtdata', $upd_proxy_rtdata);
	}

	/**
	 * Create host groups.
	 */
	private function prepareTestDataHostGroups(): void {
		$db_hostgroups = CDataHelper::call('hostgroup.create', [
			'name' => 'API test host group'
		]);
		$this->assertArrayHasKey('groupids', $db_hostgroups, __FUNCTION__.'() failed: Could not create host groups.');

		self::$data['groupids'] = $db_hostgroups['groupids'];
	}

	/**
	 * Create hosts.
	 */
	private function prepareTestDataHosts(): void {
		$hosts = [
			'with_proxy' => [
				'host' => 'api_test_host_with_proxy',
				'name' => 'API test host - with proxy',
				'proxyid' => self::$data['proxyids']['delete_used_in_host'],
				'groups' => [
					[
						'groupid' => self::$data['groupids'][0]
					]
				]
			],
			'without_proxy_1' => [
				'host' => 'api_test_host_without_proxy_1',
				'name' => 'API test host - without proxy 1',
				'groups' => [
					[
						'groupid' => self::$data['groupids'][0]
					]
				]
			],
			'without_proxy_2' => [
				'host' => 'api_test_host_without_proxy_2',
				'name' => 'API test host - without proxy 2',
				'groups' => [
					[
						'groupid' => self::$data['groupids'][0]
					]
				]
			],
			'select_fields_host' => [
				'host' => 'host_fields_host',
				'name' => 'API test host - for selectHosts with extend',
				'proxyid' => self::$data['proxyids']['select_hosts_extend'],
				'groups' => [
					[
						'groupid' => self::$data['groupids'][0]
					]
				]
			]
		];
		$db_hosts = CDataHelper::call('host.create', array_values($hosts));
		$this->assertArrayHasKey('hostids', $db_hosts, __FUNCTION__.'() failed: Could not create hosts.');

		self::$data['hostids'] = array_combine(array_keys($hosts), $db_hosts['hostids']);
	}

	/**
	 * Create actions.
	 */
	private function prepareTestDataActions(): void {
		$actions = [
			'name' => 'API test discovery action',
			'eventsource' => EVENT_SOURCE_DISCOVERY,
			'filter' => [
				'evaltype' => CONDITION_EVAL_TYPE_AND_OR,
				'conditions' => [
					[
						'conditiontype' => CONDITION_TYPE_PROXY,
						'operator' => CONDITION_OPERATOR_EQUAL,
						'value' => self::$data['proxyids']['delete_used_in_action']
					]
				]
			],
			'operations' => [
				[
					'operationtype' => OPERATION_TYPE_MESSAGE,
					'opmessage_grp' => [
						[
							'usrgrpid' => 7
						]
					],
					'opmessage' => [
						'mediatypeid' => 0,
						'default_msg' => 1
					]
				]
			]
		];
		$db_actions = CDataHelper::call('action.create', $actions);
		$this->assertArrayHasKey('actionids', $db_actions, __FUNCTION__.'() failed: Could not create actions.');

		self::$data['actionids'] = $db_actions['actionids'];
	}

	/**
	 * Create discovery rules.
	 */
	private function prepareTestDataDiscoveryRules(): void {
		$drules = [
			'name' => 'API test discovery rule',
			'iprange' => '192.168.1.1-255',
			'proxyid' => self::$data['proxyids']['delete_used_in_discovery'],
			'dchecks' => [
				[
					'type' => SVC_AGENT,
					'key_' => 'system.uname',
					'ports' => 10050,
					'uniq' => 0
				]
			]
		];
		$db_drules = CDataHelper::call('drule.create', $drules);
		$this->assertArrayHasKey('druleids', $db_drules, __FUNCTION__.'() failed: Could not create discovery rules.');

		self::$data['druleids'] = $db_drules['druleids'];
	}

	/**
	 * Data provider for proxy.create. Array contains invalid proxies.
	 *
	 * @return array
	 */
	public static function getProxyCreateDataInvalid(): array {
		return [
			'Test proxy.create: empty request' => [
				'proxy' => [],
				'expected_error' => 'Invalid parameter "/": cannot be empty.'
			],
			'Test proxy.create: unexpected parameter' => [
				'proxy' => [
					'abc' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1": unexpected parameter "abc".'
			],

			// Check "name".
			'Test proxy.create: missing "host"' => [
				'proxy' => [
					'description' => ''
				],
				'expected_error' => 'Invalid parameter "/1": the parameter "name" is missing.'
			],
			'Test proxy.create: invalid "host" (empty string)' => [
				'proxy' => [
					'name' => ''
				],
				'expected_error' => 'Invalid parameter "/1/name": cannot be empty.'
			],
			'Test proxy.create: invalid "host" (UTF-8 string)' => [
				'proxy' => [
					'name' => 'АПИ прокси УТФ-8'
				],
				'expected_error' => 'Invalid parameter "/1/name": invalid host name.'
			],
			'Test proxy.create: invalid "host" (does not match naming pattern)' => [
				'proxy' => [
					'name' => 'API create proxy?'
				],
				'expected_error' => 'Invalid parameter "/1/name": invalid host name.'
			],
			'Test proxy.create: invalid "host" (too long)' => [
				'proxy' => [
					'name' => str_repeat('h', DB::getFieldLength('proxy', 'name') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/name": value is too long.'
			],
			'Test proxy.create: multiple proxies with the same "host"' => [
				'proxy' => [
					[
						'name' => 'API create proxy',
						'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
					],
					[
						'name' => 'API create proxy',
						'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
						'address' => '127.0.0.1',
						'port' => '12345'
					]
				],
				'expected_error' => 'Invalid parameter "/2": value (name)=(API create proxy) already exists.'
			],
			'Test proxy.create: invalid "host" (duplicate)' => [
				'proxy' => [
					'name' => 'API test proxy.get - active',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
				],
				'expected_error' => 'Proxy "API test proxy.get - active" already exists.'
			],

			// Check "operating_mode".
			'Test proxy.create: missing "operating_mode"' => [
				'proxy' => [
					'name' => 'API create proxy'
				],
				'expected_error' => 'Invalid parameter "/1": the parameter "operating_mode" is missing.'
			],
			'Test proxy.create: invalid "operating_mode" (string)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/operating_mode": an integer is expected.'
			],
			'Test proxy.create: invalid "operating_mode" (not in range)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/operating_mode": value must be one of '.
					implode(', ', [PROXY_OPERATING_MODE_ACTIVE, PROXY_OPERATING_MODE_PASSIVE]).'.'
			],

			// Check "description".
			'Test proxy.create: invalid "description" (bool)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'description' => false
				],
				'expected_error' => 'Invalid parameter "/1/description": a character string is expected.'
			],
			'Test proxy.create: invalid "description" (too long)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'description' => str_repeat('d', DB::getFieldLength('proxy', 'description') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/description": value is too long.'
			],

			// Check "allowed_addresses".
			'Test proxy.create: invalid "allowed_addresses" (bool)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'allowed_addresses' => false
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": a character string is expected.'
			],
			'Test proxy.create: invalid "allowed_addresses" (IP address range)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'allowed_addresses' => '192.168.0-255.0/30'
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": invalid address range "192.168.0-255.0/30".'
			],
			'Test proxy.create: invalid "allowed_addresses" (IPv6 address range)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'allowed_addresses' => '::ff-0ffff'
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": invalid address range "::ff-0ffff".'
			],
			'Test proxy.create: invalid "allowed_addresses" (user macro)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'allowed_addresses' => '{$MACRO}'
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": invalid address range "{$MACRO}".'
			],
			'Test proxy.create: invalid "allowed_addresses" (too long)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'allowed_addresses' => str_repeat('a', DB::getFieldLength('proxy', 'allowed_addresses') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": value is too long.'
			],

			// Check "hosts".
			'Test proxy.create: invalid "hosts" (string)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/hosts": an array is expected.'
			],
			'Test proxy.create: invalid "hosts" (array with string)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => ['abc']
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1": an array is expected.'
			],
			'Test proxy.create: missing "hostid" for "hosts"' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => [
						[]
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1": the parameter "hostid" is missing.'
			],
			'Test proxy.create: unexpected parameter for "hosts"' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => [
						['abc' => '']
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1": unexpected parameter "abc".'
			],
			'Test proxy.create: invalid "hostid" (empty string) for "hosts"' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => [
						['hostid' => '']
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1/hostid": a number is expected.'
			],
			'Test proxy.create: invalid "hostid" (non-existent) for "hosts"' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => [
						['hostid' => self::INVALID_NUMBER]
					]
				],
				'expected_error' => 'No permissions to referred object or it does not exist!'
			],
			'Test proxy.create: invalid "hostid" (duplicate) for "hosts"' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'hosts' => [
						['hostid' => 0],
						['hostid' => 0]
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/2": value (hostid)=(0) already exists.'
			],

			// Check "interface".
			'Test proxy.create: invalid parameter "interface" 1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'interface' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1": unexpected parameter "interface".'
			],
			'Test proxy.create: invalid parameter "interface" 2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'interface' => [
						'use_ip' => INTERFACE_USE_IP,
						'ip' => '127.0.0.1',
						'dns' => 'localhost',
						'port' => '10050'
					]
				],
				'expected_error' => 'Invalid parameter "/1": unexpected parameter "interface".'
			],
			'Test proxy.create: empty "address" and "port" for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '',
					'port' => ''
				],
				'expected_error' => 'Invalid parameter "/1/address": cannot be empty.'
			],
			'Test proxy.create: empty "port" for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => ''
				],
				'expected_error' => 'Invalid parameter "/1/port": cannot be empty.'
			],
			'Test proxy.create: invalid "port" (string) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/port": an integer is expected.'
			],
			'Test proxy.create: invalid "address" (too long) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => str_repeat('i', DB::getFieldLength('proxy', 'address') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/address": value is too long.'
			],
			'Test proxy.create: invalid "address" (bool) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => false
				],
				'expected_error' => 'Invalid parameter "/1/address": a character string is expected.'
			],
			'Test proxy.create: invalid parameter "address" (string) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => 'http://',
					'port' => '10050'
				],
				'expected_error' => 'Invalid parameter "/1/address": an IP or DNS is expected.'
			],
			'Test proxy.create: invalid "port" (not in range) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => self::INVALID_NUMBER
				],
				'expected_error' =>	'Invalid parameter "/1/port": value must be one of 0-'.ZBX_MAX_PORT_NUMBER.'.'
			],
			'Test proxy.create: invalid "address" (not empty) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'address' => 'localhost'
				],
				'expected_error' => 'Invalid parameter "/1/address": value must be "127.0.0.1".'
			],
			'Test proxy.create: invalid "port" (not empty int) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'port' => 12345
				],
				'expected_error' =>	'Invalid parameter "/1/port": a character string is expected.'
			],
			'Test proxy.create: invalid "port" (not empty string) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'port' => '12345'
				],
				'expected_error' =>	'Invalid parameter "/1/port": value must be "10051".'
			],

			// Check "tls_connect".
			'Test proxy.create: invalid "tls_connect" (string)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_connect' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_connect": an integer is expected.'
			],
			'Test proxy.create: invalid "tls_connect" (not in range) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_connect' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_connect": value must be '.HOST_ENCRYPTION_NONE.'.'
			],
			'Test proxy.create: invalid "tls_connect" (not in range) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_connect": value must be one of '.
					implode(', ', [HOST_ENCRYPTION_NONE, HOST_ENCRYPTION_PSK, HOST_ENCRYPTION_CERTIFICATE]).'.'
			],

			// Check "tls_accept".
			'Test proxy.create: invalid "tls_accept" (string)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_accept": an integer is expected.'
			],
			'Test proxy.create: invalid "tls_accept" (not in range) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_accept": value must be one of '.HOST_ENCRYPTION_NONE.'-'.
					(HOST_ENCRYPTION_NONE | HOST_ENCRYPTION_PSK | HOST_ENCRYPTION_CERTIFICATE).'.'
			],
			'Test proxy.create: invalid "tls_accept" (not in range) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_accept' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_accept": value must be '.HOST_ENCRYPTION_NONE.'.'
			],

			// Check "tls_psk_identity".
			'Test proxy.create: invalid "tls_psk_identity" (bool)' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_psk_identity' => false
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": a character string is expected.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (string) for active proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (string) for active proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_NONE,
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (string) for active proxy #3' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (string) for passive proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (string) for passive proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_NONE,
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (string) for passive proxy #3' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (empty string) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_PSK,
					'tls_psk_identity' => ''
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": cannot be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (empty string) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_PSK,
					'tls_psk_identity' => ''
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": cannot be empty.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (too long) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_PSK,
					'tls_psk_identity' => str_repeat('i', DB::getFieldLength('proxy', 'tls_psk_identity') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value is too long.'
			],
			'Test proxy.create: invalid "tls_psk_identity" (too long) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_PSK,
					'tls_psk_identity' => str_repeat('i', DB::getFieldLength('proxy', 'tls_psk_identity') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value is too long.'
			],

			// Check "tls_psk".
			'Test proxy.create: invalid "tls_psk" (bool) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_psk' => false
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk": a character string is expected.'
			],
			'Test proxy.create: invalid "tls_psk" (string) for active proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_psk' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk" (string) for active proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_NONE,
					'tls_psk' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk" (string) for active proxy #3' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_psk' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk": value must be empty.'
			],

			'Test proxy.create: invalid "tls_psk" (string) for passive proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_NONE,
					'tls_psk' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk" (string) for passive proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_psk' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk": value must be empty.'
			],
			'Test proxy.create: invalid "tls_psk" (too short) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_PSK,
					'tls_psk' => 'abc'
				],
				'expected_error' =>
					'Invalid parameter "/1/tls_psk": minimum length is 32 characters.'
			],
			'Test proxy.create: invalid "tls_psk" (too short) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_PSK,
					'tls_psk' => 'abc'
				],
				'expected_error' =>
					'Invalid parameter "/1/tls_psk": minimum length is 32 characters.'
			],
			'Test proxy.create: invalid "tls_psk" (not PSK) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_PSK,
					'tls_psk' => str_repeat('a', 33)
				],
				'expected_error' =>
					'Invalid parameter "/1/tls_psk": an even number of hexadecimal characters is expected.'
			],
			'Test proxy.create: invalid "tls_psk" (not PSK) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_PSK,
					'tls_psk' => str_repeat('a', 33)
				],
				'expected_error' =>
					'Invalid parameter "/1/tls_psk": an even number of hexadecimal characters is expected.'
			],

			// Check "tls_issuer".
			'Test proxy.create: invalid "tls_issuer" (bool) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_issuer' => false
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": a character string is expected.'
			],
			'Test proxy.create: invalid "tls_issuer" (not empty) for active proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_NONE,
					'tls_issuer' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": value must be empty.'
			],
			'Test proxy.create: invalid "tls_issuer" (not empty) for active proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_PSK,
					'tls_issuer' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": value must be empty.'
			],
			'Test proxy.create: invalid "tls_issuer" (not empty) for passive proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_NONE,
					'tls_issuer' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": value must be empty.'
			],
			'Test proxy.create: invalid "tls_issuer" (not empty) for passive proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_PSK,
					'tls_issuer' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": value must be empty.'
			],
			'Test proxy.create: invalid "tls_issuer" (too long) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_issuer' => str_repeat('i', DB::getFieldLength('proxy', 'tls_issuer') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": value is too long.'
			],
			'Test proxy.create: invalid "tls_issuer" (too long) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_issuer' => str_repeat('i', DB::getFieldLength('proxy', 'tls_issuer') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_issuer": value is too long.'
			],

			// Check "tls_subject".
			'Test proxy.create: invalid "tls_subject" (not empty) for active proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_NONE,
					'tls_subject' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_subject": value must be empty.'
			],
			'Test proxy.create: invalid "tls_subject" (not empty) for active proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_PSK,
					'tls_subject' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_subject": value must be empty.'
			],
			'Test proxy.create: invalid "tls_subject" (not empty) for passive proxy #1' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_NONE,
					'tls_subject' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_subject": value must be empty.'
			],
			'Test proxy.create: invalid "tls_subject" (not empty) for passive proxy #2' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_PSK,
					'tls_subject' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_subject": value must be empty.'
			],
			'Test proxy.create: invalid "tls_subject" (too long) for active proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE,
					'tls_accept' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_subject' => str_repeat('i', DB::getFieldLength('proxy', 'tls_subject') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_subject": value is too long.'
			],
			'Test proxy.create: invalid "tls_subject" (too long) for passive proxy' => [
				'proxy' => [
					'name' => 'API create proxy',
					'operating_mode' => PROXY_OPERATING_MODE_PASSIVE,
					'address' => '127.0.0.1',
					'port' => '10050',
					'tls_connect' => HOST_ENCRYPTION_CERTIFICATE,
					'tls_subject' => str_repeat('i', DB::getFieldLength('proxy', 'tls_subject') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_subject": value is too long.'
			]
		];
	}

	/**
	 * Data provider for proxy.create. Array contains valid proxies.
	 *
	 * @return array
	 */
	public static function getProxyCreateDataValid(): array {
		return [
			'Test proxy.create: single proxy' => [
				'proxy' => [
					'name' => 'API create single proxy',
					'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
				],
				'expected_error' => null
			],
			'Test proxy.create: multiple proxies' => [
				'proxy' => [
					[
						'name' => 'API create first proxy',
						'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
					],
					[
						'name' => 'API create second proxy',
						'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
					]
				],
				'expected_error' => null
			]
		];
	}

	/**
	 * Test proxy.create with errors like missing fields, optional invalid fields and valid fields.
	 *
	 * @dataProvider getProxyCreateDataInvalid
	 * @dataProvider getProxyCreateDataValid
	 */
	public function testProxy_Create(array $proxies, ?string $expected_error): void {
		// Accept single and multiple proxies just like API method. Work with multidimensional array in result.
		if (!array_key_exists(0, $proxies)) {
			$proxies = zbx_toArray($proxies);
		}

		$sql_proxies = 'SELECT NULL FROM proxy p';
		$old_hash_proxies = CDBHelper::getHash($sql_proxies);

		$result = $this->call('proxy.create', $proxies, $expected_error);

		if ($expected_error === null) {
			// Something was changed in DB.
			$this->assertNotSame($old_hash_proxies, CDBHelper::getHash($sql_proxies));
			$this->assertEquals(count($proxies), count($result['result']['proxyids']));

			// Add proxy IDs to create array, so they can be deleted after tests are complete.
			self::$data['created'] = array_merge(self::$data['created'], $result['result']['proxyids']);

			// Check individual fields according to each proxy operating_mode.
			foreach ($result['result']['proxyids'] as $num => $proxyid) {
				$db_proxies = $this->getProxies([$proxyid]);
				$db_proxy = $db_proxies[$proxyid];

				// Required fields.
				$this->assertNotEmpty($db_proxy['name']);
				$this->assertSame($proxies[$num]['name'], $db_proxy['name']);
				$this->assertEquals($proxies[$num]['operating_mode'], $db_proxy['operating_mode']);

				if (array_key_exists('description', $proxies[$num])) {
					$this->assertSame($proxies[$num]['description'], $db_proxy['description']);
				}
				else {
					$this->assertEmpty($db_proxy['description']);
				}

				if (array_key_exists('allowed_addresses', $proxies[$num])) {
					$this->assertSame($proxies[$num]['allowed_addresses'], $db_proxy['allowed_addresses']);
				}
				else {
					$this->assertSame($db_proxy['allowed_addresses'], DB::getDefault('proxy', 'allowed_addresses'));
				}

				if (array_key_exists('address', $proxies[$num])) {
					$this->assertSame($proxies[$num]['address'], $db_proxy['address']);
				}
				else {
					$this->assertSame($db_proxy['address'], DB::getDefault('proxy', 'address'));
				}

				if (array_key_exists('port', $proxies[$num])) {
					$this->assertSame($proxies[$num]['port'], $db_proxy['port'], 'port should match request');
				}
				else {
					$this->assertSame($db_proxy['port'], DB::getDefault('proxy', 'port'), 'port should match db');
				}

				if (array_key_exists('tls_accept', $proxies[$num])) {
					$this->assertSame($proxies[$num]['tls_accept'], $db_proxy['tls_accept']);
				}
				else {
					$this->assertSame($db_proxy['tls_accept'], DB::getDefault('proxy', 'tls_accept'));
				}

				if (array_key_exists('tls_connect', $proxies[$num])) {
					$this->assertSame($proxies[$num]['tls_connect'], $db_proxy['tls_connect']);
				}
				else {
					$this->assertSame($db_proxy['tls_connect'], DB::getDefault('proxy', 'tls_connect'));
				}

				if (array_key_exists('tls_issuer', $proxies[$num])) {
					$this->assertSame($proxies[$num]['tls_issuer'], $db_proxy['tls_issuer']);
				}
				else {
					$this->assertSame($db_proxy['tls_issuer'], DB::getDefault('proxy', 'tls_issuer'));
				}

				if (array_key_exists('tls_subject', $proxies[$num])) {
					$this->assertSame($proxies[$num]['tls_subject'], $db_proxy['tls_subject']);
				}
				else {
					$this->assertSame($db_proxy['tls_subject'], DB::getDefault('proxy', 'tls_subject'));
				}

				if (array_key_exists('tls_psk_identity', $proxies[$num])) {
					$this->assertSame($proxies[$num]['tls_psk_identity'], $db_proxy['tls_psk_identity']);
				}
				else {
					$this->assertSame($db_proxy['tls_psk_identity'], DB::getDefault('proxy', 'tls_psk_identity'));
				}

				if (array_key_exists('tls_psk', $proxies[$num])) {
					$this->assertSame($proxies[$num]['tls_psk'], $db_proxy['tls_psk']);
				}
				else {
					$this->assertSame($db_proxy['tls_psk'], DB::getDefault('proxy', 'tls_psk'));
				}

				if (array_key_exists('hosts', $proxies[$num])) {
					$this->assertEqualsCanonicalizing($proxies[$num]['hosts'], $db_proxy['hosts']);
				}
				else {
					$this->assertEmpty($db_proxy['hosts']);
				}
			}
		}
		else {
			$this->assertSame($old_hash_proxies, CDBHelper::getHash($sql_proxies));
		}
	}

	/**
	 * Data provider for proxy.get. Array contains invalid proxy parameters.
	 *
	 * @return array
	 */
	public static function getProxyGetDataInvalid(): array {
		return [
			// Check unexpected params.
			'Test proxy.get: unexpected parameter' => [
				'request' => [
					'abc' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/": unexpected parameter "abc".'
			],

			// Check "proxyids" field.
			'Test proxy.get: invalid "proxyids" (empty string)' => [
				'request' => [
					'proxyids' => ''
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/proxyids": an array is expected.'
			],
			'Test proxy.get: invalid "proxyids" (array with empty string)' => [
				'request' => [
					'proxyids' => ['']
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/proxyids/1": a number is expected.'
			],

			// Check filter.
			'Test proxy.get: invalid "filter" (empty string)' => [
				'request' => [
					'filter' => ''
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/filter": an array is expected.'
			],

			// Check unexpected parameters that exist in object, but not in filter.
			'Test proxy.get: unexpected parameter in "filter"' => [
				'request' => [
					'filter' => [
						'proxy_address' => 'proxy_address'
					]
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/filter": unexpected parameter "proxy_address".'
			],

			// Check "search" option.
			'Test proxy.get: invalid "search" (string)' => [
				'request' => [
					'search' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/search": an array is expected.'
			],

			// Check unexpected parameters that exist in object, but not in search.
			'Test proxy.get: unexpected parameter in "search"' => [
				'request' => [
					'search' => [
						'proxyid' => 'proxyid'
					]
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/search": unexpected parameter "proxyid".'
			],

			// Check "output" option.
			'Test proxy.get: invalid parameter "output" (string)' => [
				'request' => [
					'output' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/output": value must be "'.API_OUTPUT_EXTEND.'".'
			],
			'Test proxy.get: invalid parameter "output" (array with string)' => [
				'request' => [
					'output' => ['abc']
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/output/1": value must be one of "proxyid", "name", "operating_mode", "description", "allowed_addresses", "address", "port", "tls_connect", "tls_accept", "tls_issuer", "tls_subject", "lastaccess", "version", "compatibility".'
			],

			// Check write-only fields are not returned.
			'Test proxy.get: write-only field "tls_psk_identity"' => [
				'request' => [
					'output' => ['tls_psk_identity']
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/output/1": value must be one of "proxyid", "name", "operating_mode", "description", "allowed_addresses", "address", "port", "tls_connect", "tls_accept", "tls_issuer", "tls_subject", "lastaccess", "version", "compatibility".'
			],
			'Test proxy.get: write-only field "tls_psk"' => [
				'request' => [
					'output' => ['tls_psk']
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/output/1": value must be one of "proxyid", "name", "operating_mode", "description", "allowed_addresses", "address", "port", "tls_connect", "tls_accept", "tls_issuer", "tls_subject", "lastaccess", "version", "compatibility".'
			],

			// Check "selectHosts" option.
			'Test proxy.get: invalid parameter "selectHosts" (string)' => [
				'request' => [
					'selectHosts' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/selectHosts": value must be "'.API_OUTPUT_EXTEND.'".'
			],
			'Test proxy.get: invalid parameter "selectHosts" (array with string)' => [
				'request' => [
					'selectHosts' => ['abc']
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/selectHosts/1": value must be one of "hostid", "host", "status", "ipmi_authtype", "ipmi_privilege", "ipmi_username", "ipmi_password", "maintenanceid", "maintenance_status", "maintenance_type", "maintenance_from", "name", "flags", "description", "tls_connect", "tls_accept", "tls_issuer", "tls_subject", "inventory_mode", "active_available".'
			],

			// Check common fields that are not flags, but require strict validation.
			'Test proxy.get: invalid parameter "searchByAny" (string)' => [
				'request' => [
					'searchByAny' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/searchByAny": a boolean is expected.'
			],
			'Test proxy.get: invalid parameter "searchWildcardsEnabled" (string)' => [
				'request' => [
					'searchWildcardsEnabled' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/searchWildcardsEnabled": a boolean is expected.'
			],
			'Test proxy.get: invalid parameter "sortfield" (bool)' => [
				'request' => [
					'sortfield' => false
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/sortfield": an array is expected.'
			],
			'Test proxy.get: invalid parameter "sortfield"' => [
				'request' => [
					'sortfield' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/sortfield/1": value must be one of "proxyid", "name", "operating_mode".'
			],
			'Test proxy.get: invalid parameter "sortorder" (bool)' => [
				'request' => [
					'sortorder' => false
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/sortorder": an array or a character string is expected.'
			],
			'Test proxy.get: invalid parameter "sortorder" (not in range)' => [
				'request' => [
					'sortorder' => 'abc'
				],
				'expected_result' => [],
				'expected_error' =>
					'Invalid parameter "/sortorder": value must be one of "'.ZBX_SORT_UP.'", "'.ZBX_SORT_DOWN.'".'
			],
			'Test proxy.get: invalid parameter "limit" (bool)' => [
				'request' => [
					'limit' => false
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/limit": an integer is expected.'
			],
			'Test proxy.get: invalid parameter "editable" (string)' => [
				'request' => [
					'editable' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/editable": a boolean is expected.'
			],
			'Test proxy.get: invalid parameter "preservekeys" (string)' => [
				'request' => [
					'preservekeys' => 'abc'
				],
				'expected_result' => [],
				'expected_error' => 'Invalid parameter "/preservekeys": a boolean is expected.'
			]
		];
	}

	/**
	 * Data provider for proxy.get. Array contains valid proxy parameters.
	 *
	 * @return array
	 */
	public static function getProxyGetDataValid(): array {
		return [
			// Check validity of "proxyids" without getting any results.
			'Test proxy.get: empty "proxyids"' => [
				'request' => [
					'proxyids' => []
				],
				'expected_result' => [],
				'expected_error' => null
			],

			// Check no fields are returned on empty selection.
			'Test proxy.get: empty "output"' => [
				'request' => [
					'output' => [],
					'proxyids' => ['get_active_defaults', 'get_passive_defaults']
				],
				'expected_result' => [
					[],
					[]
				],
				'expected_error' => null
			],

			// Check fields from "proxy_rtdata" table are returned.
			'Test proxy.get: "lastaccess", "version", "compatibility"' => [
				'request' => [
					'output' => ['lastaccess', 'version', 'compatibility'],
					'proxyids' => ['get_version_current', 'get_version_outdated', 'get_version_unsupported']
				],
				'expected_result' => [
					[
						'lastaccess' => '1662034530',
						'version' => '60400',
						'compatibility' => '1'
					],
					[
						'lastaccess' => '1662034225',
						'version' => '60200',
						'compatibility' => '2'
					],
					[
						'lastaccess' => '1651407015',
						'version' => '50401',
						'compatibility' => '3'
					]
				],
				'expected_error' => null
			],

			// Filter by proxy operating_mode.
			'Test proxy.get: filter by "operating_mode"' => [
				'request' => [
					'output' => ['name', 'operating_mode'],
					'proxyids' => ['get_active_defaults', 'get_passive_defaults'],
					'filter' => [
						'operating_mode' => PROXY_OPERATING_MODE_ACTIVE
					]
				],
				'expected_result' => [
					[
						'name' => 'API test proxy.get - active',
						'operating_mode' => (string) PROXY_OPERATING_MODE_ACTIVE
					]
				],
				'expected_error' => null
			],

			// Filter by Zabbix version.
			'Test proxy.get: filter by "version"' => [
				'request' => [
					'output' => ['name', 'version'],
					'proxyids' => ['get_version_current', 'get_version_outdated', 'get_version_unsupported'],
					'filter' => [
						'version' => ['60000', '60200', '60400']
					]
				],
				'expected_result' => [
					[
						'name' => 'API test proxy.get for filter - version current',
						'version' => '60400'
					],
					[
						'name' => 'API test proxy.get for filter - version outdated',
						'version' => '60200'
					]
				],
				'expected_error' => null
			],

			// Filter by version compatibility.
			'Test proxy.get: filter by "compatibility"' => [
				'request' => [
					'output' => ['name', 'compatibility'],
					'proxyids' => ['get_version_current', 'get_version_outdated', 'get_version_unsupported'],
					'filter' => [
						'compatibility' => [ZBX_PROXY_VERSION_OUTDATED, ZBX_PROXY_VERSION_UNSUPPORTED]
					]
				],
				'expected_result' => [
					[
						'name' => 'API test proxy.get for filter - version outdated',
						'compatibility' => (string) ZBX_PROXY_VERSION_OUTDATED
					],
					[
						'name' => 'API test proxy.get for filter - version unsupported',
						'compatibility' => (string) ZBX_PROXY_VERSION_UNSUPPORTED
					]
				],
				'expected_error' => null
			],

			// Search by proxy name.
			'Test proxy.get: search by "name"' => [
				'request' => [
					'output' => ['name'],
					'search' => [
						'name' => 'API test proxy.get - active'
					]
				],
				'expected_result' => [
					['name' => 'API test proxy.get - active']
				],
				'expected_error' => null
			],

			// Filtering by incorrect data types.
			'Test proxy.get: invalid "operating_mode" (string) in "filter"' => [
				'request' => [
					'filter' => [
						'operating_mode' => 'abc'
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "operating_mode" in "filter"' => [
				'request' => [
					'filter' => [
						'operating_mode' => self::INVALID_NUMBER
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "lastaccess" (string) in "filter"' => [
				'request' => [
					'filter' => [
						'lastaccess' => 'abc'
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "lastaccess" (array with string) in "filter"' => [
				'request' => [
					'filter' => [
						'lastaccess' => ['abc']
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "lastaccess" (not in range) in "filter"' => [
				'request' => [
					'filter' => [
						'lastaccess' => [-1]
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "lastaccess" (too large) in "filter"' => [
				'request' => [
					'filter' => [
						'lastaccess' => [ZBX_MAX_DATE + 1]
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "compatibility" (string) for "filter"' => [
				'request' => [
					'filter' => [
						'compatibility' => 'abc'
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: invalid "compatibility" (not in range) for "filter"' => [
				'request' => [
					'filter' => [
						'compatibility' => 999999
					]
				],
				'expected_result' => [],
				'expected_error' => null
			],
			'Test proxy.get: selectHosts=extend excludes proxyid' => [
				'request' => [
					'output' => [],
					'proxyids' => 'select_hosts_extend',
					'selectHosts' => API_OUTPUT_EXTEND
				],
				'expected_result' => [[
					'hosts' => [[
						'hostid' => 'select_fields_host',
						'host' => 'host_fields_host',
						'status' => DB::getDefault('hosts', 'status'),
						'ipmi_authtype' => DB::getDefault('hosts', 'ipmi_authtype'),
						'ipmi_privilege' => DB::getDefault('hosts', 'ipmi_privilege'),
						'ipmi_username' => DB::getDefault('hosts', 'ipmi_username'),
						'ipmi_password' => DB::getDefault('hosts', 'ipmi_password'),
						'maintenanceid' => '0',
						'maintenance_status' => DB::getDefault('hosts', 'maintenance_status'),
						'maintenance_type' => DB::getDefault('hosts', 'maintenance_type'),
						'maintenance_from' => DB::getDefault('hosts', 'maintenance_from'),
						'name' => 'API test host - for selectHosts with extend',
						'flags' => DB::getDefault('hosts', 'flags'),
						'description' => DB::getDefault('hosts', 'description'),
						'tls_connect' => DB::getDefault('hosts', 'tls_connect'),
						'tls_accept' => DB::getDefault('hosts', 'tls_accept'),
						'tls_issuer' => DB::getDefault('hosts', 'tls_issuer'),
						'tls_subject' => DB::getDefault('hosts', 'tls_subject'),
						'inventory_mode' => (string) HOST_INVENTORY_DISABLED,
						'active_available' => '0'
					]]
				]],
				'expected_error' => null
			]
		];
	}

	/**
	 * Test proxy.get with all options.
	 *
	 * @dataProvider getProxyGetDataInvalid
	 * @dataProvider getProxyGetDataValid
	 */
	public function testProxy_Get(array $request, array $expected_result, ?string $expected_error): void {
		// Replace ID placeholders with real IDs.
		$request = self::resolveIds($request);

		foreach ($expected_result as &$proxy) {
			$proxy = self::resolveIds($proxy);
		}
		unset($proxy);

		$result = $this->call('proxy.get', $request, $expected_error);

		if ($expected_error === null) {
			$this->assertSame($expected_result, $result['result']);
		}
	}

	/**
	 * Data provider for proxy.update. Array contains invalid proxy parameters.
	 *
	 * @return array
	 */
	public static function getProxyUpdateDataInvalid(): array {
		return [
			'Test proxy.update: empty request' => [
				'proxy' => [],
				'expected_error' => 'Invalid parameter "/": cannot be empty.'
			],

			// Check "proxyid".
			'Test proxy.update: missing "proxyid"' => [
				'proxy' => [
					'name' => 'API update proxy'
				],
				'expected_error' => 'Invalid parameter "/1": the parameter "proxyid" is missing.'
			],
			'Test proxy.update: invalid "proxyid" (empty string)' => [
				'proxy' => [
					'proxyid' => ''
				],
				'expected_error' => 'Invalid parameter "/1/proxyid": a number is expected.'
			],
			'Test proxy.update: invalid "proxyid" (non-existent)' => [
				'proxy' => [
					'proxyid' => self::INVALID_NUMBER
				],
				'expected_error' => 'No permissions to referred object or it does not exist!'
			],
			'Test proxy.update: multiple proxies with the same "proxyid"' => [
				'proxy' => [
					['proxyid' => 0],
					['proxyid' => 0]
				],
				'expected_error' => 'Invalid parameter "/2": value (proxyid)=(0) already exists.'
			],

			// Check "operating_mode".
			'Test proxy.update: invalid "operating_mode" (string)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'operating_mode' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/operating_mode": an integer is expected.'
			],
			'Test proxy.update: invalid "operating_mode" (not in range)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'operating_mode' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/operating_mode": value must be one of '.
					implode(', ', [PROXY_OPERATING_MODE_ACTIVE, PROXY_OPERATING_MODE_PASSIVE]).'.'
			],

			// Check "host".
			'Test proxy.update: invalid "name" (bool)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'name' => false
				],
				'expected_error' => 'Invalid parameter "/1/name": a character string is expected.'
			],
			'Test proxy.update: invalid "name" (empty string)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'name' => ''
				],
				'expected_error' => 'Invalid parameter "/1/name": cannot be empty.'
			],
			'Test proxy.update: invalid "name" (too long)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'name' => str_repeat('h', DB::getFieldLength('proxy', 'name') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/name": value is too long.'
			],

			// Check "description".
			'Test proxy.update: invalid "description" (bool)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'description' => false
				],
				'expected_error' => 'Invalid parameter "/1/description": a character string is expected.'
			],
			'Test proxy.update: invalid "description" (too long)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'description' => str_repeat('d', DB::getFieldLength('proxy', 'description') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/description": value is too long.'
			],

			// Check "allowed_address".
			'Test proxy.update: invalid "allowed_addresses" (bool)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'allowed_addresses' => false
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": a character string is expected.'
			],
			'Test proxy.update: invalid "allowed_addresses" (IP address range)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'allowed_addresses' => '192.168.0-255.0/30'
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": invalid address range "192.168.0-255.0/30".'
			],
			'Test proxy.update: invalid "allowed_addresses" (IPv6 address range)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'allowed_addresses' => '::ff-0ffff'
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": invalid address range "::ff-0ffff".'
			],
			'Test proxy.update: invalid "allowed_addresses" (user macro)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'allowed_addresses' => '{$MACRO}'
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": invalid address range "{$MACRO}".'
			],
			'Test proxy.update: invalid "allowed_addresses" (too long)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'allowed_addresses' => str_repeat('a', DB::getFieldLength('proxy', 'allowed_addresses') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/allowed_addresses": value is too long.'
			],

			// Check "hosts".
			'Test proxy.update: invalid "hosts" (string)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/hosts": an array is expected.'
			],
			'Test proxy.update: invalid "hosts" (array with string)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => ['abc']
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1": an array is expected.'
			],
			'Test proxy.update: missing "hostid" for "hosts"' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => [
						[]
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1": the parameter "hostid" is missing.'
			],
			'Test proxy.update: unexpected parameter for "hosts"' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => [
						['abc' => '']
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1": unexpected parameter "abc".'
			],
			'Test proxy.update: invalid "hostid" (empty string) for "hosts"' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => [
						['hostid' => '']
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/1/hostid": a number is expected.'
			],
			'Test proxy.update: invalid "hostid" (non-existent) for "hosts"' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => [
						['hostid' => self::INVALID_NUMBER]
					]
				],
				'expected_error' => 'No permissions to referred object or it does not exist!'
			],
			'Test proxy.update: invalid "hostid" (duplicate) for "hosts"' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'hosts' => [
						['hostid' => 0],
						['hostid' => 0]
					]
				],
				'expected_error' => 'Invalid parameter "/1/hosts/2": value (hostid)=(0) already exists.'
			],

			// Check "interface".
			'Test proxy.update: unexpected parameter "interface"' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'interface' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1": unexpected parameter "interface".'
			],
			'Test proxy.update: invalid "address" (bool) for "interface"' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'address' => false
				],
				'expected_error' => 'Invalid parameter "/1/address": a character string is expected.'
			],
			'Test proxy.update: invalid "port" (not in range) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'port' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/port": value must be one of 0-'.ZBX_MAX_PORT_NUMBER.'.'
			],
			'Test proxy.update: invalid "address" (string) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'address' => 'http://'
				],
				'expected_error' => 'Invalid parameter "/1/address": an IP or DNS is expected.'
			],
			'Test proxy.update: invalid "address" (too long) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'address' => str_repeat('a', DB::getFieldLength('proxy', 'address') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/address": value is too long.'
			],
			'Test proxy.update: invalid "port" (bool) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'port' => false
				],
				'expected_error' => 'Invalid parameter "/1/port": a number is expected.'
			],
			'Test proxy.update: invalid "port" (too long) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'port' => str_repeat('d', DB::getFieldLength('proxy', 'port') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/port": value is too long.'
			],
			'Test proxy.update: invalid "port" (string) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'port' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/port": an integer is expected.'
			],
			'Test proxy.update: invalid "port" (not empty int for active proxy)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'port' => 12345
				],
				'expected_error' =>	'Invalid parameter "/1/port": a character string is expected.'
			],
			'Test proxy.update: invalid "address" (not empty for active proxy)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'address' => 'localhost'
				],
				'expected_error' => 'Invalid parameter "/1/address": value must be "127.0.0.1".'
			],
			'Test proxy.update: invalid "port" (not empty string for active proxy)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'port' => '12345'
				],
				'expected_error' =>	'Invalid parameter "/1/port": value must be "10051".'
			],

			// Check "tls_connect".
			'Test proxy.update: invalid "tls_connect" (string)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'tls_connect' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_connect": an integer is expected.'
			],
			'Test proxy.update: invalid "tls_connect" (not in range) for active proxy' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'tls_connect' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_connect": value must be '.HOST_ENCRYPTION_NONE.'.'
			],
			'Test proxy.update: invalid "tls_connect" (not in range) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'tls_connect' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_connect": value must be one of '.
					implode(', ', [HOST_ENCRYPTION_NONE, HOST_ENCRYPTION_PSK, HOST_ENCRYPTION_CERTIFICATE]).'.'
			],

			// Check "tls_accept".
			'Test proxy.update: invalid "tls_accept" (string)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'tls_accept' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_accept": an integer is expected.'
			],
			'Test proxy.update: invalid "tls_accept" (not in range) for active proxy' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'tls_accept' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_accept": value must be one of '.HOST_ENCRYPTION_NONE.'-'.
					(HOST_ENCRYPTION_NONE | HOST_ENCRYPTION_PSK | HOST_ENCRYPTION_CERTIFICATE).'.'
			],
			'Test proxy.update: invalid "tls_accept" (not in range) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'tls_accept' => self::INVALID_NUMBER
				],
				'expected_error' => 'Invalid parameter "/1/tls_accept": value must be '.HOST_ENCRYPTION_NONE.'.'
			],

			// Check "tls_psk_identity".
			'Test proxy.update: invalid "tls_psk_identity" (bool)' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'tls_psk_identity' => false
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": a character string is expected.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (string) for active proxy #1' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults',
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (string) for active proxy #2' => [
				'proxy' => [
					'proxyid' => 'update_active_cert',
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (empty string) for active proxy #1' => [
				'proxy' => [
					'proxyid' => 'update_active_psk',
					'tls_psk_identity' => ''
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": cannot be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (empty string) for active proxy #2' => [
				'proxy' => [
					'proxyid' => 'update_active_any',
					'tls_psk_identity' => ''
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": cannot be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (too long) for active proxy #1' => [
				'proxy' => [
					'proxyid' => 'update_active_psk',
					'tls_psk_identity' => str_repeat('i', DB::getFieldLength('proxy', 'tls_psk_identity') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value is too long.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (too long) for active proxy #2' => [
				'proxy' => [
					'proxyid' => 'update_active_any',
					'tls_psk_identity' => str_repeat('i', DB::getFieldLength('proxy', 'tls_psk_identity') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value is too long.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (string) for passive proxy #1' => [
				'proxy' => [
					'proxyid' => 'update_passive_defaults',
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (string) for passive proxy #2' => [
				'proxy' => [
					'proxyid' => 'update_passive_cert',
					'tls_psk_identity' => 'abc'
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value must be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (empty string) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_psk',
					'tls_psk_identity' => ''
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": cannot be empty.'
			],
			'Test proxy.update: invalid "tls_psk_identity" (too long) for passive proxy' => [
				'proxy' => [
					'proxyid' => 'update_passive_psk',
					'tls_psk_identity' => str_repeat('i', DB::getFieldLength('proxy', 'tls_psk_identity') + 1)
				],
				'expected_error' => 'Invalid parameter "/1/tls_psk_identity": value is too long.'
			]
		];
	}

	/**
	 * Data provider for proxy.update. Array contains valid proxy parameters.
	 *
	 * @return array
	 */
	public static function getProxyUpdateDataValid(): array {
		return [
			'Test proxy.update: update single proxy without changes' => [
				'proxy' => [
					'proxyid' => 'update_active_defaults'
				],
				'expected_error' => null
			],
			'Test proxy.update: update multiple proxies' => [
				'proxy' => [
					[
						'proxyid' => 'update_active_defaults',
						'name' => 'API test proxy.update - active proxy updated',
						'description' => 'Active proxy'
					],
					[
						'proxyid' => 'update_passive_defaults',
						'name' => 'API test proxy.update - passive proxy updated',
						'description' => 'Passive proxy',
						'address' => 'localhost',
						'port' => '10051'
					]
				],
				'expected_error' => null
			],

			// Check proxy can be assigned to host.
			'Test proxy.update: assign proxy to single host' => [
				'proxy' => [
					'proxyid' => 'update_hosts',
					'hosts' => [
						['hostid' => 'without_proxy_1']
					]
				],
				'expected_error' => null
			],
			'Test proxy.update: assign proxy to multiple hosts' => [
				'proxy' => [
					'proxyid' => 'update_hosts',
					'hosts' => [
						['hostid' => 'without_proxy_1'],
						['hostid' => 'without_proxy_2']
					]
				],
				'expected_error' => null
			]
		];
	}

	/**
	 * Test proxy.update method.
	 *
	 * @dataProvider getProxyUpdateDataInvalid
	 * @dataProvider getProxyUpdateDataValid
	 */
	public function testProxy_Update(array $proxies, ?string $expected_error): void {
		// Accept single and multiple proxies just like API method. Work with multidimensional array in result.
		if (!array_key_exists(0, $proxies)) {
			$proxies = zbx_toArray($proxies);
		}

		// Replace ID placeholders with real IDs.
		foreach ($proxies as &$proxy) {
			$proxy = self::resolveIds($proxy);
		}
		unset($proxy);

		$sql_proxies = 'SELECT NULL FROM proxy p';
		$old_hash_proxies = CDBHelper::getHash($sql_proxies);

		if ($expected_error === null) {
			$proxyids = array_column($proxies, 'proxyid');
			$db_proxies = $this->getProxies($proxyids);

			$this->call('proxy.update', $proxies, $expected_error);

			$proxies_upd = $this->getProxies($proxyids);

			// Compare records from DB before and after API call.
			foreach ($proxies as $proxy) {
				$db_proxy = $db_proxies[$proxy['proxyid']];
				$proxy_upd = $proxies_upd[$proxy['proxyid']];

				// Check "name".
				$this->assertNotEmpty($proxy_upd['name']);

				if (array_key_exists('name', $proxy)) {
					$this->assertSame($proxy['name'], $proxy_upd['name']);
				}
				else {
					$this->assertSame($db_proxy['name'], $proxy_upd['name']);
				}

				// Check "operating_mode".
				if (array_key_exists('operating_mode', $proxy)) {
					$this->assertEquals($proxy['operating_mode'], $proxy_upd['operating_mode']);
				}
				else {
					// operating_mode has not changed.
					$this->assertEquals($db_proxy['operating_mode'], $proxy_upd['operating_mode']);
				}

				// Check "description".
				if (array_key_exists('description', $proxy)) {
					$this->assertSame($proxy['description'], $proxy_upd['description']);
				}
				else {
					$this->assertSame($db_proxy['description'], $proxy_upd['description']);
				}

				// Check "allowed_address".
				if (array_key_exists('allowed_addresses', $proxy)) {
					$this->assertSame($proxy['allowed_addresses'], $proxy_upd['allowed_addresses']);
				}
				else {
					$this->assertSame($db_proxy['allowed_addresses'], $proxy_upd['allowed_addresses']);
				}

				// Check "address".
				if (array_key_exists('address', $proxy)) {
					$this->assertSame($proxy['address'], $proxy_upd['address']);
				}
				else {
					$this->assertSame($db_proxy['address'], $proxy_upd['address']);
				}

				// Check "port".
				if (array_key_exists('port', $proxy)) {
					$this->assertSame($proxy['port'], $proxy_upd['port']);
				}
				else {
					$this->assertSame($db_proxy['port'], $proxy_upd['port']);
				}

				// Check hosts.
				if (array_key_exists('hosts', $proxy)) {
					if ($proxy['hosts']) {
						$this->assertNotEmpty($proxy_upd['hosts']);
						$this->assertEqualsCanonicalizing($proxy['hosts'], $proxy_upd['hosts']);
					}
					else {
						$this->assertEmpty($proxy_upd['hosts']);
					}
				}
				else {
					$this->assertEqualsCanonicalizing($db_proxy['hosts'], $proxy_upd['hosts']);
				}
			}

			// Restore proxy original data after each test.
			$this->restoreProxies($db_proxies);
		}
		else {
			// Call method and make sure it really returns the error.
			$this->call('proxy.update', $proxies, $expected_error);

			// Make sure nothing has changed as well.
			$this->assertSame($old_hash_proxies, CDBHelper::getHash($sql_proxies));
		}
	}

	/**
	 * Data provider for proxy.delete. Array contains invalid proxies that are not possible to delete.
	 *
	 * @return array
	 */
	public static function getProxyDeleteDataInvalid(): array {
		return [
			// Check proxy IDs.
			'Test proxy.delete: empty ID' => [
				'proxyids' => [''],
				'expected_error' => 'Invalid parameter "/1": a number is expected.'
			],
			'Test proxy.delete: non-existent ID' => [
				'proxyids' => [self::INVALID_NUMBER],
				'expected_error' => 'No permissions to referred object or it does not exist!'
			],
			'Test proxy.delete: with two same IDs' => [
				'proxyids' => [0, 0],
				'expected_error' => 'Invalid parameter "/2": value (0) already exists.'
			],

			// Check if deleted proxies used to monitor hosts.
			'Test proxy.delete: used in host' => [
				'proxyids' => ['delete_used_in_host'],
				'expected_error' =>
					'Host "API test host - with proxy" is monitored by proxy "API test proxy.delete - used in hosts".'
			],

			// Check if deleted proxies used in actions.
			'Test proxy.delete: used in action' => [
				'proxyids' => ['delete_used_in_action'],
				'expected_error' =>
					'Proxy "API test proxy.delete - used in actions" is used by action "API test discovery action".'
			],

			// Check if deleted proxies used in network discovery rules.
			'Test proxy.delete: used in discovery rule' => [
				'proxyids' => ['delete_used_in_discovery'],
				'expected_error' => 'Proxy "API test proxy.delete - used in discovery rules" is used by discovery rule "API test discovery rule".'
			]
		];
	}

	/**
	 * Data provider for proxy.delete. Array contains valid proxies.
	 *
	 * @return array
	 */
	public static function getProxyDeleteDataValid(): array {
		return [
			'Test proxy.delete: delete single' => [
				'proxy' => ['delete_single'],
				'expected_error' => null
			],
			'Test proxy.delete: delete multiple' => [
				'proxy' => [
					'delete_multiple_1',
					'delete_multiple_2'
				],
				'expected_error' => null
			]
		];
	}

	/**
	 * Test proxy.delete method.
	 *
	 * @dataProvider getProxyDeleteDataInvalid
	 * @dataProvider getProxyDeleteDataValid
	 */
	public function testProxy_Delete(array $proxyids, ?string $expected_error): void {
		// Replace ID placeholders with real IDs.
		foreach ($proxyids as &$proxyid) {
			if (self::isValidIdPlaceholder($proxyid)) {
				$proxyid = self::$data['proxyids'][$proxyid];
			}
		}
		unset($proxyid);

		$sql_proxies = 'SELECT NULL FROM proxy p';
		$old_hash_proxies = CDBHelper::getHash($sql_proxies);

		$this->call('proxy.delete', $proxyids, $expected_error);

		if ($expected_error === null) {
			$this->assertNotSame($old_hash_proxies, CDBHelper::getHash($sql_proxies));
			$this->assertEquals(0, CDBHelper::getCount(
				'SELECT p.proxyid FROM proxy p WHERE '.dbConditionId('p.proxyid', $proxyids)
			));

			// proxy.delete checks if given "proxyid" exists, so they need to be removed from self::$data['proxyids']
			foreach ($proxyids as $proxyid) {
				$key = array_search($proxyid, self::$data['proxyids']);

				if ($key !== false) {
					unset(self::$data['proxyids'][$key]);
				}
			}
		}
		else {
			$this->assertSame($old_hash_proxies, CDBHelper::getHash($sql_proxies));
		}
	}

	/**
	 * Get the original proxies before update.
	 *
	 * @param array $proxyids
	 *
	 * @return array
	 */
	private function getProxies(array $proxyids): array {
		$response = $this->call('proxy.get', [
			'output' => ['proxyid', 'name', 'operating_mode', 'description', 'allowed_addresses', 'address', 'port',
				'tls_connect', 'tls_accept', 'tls_issuer', 'tls_subject', 'lastaccess', 'version', 'compatibility'
			],
			'selectHosts' => ['hostid'],
			'proxyids' => $proxyids,
			'preservekeys' => true,
			'nopermissions' => true
		]);

		$options = [
			'output' => ['proxyid', 'tls_psk_identity', 'tls_psk'],
			'filter' => ['proxyid' => $proxyids]
		];
		$db_proxies = DBselect(DB::makeSql('proxy', $options));

		while ($db_proxy = DBfetch($db_proxies)) {
			$response['result'][$db_proxy['proxyid']]['tls_psk_identity'] = $db_proxy['tls_psk_identity'];
			$response['result'][$db_proxy['proxyid']]['tls_psk'] = $db_proxy['tls_psk'];
		}

		return $response['result'];
	}

	/**
	 * Restore proxies to their original state.
	 *
	 * @param array $proxies
	 */
	private function restoreProxies(array $proxies): void {
		$rtdata_fields = array_flip(['lastaccess', 'version', 'compatibility']);
		$upd_proxy_rtdata = [];

		foreach ($proxies as &$proxy) {
			$upd_proxy_rtdata[] = [
				'values' => array_intersect_key($proxy, $rtdata_fields),
				'where' => ['proxyid' => $proxy['proxyid']]
			];
			$proxy = array_diff_key($proxy, $rtdata_fields);
		}
		unset($proxy);

		$this->call('proxy.update', $proxies);

		DB::update('proxy_rtdata', $upd_proxy_rtdata);
	}

	/**
	 * Delete all created data after test.
	 */
	public static function clearData(): void {
		// Delete actions.
		CDataHelper::call('action.delete', self::$data['actionids']);

		// Delete discovery rules.
		CDataHelper::call('drule.delete', self::$data['druleids']);

		// Delete hosts.
		CDataHelper::call('host.delete', array_values(self::$data['hostids']));

		// Delete host groups.
		CDataHelper::call('hostgroup.delete', self::$data['groupids']);

		// Delete proxies.
		$proxyids = array_values(self::$data['proxyids']);
		$proxyids = array_merge($proxyids, self::$data['created']);
		CDataHelper::call('proxy.delete', $proxyids);
	}

	/**
	 * Helper method to convert placeholders to real IDs.
	 *
	 * @param array $request
	 *
	 * @return array
	 */
	private static function resolveIds(array $request): array {
		if (array_key_exists('proxyids', $request)) {
			if (is_array($request['proxyids'])) {
				foreach ($request['proxyids'] as &$id) {
					if (self::isValidIdPlaceholder($id)) {
						$id = self::$data['proxyids'][$id];
					}
				}
				unset($id);
			}
			elseif (self::isValidIdPlaceholder($request['proxyids'])) {
				$request['proxyids'] = self::$data['proxyids'][$request['proxyids']];
			}
		}
		elseif (array_key_exists('proxyid', $request) && self::isValidIdPlaceholder($request['proxyid'])) {
			$request['proxyid'] = self::$data['proxyids'][$request['proxyid']];
		}

		if (array_key_exists('hosts', $request) && is_array($request['hosts'])) {
			foreach ($request['hosts'] as &$host) {
				if (is_array($host) && array_key_exists('hostid', $host)
						&& self::isValidIdPlaceholder($host['hostid'])) {
					$host['hostid'] = self::$data['hostids'][$host['hostid']];
				}
			}
			unset($host);
		}

		return $request;
	}

	/**
	 * Helper method to check ID placeholder.
	 *
	 * @param $id
	 *
	 * @return bool
	 */
	private static function isValidIdPlaceholder($id): bool {
		// Do not compare != 0 (it will not work) or !== 0 or !== '0' (avoid type check here).
		return !is_array($id) && $id != '0' && $id !== '' && $id !== null && $id != self::INVALID_NUMBER;
	}
}