<?php
/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

require_once dirname(__FILE__).'/../include/CIntegrationTest.php';
require_once dirname(__FILE__).'/../include/CAPITest.php';

/**
 * Test suite for macros with context. It tests the most basic use case from the documentation page.
 * Trapper items are used instead of low level discovery in this test for portability.
 *
 * https://www.zabbix.com/documentation/current/en/manual/config/macros/user_macros_context
 *
 * The following macros are defined:
 *     {$LOW_SPACE_LIMIT} = 10
 *     {$LOW_SPACE_LIMIT:/home} = 20
 *     {$LOW_SPACE_LIMIT:regex:"^/[a-z]+$"} = 30
 *
 * Trapper items are created with triggers having the following expressions:
 *
 *     last(/<host>'/trap1)={$LOW_SPACE_LIMIT}
 *     last(/<host>'/trap2)={$LOW_SPACE_LIMIT:/home}
 *     last(/<host>'/trap3)={$LOW_SPACE_LIMIT:/etc}
 *     last(/<host>'/trap4)={$LOW_SPACE_LIMIT:/tmp}
 *     last(/<host>'/trap5)={$LOW_SPACE_LIMIT:/var}
 *     last(/<host>'/trap5)={$LOW_SPACE_LIMIT:404}
 *
 * Agent items are created with the following keys
 *
 *     system.run["echo {$LOW_SPACE_LIMIT}",wait]
 *     system.run["echo {$LOW_SPACE_LIMIT:/home}",wait]
 *     system.run["echo {$LOW_SPACE_LIMIT:/etc}",wait]
 *     system.run["echo {$LOW_SPACE_LIMIT:/tmp}",wait]
 *     system.run["echo {$LOW_SPACE_LIMIT:/var}",wait]
 *     system.run["echo {$LOW_SPACE_LIMIT:404}",wait
 *
 * The macros in trigger expressions, agent item keys are expected to be expanded like so:
 *     {$LOW_SPACE_LIMIT} => 10
 *     {$LOW_SPACE_LIMIT:/home} = > 20
 *     {$LOW_SPACE_LIMIT:/etc}, {$LOW_SPACE_LIMIT:/tmp}, {$LOW_SPACE_LIMIT:/var} => 30
 *     {$LOW_SPACE_LIMIT:404} => 10
 *
 * Values are sent to trappers which so trigger expressions are expected to become true.
 * Problems for all triggers should start.
 *
 * Agent items are expected to return different values depending on the macro context.
 *
 * @required-components server
 * @onAfter clearData
 */
class testUserMacrosWithContext extends CIntegrationTest {
	const HOSTNAME = 'host_user_macros_with_context';
	const TRAPPER_ITEMS_COUNT = 6;
	const AGENT_ITEMS_COUNT = 6;
	private static $hostId;
	private static $hostInterfaceId;
	private static $macroIds = [];
	private static $triggerIds = [];
	private static $agentItemIds = [];

	/**
	 * @inheritdoc
	 */
	public function prepareData() {
		// Create host
		$response = $this->call('host.create', [
			[
				'host' => self::HOSTNAME,
				'interfaces' => [
					'type' => INTERFACE_TYPE_AGENT,
					'main' => 1,
					'useip' => 1,
					'ip' => '127.0.0.1',
					'dns' => '',
					'port' => PHPUNIT_PORT_PREFIX.self::AGENT_PORT_SUFFIX
				],
				'groups' => [['groupid' => 4]], // Zabbix servers
				'status' => HOST_STATUS_MONITORED
			]
		]);
		$this->assertArrayHasKey('result', $response);
		$this->assertArrayHasKey('hostids', $response['result']);
		$this->assertArrayHasKey(0, $response['result']['hostids']);
		self::$hostId = $response['result']['hostids'][0];

		// Get host interface ID for agent checks.
		$response = $this->call('host.get', [
			'output' => ['host'],
			'hostids' => [self::$hostId],
			'selectInterfaces' => ['interfaceid']
		]);

		$this->assertArrayHasKey(0, $response['result']);
		$this->assertArrayHasKey('interfaces', $response['result'][0]);
		$this->assertArrayHasKey(0, $response['result'][0]['interfaces']);
		self::$hostInterfaceId = $response['result'][0]['interfaces'][0]['interfaceid'];

		// Create trapper items
		$trapperItems = [];
		for ($i = 0; $i < self::TRAPPER_ITEMS_COUNT; $i++) {
			$trapperItems[] = [
				'hostid' => self::$hostId,
				'name' => sprintf("trapper item %d", $i+1),
				'key_' => sprintf("trap%d", $i+1),
				'type' => ITEM_TYPE_TRAPPER,
				'value_type' => ITEM_VALUE_TYPE_UINT64
			];
		}
		$response = $this->call('item.create', $trapperItems);
		$this->assertArrayHasKey('result', $response);
		$this->assertArrayHasKey('itemids', $response['result']);
		for ($i = 0; $i < self::TRAPPER_ITEMS_COUNT; $i++) {
			$this->assertArrayHasKey($i, $response['result']['itemids']);
		}

		// Create agent items
		$agentItemKeys = [
			'system.run["echo {$LOW_SPACE_LIMIT}",wait]',
			'system.run["echo {$LOW_SPACE_LIMIT:/home}",wait]',
			'system.run["echo {$LOW_SPACE_LIMIT:/etc}",wait]',
			'system.run["echo {$LOW_SPACE_LIMIT:/tmp}",wait]',
			'system.run["echo {$LOW_SPACE_LIMIT:/var}",wait]',
			'system.run["echo {$LOW_SPACE_LIMIT:404}",wait]'
		];
		$agentItems = [];
		for ($i = 0; $i < self::AGENT_ITEMS_COUNT; $i++) {
			$agentItems[] = [
				'hostid' => self::$hostId,
				'interfaceid' => self::$hostInterfaceId,
				'name' => sprintf("agent item %d", $i+1),
				'key_' => $agentItemKeys[$i],
				'type' => ITEM_TYPE_ZABBIX,
				'value_type' => ITEM_VALUE_TYPE_TEXT,
				'delay' => '1s'
			];
		}
		$response = $this->call('item.create', $agentItems);
		$this->assertArrayHasKey('result', $response);
		$this->assertArrayHasKey('itemids', $response['result']);
		for ($i = 0; $i < self::AGENT_ITEMS_COUNT; $i++) {
			$this->assertArrayHasKey($i, $response['result']['itemids']);
			self::$agentItemIds[] = $response['result']['itemids'][$i];
		}

		// Create triggers, one trigger for each trapper item
		$triggerExpressions = [
			'last(/'.self::HOSTNAME.'/trap1)={$LOW_SPACE_LIMIT}',
			'last(/'.self::HOSTNAME.'/trap2)={$LOW_SPACE_LIMIT:/home}',
			'last(/'.self::HOSTNAME.'/trap3)={$LOW_SPACE_LIMIT:/etc}',
			'last(/'.self::HOSTNAME.'/trap4)={$LOW_SPACE_LIMIT:/tmp}',
			'last(/'.self::HOSTNAME.'/trap5)={$LOW_SPACE_LIMIT:/var}',
			'last(/'.self::HOSTNAME.'/trap6)={$LOW_SPACE_LIMIT:404}',
		];
		$triggers = [];
		for ($i = 0; $i < self::TRAPPER_ITEMS_COUNT; $i++) {
			$triggers[] = [
				'expression' => $triggerExpressions[$i],
				'event_name' => sprintf("event%d", $i+1),
				'description' => 'description'
				// 'description' => sprintf("description %d", $i+1)
			];
		}
		$response = $this->call('trigger.create', $triggers);
		$this->assertArrayHasKey('result', $response);
		$this->assertArrayHasKey('triggerids', $response['result']);
		for ($i = 0; $i < self::TRAPPER_ITEMS_COUNT; $i++) {
			$this->assertArrayHasKey($i, $response['result']['triggerids']);
			self::$triggerIds[] = $response['result']['triggerids'][$i];
		}

		// Create macros
		$response = $this->call('usermacro.create', [

			[
				'hostid' => self::$hostId,
				'macro'  => '{$LOW_SPACE_LIMIT}',
				'value'  => '10'
			],
			[
				'hostid' => self::$hostId,
				'macro'  => '{$LOW_SPACE_LIMIT:/home}',
				'value'  => '20'
			],
			[
				'hostid' => self::$hostId,
				'macro'  => '{$LOW_SPACE_LIMIT:regex:"^/[a-z]+$"}',
				'value'  => '30'
			]
		]);
		$this->assertArrayHasKey('result', $response);
		$this->assertArrayHasKey('hostmacroids', $response['result']);
		for ($i = 0; $i < 3; $i++) {
			$this->assertArrayHasKey($i, $response['result']['hostmacroids']);
			self::$macroIds[] = $response['result']['hostmacroids'][$i];
		}
	}

	/**
	 *
	 * @required-components server
	 */
	public function testUserMacrosWithContext_inTriggerExpressions() {
		$this->sendSenderValue(self::HOSTNAME, 'trap1', 10);
		$this->sendSenderValue(self::HOSTNAME, 'trap2', 20);
		$this->sendSenderValue(self::HOSTNAME, 'trap3', 30);
		$this->sendSenderValue(self::HOSTNAME, 'trap4', 30);
		$this->sendSenderValue(self::HOSTNAME, 'trap5', 30);
		$this->sendSenderValue(self::HOSTNAME, 'trap6', 10);

		$response = $this->callUntilDataIsPresent('problem.get', [
			'output' => ['name'],
			'hostids' => [self::$hostId],
			'source' => EVENT_SOURCE_TRIGGERS,
			'object' => EVENT_OBJECT_TRIGGER
		]);
		$this->assertArrayHasKey('result', $response);
		$evtNames = [];
		for ($i = 0; $i < count($response['result']); $i++) {
			$this->assertArrayHasKey($i, $response['result']);
			$this->assertArrayHasKey('name', $response['result'][$i]);
			$evtNames[] = $response['result'][$i]['name'];
		}

		$this->assertContains('event1', $evtNames, 'Test case for macro {$LOW_SPACE_LIMIT} in trigger expression failed');
		$this->assertContains('event2', $evtNames, 'Test case for macro {$LOW_SPACE_LIMIT:/home} in trigger expression failed');
		$this->assertContains('event3', $evtNames, 'Test case for macro {$LOW_SPACE_LIMIT:/etc} in trigger expression failed');
		$this->assertContains('event4', $evtNames, 'Test case for macro {$LOW_SPACE_LIMIT:/tmp} in trigger expression failed');
		$this->assertContains('event5', $evtNames, 'Test case for macro {$LOW_SPACE_LIMIT:/var} in trigger expression failed');
		$this->assertContains('event6', $evtNames, 'Test case for macro {$LOW_SPACE_LIMIT:404} in trigger expression failed');
	}

	/**
	 * Component configuration provider for agent related tests.
	 *
	 * @return array
	 */
	public function agentConfigurationProvider() {
		return [
			self::COMPONENT_AGENT => [
				'Hostname' => self::HOSTNAME,
				'AllowKey' => 'system.run[*]'
			]
		];
	}

	/**
	 *
	 * @required-components server, agent
	 * @configurationDataProvider agentConfigurationProvider
	 */
	public function testUserMacrosWithContext_inAgentItemKeys() {
		$wait_iterations = 10;
		$wait_iteration_delay = 2;

		for ($r = 0; $r < $wait_iterations; $r++) {

			$response = $this->call('item.get', [
				'output' => ['lastvalue', 'lastclock'],
				'itemids' => self::$agentItemIds,
				'preservekeys' => true
			]);
			$this->assertArrayHasKey('result', $response);

			$all_collected = true;

			foreach (self::$agentItemIds as $itemId) {
				if ($response['result'][$itemId]['lastclock'] == 0) {
					$all_collected = false;
				}
			}

			if ($all_collected) {
				break;
			}

			sleep($wait_iteration_delay);
		}

		$lastValues =[];
		foreach (self::$agentItemIds as $itemId) {
			$this->assertArrayHasKey('lastvalue', $response['result'][$itemId]);
			$lastValues[] = $response['result'][$itemId]['lastvalue'];
		}

		$this->assertEquals('10', $lastValues[0], 'Test case for macro {$LOW_SPACE_LIMIT} in agent item key failed');
		$this->assertEquals('20', $lastValues[1], 'Test case for macro {$LOW_SPACE_LIMIT:/home} in agent item key failed');
		$this->assertEquals('30', $lastValues[2], 'Test case for macro {$LOW_SPACE_LIMIT:/etc} in agent item key failed');
		$this->assertEquals('30', $lastValues[3], 'Test case for macro {$LOW_SPACE_LIMIT:/tmp} in agent item key failed');
		$this->assertEquals('30', $lastValues[4], 'Test case for macro {$LOW_SPACE_LIMIT:/var} in agent item key failed');
		$this->assertEquals('10', $lastValues[5], 'Test case for macro {$LOW_SPACE_LIMIT:404} in agent item key failed');
	}

	/**
	 * Delete data objects created for this test suite
	 *
	 */
	public static function clearData(): void {
		// Triggers, items, and user macros should be cascade deleted.
		CDataHelper::call('host.delete', [self::$hostId]);
	}
}