<?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';
require_once dirname(__FILE__).'/../../include/helpers/CDataHelper.php';

/**
 * Test for checking Proxy groups page.
 *
 * @dataSource Proxies
 *
 * @backup proxy_group
 */
class testPageAdministrationProxyGroups extends CWebTest {

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

	public function testPageAdministrationProxyGroups_Layout() {
		$this->page->login()->open('zabbix.php?action=proxygroup.list')->waitUntilReady();
		$this->page->assertTitle('Configuration of proxy groups');
		$this->page->assertHeader('Proxy groups');

		$this->assertTrue($this->query('button:Create proxy group')->one()->isClickable());
		$filter_form = $this->query('name:zbx_filter')->waitUntilPresent()->asForm()->one();

		// Check default fields and values.
		$filter_fields = [
			'Name' => [
				'default' => '',
				'maxlength' => 255
			],
			'State' => [
				'default' => 'Any',
				'options' => ['Any', 'Online', 'Degrading', 'Offline', 'Recovering']
			]
		];

		foreach ($filter_fields as $field_name => $field_params) {
			$filter_field = $filter_form->getField($field_name);
			$this->assertEquals($field_params['default'], $filter_field->getValue());

			if (array_key_exists('maxlength', $field_params)) {
				$this->assertEquals($field_params['maxlength'], $filter_field->getAttribute('maxlength'));
			}

			if (array_key_exists('options', $field_params)) {
				$this->assertEquals($field_params['options'], $filter_field->getLabels()->asText());
			}
		}

		$this->assertEquals(['Apply', 'Reset'], $filter_form->query('tag:button')->all()
				->filter(new CElementFilter(CElementFilter::CLICKABLE))->asText()
		);

		// Check filter collapse/expand.
		$filter_tab = $filter_form->query('xpath://div[contains(@class, "ui-tabs-panel")]')->one();
		$filter_button = $this->query('xpath://a[contains(@class, "filter-trigger")]')->one();

		foreach ([true, false] as $visible) {
			$this->assertEquals($visible, $filter_tab->isDisplayed());
			$filter_button->click();
		}

		$table = $this->query('class:list-table')->asTable()->one()->waitUntilPresent();
		$this->assertEquals(['', 'Name', 'State', 'Failover period', 'Online proxies', 'Minimum proxies', 'Proxies'],
				$table->getHeadersText()
		);

		// Column Proxies occupies 2 columns, which is why the proxy list needs to be addressed via index (7).
		$proxy_groups = [
			[
				'Name' => '2nd Online proxy group',
				'State' => 'Online',
				'Failover period' => '666',
				'Online proxies' => '1',
				'Minimum proxies' => '666',
				'Proxies' => '2',
				7 => 'active_proxy3, active_proxy5'
			],
			[
				'Name' => 'Default values - recovering',
				'State' => 'Recovering',
				'Failover period' => '1m',
				'Online proxies' => '0',
				'Minimum proxies' => '1',
				'Proxies' => '1',
				7 => 'passive_proxy7'
			],
			[
				'Name' => 'Degrading proxy group',
				'State' => 'Degrading',
				'Failover period' => '15m',
				'Online proxies' => '0',
				'Minimum proxies' => '100',
				'Proxies' => '3',
				7 => 'Passive proxy 1, passive_proxy1, passive_unsupported'
			],
			[
				'Name' => 'Delete me 1',
				'State' => '',
				'Failover period' => '10',
				'Online proxies' => '0',
				'Minimum proxies' => '1',
				'Proxies' => '',
				7 => ''
			],
			[
				'Name' => 'Delete me 2',
				'State' => '',
				'Failover period' => '10',
				'Online proxies' => '0',
				'Minimum proxies' => '1',
				'Proxies' => '',
				7 => ''
			],
			[
				'Name' => 'Group without proxies',
				'State' => '',
				'Failover period' => '899',
				'Online proxies' => '0',
				'Minimum proxies' => '999',
				'Proxies' => '',
				7 => ''
			],
			[
				'Name' => 'Group without proxies with linked host',
				'State' => '',
				'Failover period' => '10',
				'Online proxies' => '0',
				'Minimum proxies' => '1',
				'Proxies' => '',
				7 => ''
			],
			[
				'Name' => 'Offline group',
				'State' => 'Offline',
				'Failover period' => '900s',
				'Online proxies' => '0',
				'Minimum proxies' => '1',
				'Proxies' => '1',
				7 => 'active_proxy7'
			],
			[
				'Name' => 'Online proxy group',
				'State' => 'Online',
				'Failover period' => '10',
				'Online proxies' => '3',
				'Minimum proxies' => '1',
				'Proxies' => '6',
				7 => 'Active proxy 1, Active proxy 2, Active proxy 3, Active proxy to delete, Proxy_1 for filter,'.
						' Proxy_2 for filter'
			],
			[
				'Name' => '⭐️😀⭐Smiley प्रॉक्सी 团体⭐️😀⭐ - unknown',
				'State' => 'Unknown',
				'Failover period' => '123s',
				'Online proxies' => '0',
				'Minimum proxies' => '123',
				'Proxies' => '1',
				7 => 'passive_outdated'
			]
		];

		$this->assertTableData($proxy_groups);

		$state_classes = [
			'Online' => 'status-green',
			'Offline' => 'status-red',
			'Recovering' => 'status-yellow',
			'Degrading' => 'status-yellow',
			'Unknown' => 'status-grey'
		];
		foreach($state_classes as $state_value => $state_class) {
			$this->assertTrue($table->findRow('State', $state_value)->query('xpath:.//span[text()='.
					CXPathHelper::escapeQuotes($state_value).']')->one()->hasClass($state_class)
			);
		}

		$group_count = count($proxy_groups);
		$this->assertTableStats($group_count);
		$action_buttons = $this->query('xpath://div[@id="action_buttons"]/button')->all();
		$this->assertEquals(['Delete'], $action_buttons->asText());

		$all_checkbox = $this->query('id:all_proxy_groups')->asCheckbox()->one();

		foreach ([true => $group_count, false => 0] as $enable => $selected_count) {
			$all_checkbox->fill($enable);
			$this->assertSelectedCount($selected_count);
			$this->assertEquals($enable, $action_buttons->first()->isClickable());
		}
	}

	public function getProxyGroupFilterData() {
		return [
			// #0 Complete "Name" match.
			[
				[
					'filter' => [
						'Name' => 'Offline group'
					],
					'result' => [
						'Offline group'
					]
				]
			],
			// #1 Partial "Name" match.
			[
				[
					'filter' => [
						'Name' => 'proxy'
					],
					'result' => [
						'2nd Online proxy group',
						'Degrading proxy group',
						'Online proxy group'
					]
				]
			],
			// #2 Search string with space in between.
			[
				[
					'filter' => [
						'Name' => 'e p'
					],
					'result' => [
						'2nd Online proxy group',
						'Online proxy group'
					]
				]
			],
			// #3 Search string with space in the beginning and in the end.
			[
				[
					'filter' => [
						'Name' => ' Online '
					],
					'result' => [
						'2nd Online proxy group'
					]
				]
			],
			// #4 Search string with utf8mb4 smiley.
			[
				[
					'filter' => [
						'Name' => '😀⭐'
					],
					'result' => [
						'⭐️😀⭐Smiley प्रॉक्सी 团体⭐️😀⭐ - unknown'
					]
				]
			],
			// #5 search should be case insensitive.
			[
				[
					'filter' => [
						'Name' => 'GrOuP'
					],
					'result' => [
						'2nd Online proxy group',
						'Degrading proxy group',
						'Group without proxies',
						'Group without proxies with linked host',
						'Offline group',
						'Online proxy group'
					]
				]
			],
			// #6 Return online proxy groups.
			[
				[
					'filter' => [
						'State' => 'Online'
					],
					'result' => [
						'2nd Online proxy group',
						'Online proxy group'
					]
				]
			],
			// #7 Return degrading proxy group.
			[
				[
					'filter' => [
						'State' => 'Degrading'
					],
					'result' => [
						'Degrading proxy group'
					]
				]
			],
			// #8 Return offline proxy group.
			[
				[
					'filter' => [
						'State' => 'Offline'
					],
					'result' => [
						'Offline group'
					]
				]
			],
			// #9 Return recovering proxy group.
			[
				[
					'filter' => [
						'State' => 'Recovering'
					],
					'result' => [
						'Default values - recovering'
					]
				]
			],
			// #10 Filter both by name and state.
			[
				[
					'filter' => [
						'Name' => 'group',
						'State' => 'Online'
					],
					'result' => [
						'2nd Online proxy group',
						'Online proxy group'
					]
				]
			],
			// #11 No matches for the specified filter.
			[
				[
					'filter' => [
						'Name' => 'Online',
						'State' => 'Degrading'
					],
					'result' => []
				]
			]
		];
	}

	/**
	 * @dataProvider getProxyGroupFilterData
	 */
	public function testPageAdministrationProxyGroups_Filter($data) {
		$this->page->login()->open('zabbix.php?action=proxygroup.list')->waitUntilReady();
		$form = $this->query('name:zbx_filter')->waitUntilPresent()->asForm()->one();

		// Reset filter in case if some filtering remained before ongoing test case.
		$form->query('button:Reset')->one()->click();

		// Fill filter form with data.
		$form->fill($data['filter']);
		$form->submit();
		$this->page->waitUntilReady();

		// Check filtered result.
		$this->assertTableDataColumn($data['result']);
		$this->assertTableStats(count($data['result']));
	}

	public function testPageAdministrationProxyGroups_Sort() {
		$this->page->login()->open('zabbix.php?action=proxygroup.list')->waitUntilReady();

		// Reset filter to avoid impact from the filter scenario on this test.
		$this->query('button:Reset')->one()->click();

		$table = $this->query('class:list-table')->asTable()->one()->waitUntilPresent();
		$names = $this->getTableColumnData('Name');

		// Sort names ascendingly.
		usort($names, function($a, $b) {
			return strcasecmp($a, $b);
		});
		$names_asc = $names;

		// Sort names descendingly.
		usort($names, function($a, $b) {
			return strcasecmp($b, $a);
		});
		$names_desc = $names;

		// Check ascending and descending sorting of proxy group names.
		foreach ([$names_desc, $names_asc] as $sorted_names) {
			$table->query('link:Name')->waitUntilClickable()->one()->click();
			$table->waitUntilReloaded();
			$this->assertTableDataColumn($sorted_names);
		}
	}

	public static function getDeleteData() {
		return [
			// Attempt to delete a proxy group that has proxies assigned.
			[
				[
					'expected' => TEST_BAD,
					'groups' => ['Default values - recovering'],
					'error' => 'Proxy group "Default values - recovering" is used by proxy "passive_proxy7".'
				]
			],
			// Attempt to delete a proxy group that has no proxies but has an assigned host.
			[
				[
					'expected' => TEST_BAD,
					'groups' => ['Group without proxies with linked host'],
					'error' => 'Host "Host linked to proxy group" is monitored by proxy group "Group without proxies'.
							' with linked host".'
				]
			],
			// Attempt to delete two proxy groups one of which has a linked proxy.
			[
				[
					'expected' => TEST_BAD,
					'groups' => ['Offline group', 'Group without proxies'],
					'error' => 'Proxy group "Offline group" is used by proxy "active_proxy7".'
				]
			],
			// Delete a proxy group that has nothing linked to it.
			[
				[
					'groups' => ['Group without proxies']
				]
			],
			// Delete multiple proxy groups that have nothing linked to them.
			[
				[
					'groups' => ['Delete me 1', 'Delete me 2']
				]
			]
		];
	}

	/**
	 * @dataProvider getDeleteData
	 */
	public function testPageAdministrationProxyGroups_Delete($data) {
		$sql = 'SELECT * FROM proxy_group';
		$multiple_groups = count($data['groups']) > 1;

		if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) {
			$old_hash = CDBHelper::getHash($sql);
		}

		$this->page->login()->open('zabbix.php?action=proxygroup.list')->waitUntilReady();
		$this->query('class:list-table')->asTable()->one()->findRows('Name', $data['groups'])->select();
		$this->query('button:Delete')->waitUntilClickable()->one()->click();

		$this->assertTrue($this->page->isAlertPresent());
		$expected_alert = ($multiple_groups)
			? 'Delete selected proxy groups?'
			: 'Delete selected proxy group?';
		$this->assertEquals($expected_alert, $this->page->getAlertText());
		$this->page->acceptAlert();

		if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) {
			$title = ($multiple_groups)
				? 'Cannot delete proxy groups'
				: 'Cannot delete proxy group';
			$this->assertMessage(TEST_BAD, $title, $data['error']);
			$this->assertEquals($old_hash, CDBHelper::getHash($sql));

			foreach ($data['groups'] as $proxy_group) {
				$this->assertTrue($this->query('link', $proxy_group)->exists());
			}
		}
		else {
			$title = ($multiple_groups)
				? 'Proxy groups deleted'
				: 'Proxy group deleted';
			$this->assertMessage(TEST_GOOD, $title);

			foreach ($data['groups'] as $deleted_group) {
				$this->assertEquals(0, CDBHelper::getCount('SELECT NULL FROM proxy_group WHERE name='
						.zbx_dbstr($deleted_group))
				);
				$this->assertFalse($this->query('link', $deleted_group)->exists());
			}
		}
	}
}