<?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/CWebTest.php';

/**
 * @backup profiles
 *
 * @onBefore prepareAlarmData
 *
 * @onAfterEach closeAndAcknowledgeEvents
 */
class testAlarmNotification extends CWebTest {

	/**
	 * Attach MessageBehavior to the test.
	 *
	 * @return array
	 */
	public function getBehaviors() {
		return [
			CTableBehavior::class
		];
	}

	protected static $maintenanceid;
	protected static $eventids;
	protected static $hostid;
	const DEFAULT_COLORPICKER = 'xpath:./following::div[@class="color-picker"]';

	/**
	 * Trigger names.
	 */
	const ALL_TRIGGERS = [
		'Average_trigger',
		'Disaster_trigger',
		'High_trigger',
		'Information_trigger',
		'Not_classified_trigger',
		'Warning_trigger'
	];

	const HOST_NAME = 'Host for alarm item';

	public static function prepareAlarmData() {
		$response = CDataHelper::createHosts([
			[
				'host' => self::HOST_NAME,
				'groups' => [['groupid' => 4]], // Zabbix server
				'items' => [
					[
						'name' => 'Not classified',
						'key_' => 'not_classified',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					],
					[
						'name' => 'Information',
						'key_' => 'information',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					],
					[
						'name' => 'Warning',
						'key_' => 'warning',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					],
					[
						'name' => 'Average',
						'key_' => 'average',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					],
					[
						'name' => 'High',
						'key_' => 'high',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					],
					[
						'name' => 'Disaster',
						'key_' => 'disaster',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					],
					[
						'name' => 'Multiple errors',
						'key_' => 'multiple_errors',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					]
				]
			],
			[
				'host' => 'Host for maintenance alarm',
				'groups' => [['groupid' => 4]], // Zabbix server
				'items' => [
					[
						'name' => 'Suppressed item',
						'key_' => 'suppressed_item',
						'type' => ITEM_TYPE_TRAPPER,
						'value_type' => ITEM_VALUE_TYPE_UINT64
					]
				]
			]
		]);
		self::$hostid = $response['hostids'];

		CDataHelper::call('trigger.create', [
			[
				'description' => 'Not_classified_trigger',
				'expression' => 'last(/Host for alarm item/not_classified)=0',
				'priority' => TRIGGER_SEVERITY_NOT_CLASSIFIED,
				'manual_close' => 1
			],
			[
				'description' => 'Not_classified_trigger_2',
				'expression' => 'last(/Host for alarm item/not_classified)=1',
				'priority' => TRIGGER_SEVERITY_NOT_CLASSIFIED,
				'manual_close' => 1
			],
			[
				'description' => 'Not_classified_trigger_3',
				'expression' => 'last(/Host for alarm item/not_classified)=1',
				'priority' => TRIGGER_SEVERITY_NOT_CLASSIFIED,
				'manual_close' => 1
			],
			[
				'description' => 'Not_classified_trigger_4',
				'expression' => 'last(/Host for alarm item/not_classified)=2',
				'priority' => TRIGGER_SEVERITY_NOT_CLASSIFIED,
				'manual_close' => 1
			],
			[
				'description' => 'Information_trigger',
				'expression' => 'last(/Host for alarm item/information)=1',
				'priority' => TRIGGER_SEVERITY_INFORMATION,
				'manual_close' => 1
			],
			[
				'description' => 'Warning_trigger',
				'expression' => 'last(/Host for alarm item/warning)=2',
				'priority' => TRIGGER_SEVERITY_WARNING,
				'manual_close' => 1
			],
			[
				'description' => 'Average_trigger',
				'expression' => 'last(/Host for alarm item/average)=3',
				'priority' => TRIGGER_SEVERITY_AVERAGE,
				'manual_close' => 1
			],
			[
				'description' => 'High_trigger',
				'expression' => 'last(/Host for alarm item/high)=4',
				'priority' => TRIGGER_SEVERITY_HIGH,
				'manual_close' => 1
			],
			[
				'description' => 'Disaster_trigger',
				'expression' => 'last(/Host for alarm item/disaster)=5',
				'priority' => TRIGGER_SEVERITY_DISASTER,
				'manual_close' => 1
			],
			[
				'description' => 'Multiple_errors',
				'expression' => 'last(/Host for alarm item/multiple_errors)=0',
				'priority' => TRIGGER_SEVERITY_NOT_CLASSIFIED,
				'manual_close' => 1,
				'type' => 1
			],
			[
				'description' => 'Suppressed_error',
				'expression' => 'last(/Host for maintenance alarm/suppressed_item)=0',
				'priority' => TRIGGER_SEVERITY_DISASTER,
				'manual_close' => 1,
				'type' => 1
			]
		]);

		// Enable Alarm Notification display for user.
		DBexecute('INSERT INTO profiles (profileid, userid, idx, value_str, source, type)'.
				' VALUES (555,1,'.zbx_dbstr('web.messages').',1,'.zbx_dbstr('enabled').',3)');
		DBexecute('INSERT INTO profiles (profileid, userid, idx, value_str, source, type)'.
				' VALUES (556,1,'.zbx_dbstr('web.messages').',180,'.zbx_dbstr('timeout').',3)');

		// Create Maintenance and host in maintenance.
		$maintenance = CDataHelper::call('maintenance.create', [
			[
				'name' => 'Alarm notification maintenance',
				'active_since' => time() - 1000,
				'active_till' => time() + 31536000,
				'hosts' => [['hostid' => self::$hostid['Host for maintenance alarm']]],
				'timeperiods' => [[]]
			]
		]);
		self::$maintenanceid = $maintenance['maintenanceids'][0];

		DBexecute('UPDATE hosts SET maintenanceid='.zbx_dbstr(self::$maintenanceid).
			', maintenance_status='.HOST_MAINTENANCE_STATUS_ON.', maintenance_type='.MAINTENANCE_TYPE_NORMAL.', maintenance_from='.zbx_dbstr(time()-1000).
			' WHERE hostid='.zbx_dbstr(self::$hostid['Host for maintenance alarm'])
		);
	}

	/**
	 * Check Alarm notification overlay dialog layout.
	 *
	 * @onAfter openResetedPage
	 */
	public function testAlarmNotification_Layout() {
		// Trigger problem.
		$time = time();
		$event_time = date('Y-m-d H:i:s', $time);
		self::$eventids = CDBHelper::setTriggerProblem('Not_classified_trigger_4', TRIGGER_VALUE_TRUE, ['clock' => $time]);

		$this->page->login()->open('zabbix.php?action=problem.view')->waitUntilReady();

		// Find appeared Alarm notification overlay dialog.
		$alarm_dialog = $this->getAlarmOverlay();

		// Check that Problem on text exists.
		$this->assertEquals('Problem on Host for alarm item', $alarm_dialog->query('xpath:.//h4')->one()->getText());

		// Check that link for host and trigger filtering works.
		foreach (['Hosts' => self::HOST_NAME, 'Triggers' => 'Not_classified_trigger_4'] as $field => $name) {
			$alarm_dialog->query('link', $name)->waitUntilClickable()->one()->click();
			$this->page->waitUntilReady();

			// Check that opens Monitoring->Problems page and correct values filtered.
			$this->page->assertTitle('Problems');
			$this->page->assertHeader('Problems');
			$form = $this->query('name:zbx_filter')->asForm()->one();

			if ($field === 'Triggers') {
				$name = 'Host for alarm item: '.$name;
			}

			$form->checkValue([$field => $name]);
		}

		// Check that after clicking on time - Event page opens.
		$alarm_dialog->query('link', $event_time)->waitUntilClickable()->one()->click();
		$this->page->waitUntilReady();
		$this->page->assertTitle('Event details');
		$this->page->assertHeader('Event details');

		// Check that events details opened for correct trigger/host.
		$table = $this->query('xpath://section[@id="hat_triggerdetails"]/div/table')->asTable()->one();
		$this->assertEquals(['Host', 'Host for alarm item'], $table->getRow(0)->getColumns()->asText());
		$this->assertEquals(['Trigger', 'Not_classified_trigger_4'], $table->getRow(1)->getColumns()->asText());

		// Check displayed icons.
		foreach (['Mute for Admin' => 'btn-icon zi-speaker', 'Snooze for Admin' => 'btn-icon zi-bell'] as $button => $class) {
			$selector = 'xpath:.//button[@title='.CXPathHelper::escapeQuotes($button).']';

			// Check that buttons exists and class says that button is ON.
			$this->assertTrue($alarm_dialog->query($selector)->exists());
			$this->assertEquals($class, $alarm_dialog->query($selector)->one()->getAttribute('class'));

			if ($button === 'Mute for Admin') {
				// After clicking on button it changes status to off and become Unmute.
				$alarm_dialog->query($selector)->one()->click();
				$alarm_dialog->query('xpath:.//button[@title="Unmute for Admin"]')->waitUntilVisible()->one();
				$this->assertEquals($class.'-off', $alarm_dialog->query('xpath:.//button[@title="Unmute for Admin"]')
					->one()->getAttribute('class')
				);

				// Check that after clicking on Unmute button, Mute icon changed back.
				$alarm_dialog->query('xpath:.//button[@title="Unmute for Admin"]')->one()->click();
				$alarm_dialog->query($selector)->waitUntilVisible()->one();
				$this->assertEquals($class, $alarm_dialog->query($selector)->one()->getAttribute('class'));
			}
			else {
				// Check that after clicking second time on already Snoozed button, it doesn't change status.
				for ($i = 0; $i <=1; $i++) {
					$alarm_dialog->query($selector)->one()->click();
					$this->page->refresh()->waitUntilReady();
					$this->assertTrue($alarm_dialog->query($selector)->exists());
					$this->assertEquals($class.'-off', $alarm_dialog->query($selector)->one()->getAttribute('class'));
				}
			}
		}

		// Close problem and open Problem page.
		CDBHelper::setTriggerProblem('Not_classified_trigger_4', TRIGGER_VALUE_FALSE);
		$this->page->open('zabbix.php?action=problem.view')->waitUntilReady();

		// Check that problem resolved and problem color is green now.
		$this->assertEquals('Resolved Host for alarm item', $alarm_dialog->query('xpath:.//h4')->one()->getText());
		$this->assertEquals('rgba(89, 219, 143, 1)', $alarm_dialog->query('class:notif-indic')
				->one()->getCSSValue('background-color')
		);

		// Check close button.
		$alarm_dialog->query('xpath:.//button[@title="Close"]')->one()->click()->waitUntilNotVisible();
	}

	/**
	 * Check that colors displayed in alarm notification overlay are the same as in configuration.
	 */
	public function testAlarmNotification_CheckColorChange() {
		// Trigger problem.
		self::$eventids = CDBHelper::setTriggerProblem(self::ALL_TRIGGERS);

		$severity_names = [
			'Disaster' => '00FF00',
			'High' => '00FF00',
			'Average' => '00FFFF',
			'Warning' => '00FFFF',
			'Information' => 'FF0080',
			'Not classified' => 'FF0080'
		];

		// Open Trigger displaying options page for color check and change.
		$this->page->login()->open('zabbix.php?action=trigdisplay.edit')->waitUntilReady();
		$form = $this->query('id:trigdisplay-form')->asForm()->one();

		// Find actual colors for all severity levels.
		$default_colors = [];
		foreach ($severity_names as $severity_name => $hexa_color) {
			$field = $form->getField($severity_name);
			$default_colors[] = $field->query(self::DEFAULT_COLORPICKER.'/button')->one()->getCSSValue('background-color');
		}

		// Compare colors in alarm and in form.
		$alarm_colors = $this->getAlarmColors();
		$this->assertEquals($default_colors, $alarm_colors);

		// Change color for every severity.
		foreach ($severity_names as $severity_name => $color) {
			$form->getField($severity_name)->query(self::DEFAULT_COLORPICKER)->asColorPicker()->one()->fill($color);
		}

		$form->submit();
		$this->page->waitUntilReady();
		$form->invalidate();

		$changed_colors = [];
		foreach ($severity_names as $severity_name => $color) {
			$field = $form->getField($severity_name);
			$changed_colors[] = $field->query(self::DEFAULT_COLORPICKER.'/button')->one()->getCSSValue('background-color');
		}

		// Compare colors in alarm and in form after change.
		$alarm_colors_changed = $this->getAlarmColors();
		$this->assertEquals($changed_colors, $alarm_colors_changed);

		// Check close button and close the problems.
		$alarm_dialog = $this->getAlarmOverlay();
		$alarm_dialog->query('xpath:.//button[@title="Close"]')->one()->click()->waitUntilNotVisible();
	}

	public static function getDisplayedProblemsData() {
		return [
			// #0 Not classified.
			[
				[
					'trigger_name' => ['Not_classified_trigger']
				]
			],
			// #1 Two problems at once.
			[
				[
					'trigger_name' => ['Not_classified_trigger_2', 'Not_classified_trigger_3']
				]
			],
			// #2 Information.
			[
				[
					'trigger_name' => ['Information_trigger']
				]
			],
			//#3 Warning.
			[
				[
					'trigger_name' => ['Warning_trigger']
				]
			],
			//#4 Average.
			[
				[
					'trigger_name' => ['Average_trigger']
				]
			],
			//#5 High.
			[
				[
					'trigger_name' => ['High_trigger']
				]
			],
			//#6 Disaster.
			[
				[
					'trigger_name' => ['Disaster_trigger']
				]
			],
			// #7 All together.
			[
				[
					'trigger_name' => [
						'Average_trigger',
						'Disaster_trigger',
						'High_trigger',
						'Information_trigger',
						'Not_classified_trigger',
						'Warning_trigger'
					]
				]
			],
			//#8 Multiple same error for one trigger.
			[
				[
					'trigger_name' => [
						'Disaster_trigger',
						'Disaster_trigger',
						'Disaster_trigger',
						'Disaster_trigger'
					]
				]
			]
		];
	}

	/**
	 * Check that correct problems displayed in alarm notification overlay.
	 *
	 * @dataProvider getDisplayedProblemsData
	 */
	public function testAlarmNotification_DisplayedProblems($data) {
		// Trigger problem.
		self::$eventids = CDBHelper::setTriggerProblem($data['trigger_name']);

		// Open problem page and filter with correct host.
		$this->page->login()->open('zabbix.php?action=problem.view&acknowledgement_status=1&sort=name&sortorder=ASC&hostids%5B%5D='.
				self::$hostid[self::HOST_NAME])->waitUntilReady();

		// Check that problems displayed in table.
		$this->assertTableDataColumn($data['trigger_name'], 'Problem');

		// Find appeared Alarm notification overlay dialog and check triggered problems by trigger name.
		$alarm_dialog = $this->getAlarmOverlay();
		$triggered_alarms = $alarm_dialog->query('xpath:.//ul[@class="notif-body"]/li//a[contains(@href, "triggerids")]')
				->all()->asText();
		sort($triggered_alarms);
		$this->assertEquals($data['trigger_name'], $triggered_alarms);

		// Check close button and close the problems.
		$alarm_dialog->query('xpath:.//button[@title="Close"]')->one()->click()->waitUntilNotVisible();
	}

	public static function getNotificationSettingsData() {
		return [
			// #0 Not classified turned off.
			[
				[
					'profile_setting' => ['Not classified' => false],
					'trigger_name' => [
						'Average_trigger',
						'Disaster_trigger',
						'High_trigger',
						'Information_trigger',
						'Warning_trigger'
					]
				]
			],
			// #1 Information turned off.
			[
				[
					'profile_setting' => ['Information' => false],
					'trigger_name' => [
						'Average_trigger',
						'Disaster_trigger',
						'High_trigger',
						'Not_classified_trigger',
						'Warning_trigger'
					]
				]
			],
			// #2 Warning turned off.
			[
				[
					'profile_setting' => ['Warning' => false],
					'trigger_name' => [
						'Average_trigger',
						'Disaster_trigger',
						'High_trigger',
						'Information_trigger',
						'Not_classified_trigger'
					]
				]
			],
			// #3 Average turned off.
			[
				[
					'profile_setting' => ['Average' => false],
					'trigger_name' => [
						'Disaster_trigger',
						'High_trigger',
						'Information_trigger',
						'Not_classified_trigger',
						'Warning_trigger'
					]
				]
			],
			// #4 High turned off.
			[
				[
					'profile_setting' => ['High' => false],
					'trigger_name' => [
						'Average_trigger',
						'Disaster_trigger',
						'Information_trigger',
						'Not_classified_trigger',
						'Warning_trigger'
					]
				]
			],
			// #5 Disaster turned off.
			[
				[
					'profile_setting' => ['Disaster' => false],
					'trigger_name' => [
						'Average_trigger',
						'High_trigger',
						'Information_trigger',
						'Not_classified_trigger',
						'Warning_trigger'
					]
				]
			],
			// #6 Not classified and High severities turned off.
			[
				[
					'profile_setting' => [
						'Not classified' => false,
						'High' => false
					],
					'trigger_name' => [
						'Average_trigger',
						'Disaster_trigger',
						'Information_trigger',
						'Warning_trigger'
					]
				]
			],
			// #7 Display suppressed problems.
			[
				[
					'profile_setting' => ['Show suppressed problems' => true],
					'suppressed_problem' => ['Suppressed_error'],
					'trigger_name' => ['Suppressed_error']
				]
			],
			// #8 Don't display suppressed problems.
			[
				[
					'profile_setting' => ['Show suppressed problems' => false],
					'suppressed_problem' => ['Suppressed_error'],
					'trigger_name' => ''
				]
			],
			// #9 All turned off.
			[
				[
					'profile_setting' => [
						'Not classified' => false,
						'Information' => false,
						'Warning' => false,
						'Average' => false,
						'High' => false,
						'Disaster' => false
					],
					'trigger_name' => ''
				]
			],
			// #10 Message notification turned off.
			[
				[
					'profile_setting' => ['Frontend notifications' => false],
					'trigger_name' => ''
				]
			]
		];
	}

	/**
	 * Check notification display after changing user Frontend notification settings.
	 *
	 * @onBefore resetTriggerSeverities
	 *
	 * @dataProvider getNotificationSettingsData
	 */
	public function testAlarmNotification_NotificationSettings($data) {
		// Set checked trigger severity in messaging settings.
		$this->page->login()->open('zabbix.php?action=userprofile.edit')->waitUntilReady();
		$form = $this->query('id:user-form')->asForm()->one();
		$form->selectTab('Frontend notifications');
		$form->fill($data['profile_setting']);
		$form->submit();
		$this->page->waitUntilReady();

		// Trigger problem.
		if (array_key_exists('suppressed_problem', $data)) {
			self::$eventids = CDBHelper::setTriggerProblem($data['suppressed_problem']);
			$time = time()+10000;

			// To check that suppressed notification can be visible after profile settings change.
			DBexecute('INSERT INTO event_suppress (event_suppressid, eventid, maintenanceid, suppress_until) VALUES '.
					'('.zbx_dbstr(self::$eventids[0]).', '.zbx_dbstr(self::$eventids[0]).', '.
					zbx_dbstr(self::$maintenanceid).', '.zbx_dbstr($time).')');
		}
		else {
			self::$eventids = CDBHelper::setTriggerProblem(self::ALL_TRIGGERS);
		}

		$this->page->login()->open('zabbix.php?action=problem.view&acknowledgement_status=1&show_suppressed=1&sort=name&sortorder=ASC&hostids%5B%5D='.
				self::$hostid[self::HOST_NAME].'&hostids%5B%5D='.self::$hostid['Host for maintenance alarm'])->waitUntilReady();

		// Check that problems displayed in table.
		$triggered_problems = (array_key_exists('suppressed_problem', $data)) ? $data['suppressed_problem'] : self::ALL_TRIGGERS;
		$this->assertTableDataColumn($triggered_problems, 'Problem');

		if ($data['trigger_name'] === '') {
			$this->assertFalse($this->query('xpath://div[@class="overlay-dialogue notif ui-draggable"]')->one()->isDisplayed());
		}
		else {
			// Find appeared Alarm notification overlay dialog and check triggered problems by trigger name.
			$alarm_dialog = $this->getAlarmOverlay();
			$triggered_alarms = $alarm_dialog->query('xpath:.//ul[@class="notif-body"]/li//a[contains(@href, "triggerids")]')
					->waitUntilVisible()->all()->asText();
			sort($triggered_alarms);
			$this->assertEquals($data['trigger_name'], $triggered_alarms);

			// Check close button.
			$alarm_dialog->query('xpath:.//button[@title="Close"]')->one()->click()->waitUntilNotVisible();
		}

		// Delete the events so they don't appear in the next test case.
		DB::delete('events', ['eventid' => self::$eventids]);
	}

	/**
	 * Update Frontend notifications settings, set all severities checkboxes => true.
	 */
	protected function resetTriggerSeverities() {
		// Delete old setting whatever it was.
		DBexecute('DELETE FROM profiles WHERE source='.zbx_dbstr('triggers.severities').' AND userid=1');

		// Insert new row where value_str field means that all severities are checked.
		DBexecute('INSERT INTO profiles (profileid, userid, idx, value_str, source, type)'.
				' VALUES (9950, 1, '.zbx_dbstr('web.messages').', '.
				zbx_dbstr('a:6:{i:0;s:1:"1";i:1;s:1:"1";i:2;s:1:"1";i:3;s:1:"1";i:4;s:1:"1";i:5;s:1:"1";}').', '.
				zbx_dbstr('triggers.severities').', 3)'
		);
	}

	/**
	 * Get color value from alarm notification overlay.
	 *
	 * @return array
	 */
	protected function getAlarmColors() {
		$notification_color_class = ['disaster-bg', 'high-bg', 'average-bg', 'warning-bg', 'info-bg', 'na-bg'];

		// Find appeared Alarm notification overlay dialog.
		$alarm_dialog = $this->getAlarmOverlay();

		// Get alarm color codes.
		$alarm_colors = [];
		foreach ($notification_color_class as $color_class) {
			$bg_color = $alarm_dialog->query('class', $color_class)->one()->getCSSValue('background-color');
			$alarm_colors[] = $bg_color;
		}

		return $alarm_colors;
	}

	protected function getAlarmOverlay() {
		return $this->query('xpath://div['.CXPathHelper::fromClass('overlay-dialogue notif').']')->waitUntilVisible()->one();
	}

	/**
	 * Acknowledge and close triggered problem.
	 */
	protected function closeAndAcknowledgeEvents() {
		CDataHelper::call('event.acknowledge', [
			'eventids' => self::$eventids,
			'action' => 3
		]);
	}

	/**
	 * Open problem page with filter reset.
	 */
	protected function openResetedPage() {
		$this->page->login()->open('zabbix.php?action=problem.view&filter_reset=1')->waitUntilReady();
	}
}