<?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 dirname(__FILE__).'/../include/CWebTest.php';
require_once dirname(__FILE__).'/behaviors/CMessageBehavior.php';
require_once dirname(__FILE__).'/behaviors/CTableBehavior.php';

/**
 * @backup module, widget
 */

class testPageAdministrationGeneralModules extends CWebTest {

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

	private static $dashboardid;
	private static $template_dashboardid;
	private static $hostid;

	const TEMPLATEID = 50000;
	const ITEMID = 400410;
	const INACCESSIBLE_TEXT = 'No permissions to referred object or it does not exist!';
	const INACCESSIBLE_XPATH = 'xpath:.//div[contains(@class, "dashboard-widget-inaccessible")]';
	const HOSTNAME = 'Host for widget module test';

	private static $widget_descriptions = [
		'Action log' => 'Displays records about executed action operations (notifications, remote commands).',
		'Clock' => 'Displays local, server, or specified host time.',
		'Data overview' => 'Displays the latest item data and current status of each item for selected hosts.',
		'Discovery status' => 'Displays the status summary of the active network discovery rules.',
		'Favorite graphs' => 'Displays shortcuts to the most needed graphs (marked as favorite).',
		'Favorite maps' => 'Displays shortcuts to the most needed network maps (marked as favorite).',
		'Gauge' => 'Displays the value of a single item as gauge.',
		'Geomap' => 'Displays hosts as markers on a geographical map.',
		'Graph' => 'Displays data of up to 50 items as line, points, staircase, or bar charts.',
		'Graph (classic)' => 'Displays a single custom graph or a simple graph.',
		'Graph prototype' => 'Displays a grid of graphs created by low-level discovery from either a graph prototype or '.
				'an item prototype.',
		'Host availability' => 'Displays the host count by status (available/unavailable/unknown).',
		'Item value' => 'Displays the value of a single item prominently.',
		'Map' => 'Displays either a single configured network map or one of the configured network maps in the map '.
				'navigation tree.',
		'Map navigation tree' => 'Allows to build a hierarchy of existing maps and display problem statistics for each '.
				'included map and map group.',
		'Pie chart' => 'Displays item values as a pie or doughnut chart.',
		'Plain text' => 'Displays the latest data for the selected items in plain text.',
		'Problem hosts' => 'Displays the problem count by host group and the highest problem severity within a group.',
		'Problems' => 'Displays currently open problems with quick access links to the problem details.',
		'Problems by severity' => 'Displays the problem count by severity.',
		'SLA report' => 'Displays SLA reports.',
		'System information' => 'Displays the current status and system statistics of the Zabbix server and its '.
				'associated components.',
		'Top hosts' => 'Displays top N hosts that have the highest or the lowest item value (for example, CPU load) '.
				'with an option to add progress-bar visualizations and customize report columns.',
		'Top triggers' => 'Displays top N triggers that have the most problems within the period of evaluation,'.
				' sorted by the number of problems.',
		'Trigger overview' => 'Displays trigger states for selected hosts.',
		'URL' => 'Displays the content retrieved from the specified URL.',
		'Web monitoring' => 'Displays the status summary of the active web monitoring scenarios.'
	];

	/**
	 * Creates dashboards with widgets and defines the corresponding dashboard IDs.
	 */
	public static function prepareDashboardData() {
		$response = CDataHelper::call('dashboard.create', [
			[
				'name' => 'Dashboard for widget module testing',
				'private' => 0,
				'auto_start' => 0,
				'pages' => [
					[
						'name' => 'Map page',
						'widgets' => [
							[
								'type' => 'navtree',
								// TODO: Uncomment the below line when ZBX-22245 will be resolved.
//								'name' => 'Awesome map tree',
								'x' => 0,
								'y' => 0,
								'width' => 12,
								'height' => 4,
								'view_mode' => 0,
								'fields' => [
									[
										'type' => 1,
										'name' => 'reference',
										'value' => 'GZCSV'
									],
									[
										'type' => 1,
										'name' => 'navtree.1.name',
										'value' => 'Awesome map'
									],
									[
										'type' => 8,
										'name' => 'navtree.1.sysmapid',
										'value' => 1
									]
								]
							],
							[
								'type' => 'map',
								'x' => 12,
								'y' => 0,
								'width' => 12,
								'height' => 4,
								'view_mode' => 0,
								'fields' => [
									[
										'type' => 1,
										'name' => 'sysmapid._reference',
										'value' => 'GZCSV._mapid'
									]
								]
							],
							[
								'type' => 'favgraphs',
								'view_mode' => 0,
								'x' => 6,
								'y' => 4,
								'width' => 6,
								'height' => 4
							]
						]
					],
					[
						'name' => 'Alarm clock page',
						'widgets' => [
							[
								'type' => 'clock345',
								'view_mode' => 0,
								'x' => 0,
								'y' => 0,
								'width' => 6,
								'height' => 4
							],
							[
								'type' => 'favgraphs',
								'view_mode' => 0,
								'x' => 6,
								'y' => 0,
								'width' => 6,
								'height' => 4
							]
						]
					],
					[
						'name' => 'System info page',
						'widgets' => [
							[

								'type' => 'favgraphs',
								'view_mode' => 0,
								'x' => 0,
								'y' => 0,
								'width' => 6,
								'height' => 4
							],
							[
								'type' => 'systeminfo',
								'view_mode' => 0,
								'x' => 6,
								'y' => 0,
								'width' => 6,
								'height' => 4
							]
						]
					],
					[
						'name' => 'Empty widget page',
						'widgets' => [
							[
								'type' => 'favgraphs',
								'view_mode' => 0,
								'x' => 0,
								'y' => 0,
								'width' => 6,
								'height' => 4
							],
							[
								'type' => 'emptyWidget',
								'view_mode' => 0,
								'x' => 6,
								'y' => 0,
								'width' => 6,
								'height' => 4
							]
						]
					]
				]
			]
		]);

		self::$dashboardid = $response['dashboardids'][0];

		$template_responce = CDataHelper::call('templatedashboard.create', [
			[
				'templateid' => self::TEMPLATEID,
				'name' => 'Templated dashboard for module widgets',
				'auto_start' => 0,
				'pages' => [
					[
						'name' => 'Default clock page',
						'widgets' => [
							[
								'type' => 'clock',
								// TODO: Uncomment the below line when ZBX-22245 will be resolved.
//								'name' => 'Default clock',
								'width' => 6,
								'height' => 4
							],
							[
								'type' => 'item',
								'x' => 6,
								'y' => 0,
								'width' => 6,
								'height' => 4,
								'fields' => [
									[
										'type' => 0,
										'name' => 'itemid',
										'value' => self::ITEMID
									]
								]
							]
						]
					],
					[
						'name' => 'Alarm clock page',
						'widgets' => [
							[
								'type' => 'clock',
								'name' => 'Clock widget',
								'width' => 6,
								'height' => 4
							],
							[
								'type' => 'clock345',
								'view_mode' => 0,
								'x' => 6,
								'y' => 0,
								'width' => 6,
								'height' => 4,
								'fields' => [
									[
										'type' => 0,
										'name' => 'time_type',
										'value' => 1
									],
									[
										'type' => 1,
										'name' => 'tzone_timezone',
										'value' => 'local'
									]
								]
							]
						]
					]
				]
			]
		]);

		self::$template_dashboardid = $template_responce['dashboardids'][0];

		$host_responce = CDataHelper::createHosts([
			[
				'host' => self::HOSTNAME,
				'interfaces' => [
					'type' => INTERFACE_TYPE_AGENT,
					'main' => 1,
					'useip' => 1,
					'ip' => '127.0.0.1',
					'dns' => '',
					'port' => '10050'
				],
				'groups' => [
					'groupid' => 7
				],
				'status' => HOST_STATUS_MONITORED,
				'templates' => [
					'templateid' => self::TEMPLATEID
				]
			]
		]);

		self::$hostid = $host_responce['hostids'][self::HOSTNAME];
	}

	public function testPageAdministrationGeneralModules_Layout() {
		$modules = [
			[
				'Name' => '1st Module name',
				'Version' => '1',
				'Author' => '1st Module author',
				'Description' => '1st Module description',
				'Status' => 'Disabled'
			],
			[
				'Name' => '2nd Module name !@#$%^&*()_+',
				'Version' => 'two !@#$%^&*()_+',
				'Author' => '2nd Module author !@#$%^&*()_+',
				'Description' => 'Module description !@#$%^&*()_+',
				'Status' => 'Disabled'
			],
			[
				'Name' => '4th Module',
				'Version' => '',
				'Author' => '',
				'Description' => '',
				'Status' => 'Disabled'
			],
			[
				'Name' => '5th Module',
				'Version' => '',
				'Author' => '',
				'Description' => 'Adding top-level and sub-level menu',
				'Status' => 'Disabled'
			],
			[
				'Name' => 'Clock2',
				'Version' => '1.1',
				'Author' => 'Zabbix QA department',
				'Description' => '',
				'Status' => 'Disabled'
			],
			[
				'Name' => 'Empty widget',
				'Version' => '1.0',
				'Author' => 'Some Zabbix employee',
				'Description' => '',
				'Status' => 'Disabled'
			],
			[
				'Name' => 'шестой модуль',
				'Version' => 'бета 2',
				'Author' => 'Работник Заббикса',
				'Description' => 'Удалить "Reports" из меню верхнего уровня, а так же удалить "Maps" из секции "Monitoring".',
				'Status' => 'Disabled'
			]
		];

		// Create an array with widget modules that should be present by default.
		$widget_modules = [];
		$i = 0;

		foreach (self::$widget_descriptions as $name => $description) {
			$widget_modules[$i]['Name'] = $name;
			$widget_modules[$i]['Version'] = '1.0';
			$widget_modules[$i]['Author'] = 'Zabbix';
			$widget_modules[$i]['Description'] = $description;
			$widget_modules[$i]['Status'] = 'Enabled';

			$i++;
		}

		// Open modules page and check header.
		$this->page->login()->open('zabbix.php?action=module.list');
		$this->assertEquals('Modules', $this->query('tag:h1')->one()->getText());

		// Check status of buttons on the modules page.
		foreach (['Scan directory' => true, 'Enable' => false, 'Disable' => false] as $button => $enabled) {
			$this->assertTrue($this->query('button', $button)->one()->isEnabled($enabled));
		}

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

		// Check that only widget modules are present until the 'Scan directory' button is pressed.
		$this->assertTableData($widget_modules);

		$count = $table->getRows()->count();
		$this->assertTableStats($count);

		$this->assertEquals('0 selected', $this->query('id:selected_count')->one()->getText());
		// Check modules table headers.
		$headers = $table->getHeadersText();
		// Remove empty element from headers array.
		array_shift($headers);
		$this->assertSame(['Name', 'Version', 'Author', 'Description', 'Status'], $headers);

		// Load modules.
		$this->loadModules();
		$all_modules = array_merge($widget_modules, $modules);
		$total_count = count($all_modules);

		// Sort column contents ascending.
		usort($all_modules, function($a, $b) {
			return strcmp($a['Name'], $b['Name']);
		});

		// Check parameters of modules in the modules table.
		$this->assertTableData($all_modules);

		$count = CDBHelper::getCount('SELECT moduleid FROM module');
		$this->assertEquals('Displaying '.$total_count.' of '.$total_count.' found', $this->query('class:table-stats')
				->one()->getText()
		);

		// Load modules again and check that no new modules were added.
		$this->loadModules(false);
		$this->assertEquals('Displaying '.$count.' of '.$count.' found', $this->query('class:table-stats')->one()->getText());
	}

	public function getModuleDetails() {
		return [
			// Module 1.
			[
				[
					'Name' => '1st Module name',
					'Version' => '1',
					'Author' => '1st Module author',
					'Description' => '1st Module description',
					'Directory' => 'modules/module_number_1',
					'Namespace' => 'Modules\Example_A',
					'URL' => '1st module URL',
					'Enabled' => false
				]
			],
			// Module 2.
			[
				[
					'Name' => '2nd Module name !@#$%^&*()_+',
					'Version' => 'two !@#$%^&*()_+',
					'Author' => '2nd Module author !@#$%^&*()_+',
					'Description' => 'Module description !@#$%^&*()_+',
					'Directory' => 'modules/module_number_2',
					'Namespace' => 'Modules\Example_B',
					'URL' => '!@#$%^&*()_+',
					'Enabled' => false
				]
			],
			// Module 4.
			[
				[
					'Name' => '4th Module',
					'Version' => '',
					'Author' => '-',
					'Description' => '-',
					'Directory' => 'modules/module_number_4',
					'Namespace' => 'Modules\Example_A',
					'URL' => '-',
					'Enabled' => false
				]
			],
			// Module 5.
			[
				[
					'Name' => '5th Module',
					'Version' => '',
					'Author' => '-',
					'Description' => 'Adding top-level and sub-level menu',
					'Directory' => 'modules/module_number_5',
					'Namespace' => 'Modules\Example_E',
					'URL' => '-',
					'Enabled' => false
				]
			],
			// Clock2.
			[
				[
					'Name' => 'Clock2',
					'Version' => '1.1',
					'Author' => 'Zabbix QA department',
					'Description' => '-',
					'Directory' => 'modules/clock32',
					'Namespace' => 'Modules\Clock2',
					'URL' => '-',
					'Enabled' => false
				]
			],
			// Empty widget.
			[
				[
					'Name' => 'Empty widget',
					'Version' => '1.0',
					'Author' => 'Some Zabbix employee',
					'Description' => '-',
					'Directory' => 'modules/emptyWidget',
					'Namespace' => 'Modules\emptyWidget',
					'URL' => '-',
					'Enabled' => false
				]
			],
			// Module 6.
			[
				[
					'Name' => 'шестой модуль',
					'Version' => 'бета 2',
					'Author' => 'Работник Заббикса',
					'Description' => 'Удалить "Reports" из меню верхнего уровня, а так же удалить "Maps" из секции "Monitoring".',
					'Directory' => 'modules/module_number_6',
					'Namespace' => 'Modules\Example_F',
					'URL' => '-',
					'Enabled' => false
				]
			]
		];
	}

	/**
	 * @dataProvider getModuleDetails
	 * @depends testPageAdministrationGeneralModules_Layout
	 */
	public function testPageAdministrationGeneralModules_Details($data) {
		// Open corresponding module from the modules table.
		$this->page->login()->open('zabbix.php?action=module.list');
		$this->query('link', $data['Name'])->waitUntilVisible()->one()->click();
		$dialog = COverlayDialogElement::find()->one()->waitUntilReady();
		$form = $dialog->asForm();
		// Check value af every field in Module details form.
		foreach ($data as $key => $value) {
			$this->assertEquals($value, $form->getFieldContainer($key)->getText());
		}

		$dialog->close();
	}

	public function getModuleData() {
		return [
			// Enable only 1st module - '1st Module' entry added under Monitoring.
			[
				[
					[
						'module_name' => '1st Module name',
						'menu_entries' => [
							[
								'name' => '1st Module',
								'action' => 'first.module',
								'message' => 'If You see this message - 1st module is working'
							]
						]
					]
				]
			],
			// Enable only 2nd Module - '2nd Module' entry added under Monitoring.
			[
				[
					[
						'module_name' => '2nd Module name !@#$%^&*()_+',
						'menu_entries' => [
							[
								'name' => '2nd Module',
								'action' => 'second.module',
								'message' => '2nd module is also working'
							]
						]
					]
				]
			],
			// Enable both 1st and 2nd module - '1st Module' and '2nd Module' entries added under Monitoring.
			[
				[
					[
						'module_name' => '1st Module name',
						'menu_entries' => [
							[
								'name' => '1st Module',
								'action' => 'first.module',
								'message' => 'If You see this message - 1st module is working'
							]
						]
					],
					[
						'module_name' => '2nd Module name !@#$%^&*()_+',
						'menu_entries' => [
							[
								'name' => '2nd Module',
								'action' => 'second.module',
								'message' => '2nd module is also working'
							]
						]
					]
				]
			],
			// Attempting to enable two modules that use identical namespace.
			[
				[
					[
						'module_name' => '1st Module name',
						'menu_entries' => [
							[
								'name' => '1st Module',
								'action' => 'first.module',
								'message' => 'If You see this message - 1st module is working'
							]
						]
					],
					[
						'expected' => TEST_BAD,
						'module_name' =>'4th Module',
						'menu_entries' => [
							[
								'name' => '4th Module',
								'action' => 'forth.module'
							]
						],
						'error_details' => 'Identical namespace (Modules\Example_A) is used by modules located at '.
								'modules/module_number_1, modules/module_number_4.'
					]
				]
			],
			// Enable 5th Module - Module 5 menu top level menu is added with 3 entries.
			[
				[
					[
						'module_name' => '5th Module',
						'top_menu_entry' => 'Module 5 menu',
						'menu_entries' => [
							[
								'name' => 'Your profile',
								'action' => 'userprofile.edit',
								'message' => 'User profile: Zabbix Administrator',
								'check_disabled' => false
							],
							[
								'name' => 'пятый модуль',
								'action' => 'fifth.module',
								'message' => 'Если ты это читаешь то 5ый модуль работает'
							],
							[
								'name' => 'Module list',
								'action' => 'module.list',
								'message' => 'Modules',
								'check_disabled' => false
							]
						]
					]
				]
			],
			// Enable шестой модуль - Top level menu Reports and menu entry Maps are removed.
			[
				[
					[
						'module_name' => 'шестой модуль',
						'remove' => true,
						'top_menu_entry' => 'Reports',
						'menu_entry' => 'Maps'
					]
				]
			]
		];
	}

	/**
	 * @backupOnce module
	 * @dataProvider getModuleData
	 * @depends testPageAdministrationGeneralModules_Layout
	 */
	public function testPageAdministrationGeneralModules_EnableDisable($data) {
		$this->page->login()->open('zabbix.php?action=module.list');

		foreach (['list', 'form'] as $view) {
			// This block is separate because one of the cases requires one module to be enabled before the other to succeed.
			foreach ($data as $module) {
				// Enable module and check the success or error message.
				$this->enableModule($module, $view);
			}

			// In case if module should be enabled, check that changes took place and then disable each enabled module.
			foreach ($data as $module) {
				if (CTestArrayHelper::get($module, 'expected', TEST_GOOD) === TEST_GOOD) {
					$this->assertModuleEnabled($module);
					$this->disableModule($module, $view);
					$this->assertModuleDisabled($module);
				}
			}
		}
	}

	public function getFilterData() {
		return [
			// Exact name match.
			[
				[
					'filter' => [
						'Name' => '1st Module name'
					],
					'expected' => [
						'1st Module name'
					]
				]
			],
			// Partial name match for all 3 modules.
			[
				[
					'filter' => [
						'Name' => 'Module'
					],
					'expected' => [
						'1st Module name',
						'2nd Module name !@#$%^&*()_+',
						'4th Module',
						'5th Module'
					]
				]
			],
			// Partial name match with space in between.
			[
				[
					'filter' => [
						'Name' => 'le n'
					],
					'expected' => [
						'1st Module name',
						'2nd Module name !@#$%^&*()_+'
					]
				]
			],
			// Filter by various characters in name.
			[
				[
					'filter' => [
						'Name' => '!@#$%^&*()_+'
					],
					'expected' => [
						'2nd Module name !@#$%^&*()_+'
					]
				]
			],
			// Exact name match with leading and trailing spaces.
			[
				[
					'filter' => [
						'Name' => '  4th Module  '
					],
					'expected' => [
						'4th Module'
					]
				]
			],
			// Retrieve only Enabled modules.
			[
				[
					'filter' => [
						'Status' => 'Enabled'
					],
					'expected' => array_merge(['2nd Module name !@#$%^&*()_+'], array_keys(self::$widget_descriptions))
				]
			],
			// Retrieve only Disabled modules.
			[
				[
					'filter' => [
						'Status' => 'Disabled'
					],
					'expected' => [
						'1st Module name',
						'4th Module',
						'5th Module',
						'Clock2',
						'Empty widget',
						'шестой модуль'
					]
				]
			],
			// Retrieve only Disabled modules that have 'name' string in their name.
			[
				[
					'filter' => [
						'Name' => 'name',
						'Status' => 'Disabled'
					],
					'expected' => [
						'1st Module name'
					]
				]
			]
		];
	}

	/**
	 * @dataProvider getFilterData
	 * @depends testPageAdministrationGeneralModules_Layout
	 */
	public function testPageAdministrationGeneralModules_Filter($data) {
		$this->page->login()->open('zabbix.php?action=module.list');

		// Before checking the filter one of the modules needs to be enabled.
		$table = $this->query('class:list-table')->asTable()->one();
		$row = $table->findRow('Name', '2nd Module name !@#$%^&*()_+');
		if ($row->getColumn('Status')->getText() !== 'Enabled') {
			$row->query('link:Disabled')->one()->click();
		}

		// Apply and submit the filter from data provider.
		$form = $this->query('name:zbx_filter')->asForm()->one();
		$form->fill($data['filter']);
		$form->submit();
		$this->page->waitUntilReady();
		// Check (using module name) that only the expected filters are returned in the list.
		$this->assertTableDataColumn(CTestArrayHelper::get($data, 'expected'));
		// Reset the filter and check that all loaded modules are displayed.
		$this->query('button:Reset')->one()->click();
		$count = CDBHelper::getCount('SELECT moduleid FROM module');
		$this->assertEquals('Displaying '.$count.' of '.$count.' found', $this->query('class:table-stats')->one()->getText());
	}

	/**
	 * @depends testPageAdministrationGeneralModules_Layout
	 */
	public function testPageAdministrationGeneralModules_SimpleUpdate() {
		$sql = 'SELECT * FROM module ORDER BY moduleid';
		$initial_hash = CDBHelper::getHash($sql);

		// Open one of the modules and update it without making any changes.
		$this->page->login()->open('zabbix.php?action=module.list');
		$this->query('link:1st Module name')->waitUntilVisible()->one()->click();
		$this->page->waitUntilReady();
		$this->query('button:Update')->one()->click();

		$this->assertMessage(TEST_GOOD, 'Module updated');
		// Check that Module has been updated and that there are no changes took place.
		$this->assertEquals($initial_hash, CDBHelper::getHash($sql));
	}

	/**
	 * @depends testPageAdministrationGeneralModules_Layout
	 */
	public function testPageAdministrationGeneralModules_Cancel() {
		$sql = 'SELECT * FROM module ORDER BY moduleid';
		$initial_hash = CDBHelper::getHash($sql);

		// Open the module update of which is going to be cancelled.
		$this->page->login()->open('zabbix.php?action=module.list');
		$this->query('link:1st Module name')->waitUntilVisible()->one()->click();
		$this->page->waitUntilReady();

		// Edit module status and Cancel the update.
		$this->query('id:status')->asCheckbox()->one()->check();
		$this->query('button:Cancel')->one()->click();
		$this->page->waitUntilReady();

		// Check that Module has been updated and that there are no changes took place.
		$this->assertEquals($initial_hash, CDBHelper::getHash($sql));
	}

	public function getWidgetModuleData() {
		return [
			// Custom widget with JS, css and pre-defined widget type name
			[
				[
					'module_name' => 'Clock2',
					'widget_name' => 'Local',
					'widget_type' => 'ALARM CLOCK',
					'page' => 'Alarm clock page',
					'refresh_rate' => '1 minute'
				]
			],
			// Existing default widget.
			[
				[
					'module_name' => 'System information',
					'widget_name' => 'System information',
					'page' => 'System info page'
				]
			],
			// Existing default widget on which another widget is dependent.
			[
				[
					'module_name' => 'Map navigation tree',
					// TODO: Uncomment the below line and delete the line after it when ZBX-22245 will be resolved.
//					'widget_name' => 'Awesome map tree',
					'widget_name' => 'Map navigation tree',
					'dependent_widget' => 'Map',
					'page' => 'Map page'
				]
			],
			// Custom widget with minimal contents.
			[
				[
					'module_name' => 'Empty widget',
					'widget_name' => 'Empty widget',
					'page' => 'Empty widget page',
					'refresh_rate' => '2 minutes'
				]
			],
			// Existing default widget on template dashboard.
			[
				[
					'module_name' => 'Clock',
					// TODO: Uncomment the below line and delete the line after it when ZBX-22245 will be resolved.
//					'widget_name' => 'Default clock',
					'widget_name' => 'Local',
					'template' => true,
					'page' => 'Default clock page'
				]
			],
			// Custom widget with minimal contents.
			[
				[
					'module_name' => 'Clock2',
					'widget_name' => 'Server',
					'widget_type' => 'ALARM CLOCK',
					'template' => true,
					'not_available' => 'Empty widget',
					'page' => 'Alarm clock page'
				]
			]
		];
	}

	/**
	 * @onBeforeOnce prepareDashboardData
	 *
	 * @depends testPageAdministrationGeneralModules_Layout
	 *
	 * @dataProvider getWidgetModuleData
	 */
	public function testPageAdministrationGeneralModules_ChangeWidgetModuleStatus($module) {
		$this->page->login()->open('zabbix.php?action=module.list');

		// Determine the original status of the modules to be checked. Scenarios with mixed statuses are not considered.
		$initial_status = $this->query('class:list-table')->asTable()->one()->findRow('Name', $module['module_name'])
				->getColumn('Status')->getText();

		if ($initial_status === 'Disabled') {
			$this->enableModule($module, 'list');
			$this->checkWidgetModuleStatus($module);
			$this->disableModule($module, 'list');
			$this->checkWidgetModuleStatus($module, 'disabled');
		}
		else {
			$this->disableModule($module, 'list');
			$this->checkWidgetModuleStatus($module, 'disabled');
			$this->enableModule($module, 'list');
			$this->checkWidgetModuleStatus($module);
		}
	}

	public function getWidgetDimensions() {
		return [
			// Widget with pre-defined dimensions.
			[
				[
					'module_name' => 'Clock2',
					'widget_name' => 'Local',
					'widget_type' => 'ALARM CLOCK',
					'enable' => true,
					'page' => 'Map page',
					'dimensions' => ['width: 33.3333%', 'height: 280px']
				]
			],
			// Widget with pre-defined dimensions on template.
			[
				[
					'module_name' => 'Clock2',
					'widget_name' => 'Local',
					'widget_type' => 'ALARM CLOCK',
					'page' => 'Alarm clock page',
					'template' => true,
					'dimensions' => ['width: 33.3333%', 'height: 280px']
				]
			],
			// Widget with default dimensions.
			[
				[
					'module_name' => 'Empty widget',
					'widget_name' => 'Empty widget',
					'widget_type' => 'Empty widget',
					'enable' => true,
					'page' => 'Map page',
					'dimensions' => ['width: 50%', 'height: 350px']
				]
			]
		];
	}

	/**
	 *
	 * @depends testPageAdministrationGeneralModules_ChangeWidgetModuleStatus
	 *
	 * @dataProvider getWidgetDimensions
	 */
	public function testPageAdministrationGeneralModules_CheckWidgetDimensions($data) {
		$this->page->login();

		if (array_key_exists('enable', $data)) {
			$this->page->open('zabbix.php?action=module.list');
			$this->enableModule($data, 'list');
		}

		$this->checkWidgetDimensions($data);

		// Cancel editing dashboard not to interfere with following cases from data provider.
		$this->query('link:Cancel')->one()->click();
	}

	/**
	 * Add a widget of a specific type to dashboard or template dashboard and check its default dimensions.
	 *
	 * @param array	$data	data provider.
	 */
	private function checkWidgetDimensions($data) {
		// Open required dashboard page in edit mode.
		$url = (array_key_exists('template', $data))
			? 'zabbix.php?action=template.dashboard.edit&dashboardid='.self::$template_dashboardid
			: 'zabbix.php?action=dashboard.view&dashboardid='.self::$dashboardid;
		$this->page->open($url)->waitUntilReady();

		$dashboard = CDashboardElement::find()->one()->waitUntilVisible();
		$dashboard->selectPage($data['page']);

		if (!array_key_exists('template', $data)) {
			$dashboard->edit();
		}

		// Add widget from the data provider.
		$widget_form = $dashboard->addWidget()->asForm();
		$widget_form->fill(['Type' => CFormElement::RELOADABLE_FILL($data['widget_type'])]);
		$widget_form->submit();

		// Get widget dimensions from the style attribute of the widget grid element and compare with expected values.
		$grid_selector = 'xpath:.//div[contains(@class, "dashboard-grid-widget-head")]/../..';
		$widget_dimensions = $dashboard->getWidget($data['widget_name'])->query($grid_selector)->one()->getAttribute('style');
		$dimension_array = array_map('trim', explode(';', $widget_dimensions));

		foreach ($data['dimensions'] as $dimension) {
			$this->assertContains($dimension, $dimension_array);
		}
	}

	/**
	 * @depends testPageAdministrationGeneralModules_ChangeWidgetModuleStatus
	 */
	public function testPageAdministrationGeneralModules_DisableAllModules() {
		$this->page->login()->open('zabbix.php?action=module.list')->waitUntilReady();

		// Disable all modules.
		$this->query('id:all_modules')->waitUntilPresent()->asCheckbox()->one()->set(true);
		$this->query('button:Disable')->waitUntilCLickable()->one()->click();
		$this->page->acceptAlert();

		// Wait for the Success message to confirm that modules were disabled before heading to the dashboard.
		$this->assertMessage(TEST_GOOD, 'Modules disabled');

		// Open dashboard and check that all widgets are inaccessible.
		$this->page->open('zabbix.php?action=dashboard.view&dashboardid='.self::$dashboardid)->waitUntilReady();
		$this->checkAllWidgetsDisabledOnPage();

		// Open template dashboard and check that all widgets are inaccessible.
		$this->page->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$template_dashboardid)->waitUntilReady();
		$this->checkAllWidgetsDisabledOnPage();

		// Open template dashboard on host and check that all widgets are inaccessible.
		$this->page->open('zabbix.php?action=host.dashboard.view&hostid='.self::$hostid.'&dashboardid='.self::$template_dashboardid)
				->waitUntilReady();
		$this->checkAllWidgetsDisabledOnPage();
	}

	/**
	 * Check that all widgets that are displayed on opened dashboard page are inaccessible widgets.
	 */
	private function checkAllWidgetsDisabledOnPage() {
		$dashboard = CDashboardElement::find()->one()->waitUntilPresent();
		$total_count = $dashboard->getWidgets()->count();
		$inaccessible_count = $dashboard->query(self::INACCESSIBLE_XPATH)->waitUntilVisible()->all()->count();
		$this->assertEquals($total_count, $inaccessible_count);
	}

	/**
	 * Check widgets of the enabled/disabled modules are displayed in dashboards, host dashboard and template dashboard views.
	 *
	 * @param array		$module		module related information from data provider.
	 * @param string	$status		status of widget module before execution of this function.
	 */
	private function checkWidgetModuleStatus($module, $status = 'enabled') {
		// Open dashboard or host dashboard and check widget display in this view.
		$url = array_key_exists('template', $module)
			? 'zabbix.php?action=host.dashboard.view&hostid='.self::$hostid.'&dashboardid='.self::$template_dashboardid
			: 'zabbix.php?action=dashboard.view&dashboardid='.self::$dashboardid;
		$this->page->open($url)->waitUntilReady();
		$dashboard = CDashboardElement::find()->one()->waitUntilVisible();
		$this->checkWidgetStatusOnDashboard($dashboard, $module, $status);

		// Open Kiosk mode and check widget display again.
		$this->checkWidgetStatusOnDashboard($dashboard, $module, $status, 'kiosk');
		$this->query('xpath://button[@title="Normal view"]')->one()->click();
		$this->page->waitUntilReady();

		// Open dashboard in edit mode or open dashboard on template and check widget display again.
		if (array_key_exists('template', $module)) {
			$this->page->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$template_dashboardid)
					->waitUntilReady();
		}
		else {
			$dashboard->edit();
		}

		$this->checkWidgetStatusOnDashboard($dashboard, $module, $status, 'edit');

		// Check that widget is present among widget types dropdown.
		$widget_dialog = $dashboard->addWidget();
		$widget_type = (array_key_exists('widget_type', $module) ? $module['widget_type'] : $module['module_name']);
		$options = $widget_dialog->asForm()->getField('Type')->asDropdown()->getOptions()->asText();

		// Check that widget type is present in "Type" dropdown only if corresponding module is enabled.
		$this->assertTrue(($status === 'enabled') ? in_array($widget_type, $options) : !in_array($widget_type, $options));

		// Check that module that should be present only on regular dashboards is not present (key used only on template).
		if (array_key_exists('not_available', $module)) {
			$this->assertFalse(in_array($module['not_available'], $options));
		}

		// Go back to the list of modules after the check is complete.
		$widget_dialog->close();
		$this->page->open('zabbix.php?action=module.list');
	}

	/**
	 * Check enabled or disabled widget display and its parameters on a particular dashboard page.
	 * Requirements to the widget are dependent on corresponding module status and dashboard mode (view, kiosk, edit modes).
	 *
	 * @param CDashboardElement		$dashboard	dashboard that contains the corresponding module widget.
	 * @param array					$module		module related information from data provider.
	 * @param string				$status		status of widget module before execution of this function.
	 * @param string				$mode		mode of the dashboard.
	 */
	private function checkWidgetStatusOnDashboard($dashboard, $module, $status, $mode = null) {
		$dashboard->selectPage($module['page']);

		// Switch to kiosk mode if required.
		if ($mode === 'kiosk') {
			$this->query('xpath://button[@title="Kiosk mode"]')->one()->click();
			$this->page->waitUntilReady();
		}

		if ($status === 'enabled') {
			// Check that widget with required name is shown and that is doesn't have the inaccessilbe widget string in it.
			$widget = $dashboard->getWidget($module['widget_name']);
			$this->assertFalse($widget->query("xpath:.//div[text()=".CXPathHelper::escapeQuotes(self::INACCESSIBLE_TEXT).
					"]")->one(false)->isValid()
			);

			// Check refresh interval if such specified in the data provider.
			if (array_key_exists('refresh_rate', $module) && $mode !== 'edit') {
				$this->assertEquals($module['refresh_rate'], $widget->getRefreshInterval());
				CPopupMenuElement::find()->one()->close();
			}

			// Check that dependent widget is there and that it's content is not hidden.
			if (array_key_exists('dependent_widget', $module)) {
				$dependent_widget = $dashboard->getWidget($module['dependent_widget']);
				$this->assertTrue($dependent_widget->isValid());
				$this->assertNotEquals(self::INACCESSIBLE_TEXT, $dependent_widget->getContent()->getText());
			}
		}
		else {
			// Check that there is only 1 inaccessible widget present on the opened dashboard page.
			$this->assertEquals(1, $dashboard->query(self::INACCESSIBLE_XPATH)->waitUntilVisible()->all()->count());

			// Get the inaccessible widget and check its contents.
			$inaccessible_widget = $dashboard->getWidget('Inaccessible widget');
			$this->assertEquals(self::INACCESSIBLE_TEXT, $inaccessible_widget->getContent()->getText());

			// Check that withget of the disabled module is not present on the dashboard.
			$this->assertFalse($dashboard->getWidget($module['widget_name'], false)->isValid());

			// Check that the dependent widget is still there, but its contents is not displayed.
			if (array_key_exists('dependent_widget', $module)) {
				$dependent_widget = $dashboard->getWidget($module['dependent_widget']);
				$this->assertTrue($dependent_widget->isValid());
				$this->assertEquals(self::INACCESSIBLE_TEXT, $dependent_widget->getContent()->getText());
			}

			/**
			 * Check that edit widget button on disabled module widget is hidden and that it doesn't exist
			 * if the dashboard is opened in Monitoring => Hosts view (where All hosts link is present) or in kiosk mode.
			 */
			$edit_button = $inaccessible_widget->query('xpath:.//button['.CXPathHelper::fromClass('js-widget-edit').']');
			$this->assertFalse(($mode === 'kiosk' || $this->query('link:All hosts')->one(false)->isValid())
					? $edit_button->one(false)->isValid()
					: $edit_button->one()->isDisplayed()
			);

			// It should not be possible only to Delete the widget and only when the dashboard is in edit mode.
			$button = $inaccessible_widget->query('xpath:.//button['.CXPathHelper::fromClass('js-widget-action').']')->one();

			if ($mode === 'edit') {
				$popup_menu = $button->waitUntilPresent()->asPopupButton()->getMenu();
				$menu_items = $popup_menu->getItems();
				$this->assertEquals(['Copy', 'Paste', 'Delete'], $menu_items->asText());

				// Check that inaccessible widgets can only be deleted.
				$this->assertEquals(['Delete'], array_values($menu_items->filter(CElementFilter::CLICKABLE)->asText()));
				$popup_menu->close();
			}
			else {
				$this->assertFalse($button->isVisible());
			}
		}
	}

	/**
	 * Function loads modules in frontend and checks the message depending on whether new modules were loaded.
	 *
	 * @param bool	$first_load		flag that determines whether modules are loaded for the first time.
	 */
	private function loadModules($first_load = true) {
		// Load modules
		$this->query('button:Scan directory')->waitUntilClickable()->one()->click();
		$this->page->waitUntilReady();

		// Check message after loading modules.
		if ($first_load) {
			// Each loaded module name is checked separately due to difference in their sorting on Jenkins and locally.
			$this->assertMessage(TEST_GOOD, 'Modules updated', ['Modules added:', '1st Module name',
					'2nd Module name !@#$%^&*()_+', '4th Module', '5th Module', 'Clock2', 'Empty widget', 'шестой модуль'
			]);
		}
		else {
			$this->assertMessage(TEST_GOOD, 'No new modules discovered');
		}
	}

	/**
	 * Function checks if the corresponding menu entry exists, clicks on it and checks the URL and header of the page.
	 * If the module should remove a menu entry, the function makes sure that the corresponding menu entry doesn't exist.
	 *
	 * @param array	$module		module related information from data provider.
	 */
	private function assertModuleEnabled($module) {
		$xpath = 'xpath://ul[@class="menu-main"]//a[text()="';
		// If module removes a menu entry or top level menu entry, check that such entries are not present.
		if (CTestArrayHelper::get($module, 'remove', false)) {
			$this->assertEquals(0, $this->query($xpath.$module['menu_entry'].'"]')->count());
			if (array_key_exists('top_menu_entry', $module)) {
				$this->assertEquals(0, $this->query($xpath.$module['top_menu_entry'].'"]')->count());
			}

			return;
		}
		// If module adds single or multiple menu entries, open each corresponding view, check view header and URL.
		$top_entry = CTestArrayHelper::get($module, 'top_menu_entry', 'Monitoring');

		$this->query('link', $top_entry)->one()->waitUntilClickable()->click();
		foreach ($module['menu_entries'] as $entry) {
			sleep(1);
			$this->query($xpath.$entry['name'].'"]')->one()->waitUntilClickable()->click();
			$this->page->waitUntilReady();
			$this->assertStringContainsString('zabbix.php?action='.$entry['action'], $this->page->getCurrentURL());
			$this->assertEquals($entry['message'], $this->query('tag:h1')->waitUntilVisible()->one()->getText());
		}
		// Get back to modules list to enable or disable the next module.
		$this->page->open('zabbix.php?action=module.list')->waitUntilReady();
	}

	/**
	 * Function checks if the corresponding menu entry is removed and url is not active after the module is disabled.
	 * If enabling the module removes a menu entry, the function checks that it is back after disabling the module.
	 *
	 * @param array	$module		module related information from data provider.
	 */
	private function assertModuleDisabled($module) {
		$xpath = 'xpath://ul[@class="menu-main"]//li/a[text()="';
		// If module removes a menu entry or top level menu entry, check that entries are back after disabling the module.
		if (array_key_exists('remove', $module)) {
			$this->assertEquals(1, $this->query($xpath.$module['menu_entry'].'"]')->count());

			if (array_key_exists('top_menu_entry', $module)) {
				$this->assertEquals(1, $this->query($xpath.$module['top_menu_entry'].'"]')->count());
			}

			return;
		}
		// If module adds single or multiple menu entries, check that entries don't exist after disabling the module.
		foreach ($module['menu_entries'] as $entry) {
			$check_entry = CTestArrayHelper::get($module, 'top_menu_entry', $entry['name']);
			$this->assertEquals(0, $this->query($xpath.$check_entry.'"]')->count());
			// In case if module many entry leads to an existing view, don't check that menu entry URL isn't available.
			if (CTestArrayHelper::get($entry, 'check_disabled', true)) {
				$this->page->open('zabbix.php?action='.$entry['action'])->waitUntilReady();
				$message = CMessageElement::find()->one();
				$this->assertStringContainsString('Page not found', $message->getText());
				$this->page->open('zabbix.php?action=module.list');
			}
		}
	}

	/**
	 * Function enables module from the list in modules page or from module details form, depending on input parameters.
	 *
	 * @param array		$data	data array with module details
	 * @param string	$view	view from which the module should be enabled - module list or module details form.
	 */
	private function enableModule($module, $view) {
		$expected = CTestArrayHelper::get($module, 'expected', TEST_GOOD);

		// Change module status from Disabled to Enabled.
		if ($view === 'form') {
			$this->changeModuleStatusFromForm($module['module_name'], true, $expected);
		}
		else {
			$this->changeModuleStatusFromPage($module['module_name'], 'Disabled');
		}
		// In case of negative test check error message and confirm that module wasn't applied.
		if ($expected === TEST_BAD) {
			$title = ($view === 'form') ? 'Cannot update module' : 'Cannot enable module';
			$this->assertMessage($module['expected'], $title, $module['error_details']);

			if ($view === 'form') {
				COverlayDialogElement::find()->one()->close();
			}

			$this->assertModuleDisabled($module);

			return;
		}
		// Check message and confirm that changes, made by the enabled module, took place.
		$message = ($view === 'form') ? 'Module updated' : 'Module enabled';
		$this->assertMessage($expected, $message);
		CMessageElement::find()->one()->close();
	}

	/**
	 * Function disables module from the list in modules page or from module details form, depending on input parameters.
	 *
	 * @param array		$module	data array with module details
	 * @param string	$view	view from which the module should be enabled - module list or module details form.
	 */
	private function disableModule($module, $view) {
		$expected = CTestArrayHelper::get($module, 'expected', TEST_GOOD);

		// In case of negative test do nothing.
		if ($expected === TEST_BAD) {

			return;
		}

		// Change module status from Enabled to Disabled.
		if ($view === 'form') {
			$this->changeModuleStatusFromForm($module['module_name'], false, $expected);
		}
		else {
			$this->changeModuleStatusFromPage($module['module_name'], 'Enabled');
		}
		// Check message and confirm that changes, made by the module, were reversed.
		$message = ($view === 'form') ? 'Module updated' : 'Module disabled';
		$this->assertMessage(TEST_GOOD, $message);
	}

	/**
	 * Function changes module status from the list in modules page.
	 *
	 * @param string	$name				module name
	 * @param string	$current_status		module current status that is going to be changed.
	 */
	private function changeModuleStatusFromPage($name, $current_status) {
		$table = $this->query('class:list-table')->asTable()->one();
		$row = $table->findRow('Name', $name);
		$row->query('link', $current_status)->one()->click();
		$this->page->waitUntilReady();
	}

	/**
	 * Function changes module status from the modules details form.
	 *
	 * @param string	$name			module name
	 * @param bool		$enabled		boolean value to be set in "Enabled" checkbox in module details form.
	 * @param constant	$expected		flag that determines whether the module update should succeed or fail.
	 */
	private function changeModuleStatusFromForm($name, $enabled, $expected) {
		$this->query('link', $name)->waitUntilVisible()->one()->click();
		$dialog = COverlayDialogElement::find()->one()->waitUntilReady();

		// Edit module status and press update.
		$dialog->query('id:status')->asCheckbox()->one()->set($enabled);
		$this->query('button:Update')->one()->click();

		if ($expected === TEST_GOOD) {
			$dialog->ensureNotPresent();
		}
	}
}