<?php
/*
** Zabbix
** 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 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__).'/../../include/helpers/CDataHelper.php';
require_once dirname(__FILE__).'/../behaviors/CMessageBehavior.php';
use Facebook\WebDriver\Exception\UnexpectedAlertOpenException;
use Facebook\WebDriver\Exception\NoSuchElementException;

/**
 * @backup dashboard, hosts
 *
 * @onBefore prepareTemplateDashboardsData
 */
class testFormTemplateDashboards extends CWebTest {

	const UPDATE_TEMPLATEID = 50000;	// ID of the "Template ZBX6663 First" template used for template dashboards tests.
	const HOST_FOR_TEMPLATE = 99015;	// ID of the "Empty host" host to which a template with dashboards will be linked.

	protected static $dashboardid_with_widgets;
	protected static $empty_dashboardid;
	protected static $dashboardid_for_update;

	private static $previous_widget_name = 'Widget for update';

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

	/**
	 * Function creates template dashboards and defines the corresponding dashboard IDs.
	 */
	public static function prepareTemplateDashboardsData() {
		$response = CDataHelper::call('templatedashboard.create', [
			[
				'templateid' => self::UPDATE_TEMPLATEID,
				'name' => 'Dashboard with all widgets',
				'pages' => [
					[
						'name' => 'Page with widgets',
						'widgets' => [
							[
								'type' => 'clock',
								'name' => 'Clock widget',
								'width' => 4,
								'height' => 4
							],
							[
								'type' => 'graph',
								'name' => 'Graph (classic) widget',
								'x' => 4,
								'y' => 0,
								'width' => 8,
								'height' => 4,
								'fields' => [
									[
										'type' => 0,
										'name' => 'source_type',
										'value' => 1
									],
									[
										'type' => 4,
										'name' => 'itemid',
										'value' => 400410
									]
								]
							],
							[
								'type' => 'plaintext',
								'name' => 'Plain text widget',
								'x' => 12,
								'y' => 0,
								'width' => 6,
								'height' => 4,
								'fields' => [
									[
										'type' => 4,
										'name' => 'itemids',
										'value' => 400410
									]
								]
							],
							[
								'type' => 'url',
								'name' => 'URL widget',
								'x' => 18,
								'y' => 0,
								'width' => 6,
								'height' => 4,
								'fields' => [
									[
										'type' => 1,
										'name' => 'url',
										'value' => 'http://zabbix.com'
									]
								]
							],
							[
								'type' => 'graphprototype',
								'name' => 'Graph prototype widget',
								'x' => 0,
								'y' => 4,
								'width' => 12,
								'height' => 6,
								'fields' => [
									[
										'type' => 7,
										'name' => 'graphid',
										'value' => 700016
									]
								]
							]
						]
					]
				]
			],
			[
				'templateid' => self::UPDATE_TEMPLATEID,
				'name' => 'Empty Dashboard without widgets',
				'pages' => [[]]
			],
			[
				'templateid' => self::UPDATE_TEMPLATEID,
				'name' => 'Dashboard for widget update',
				'pages' => [
					[
						'widgets' => [
							[
								'type' => 'clock',
								'name' => 'Widget for update',
								'x' => 0,
								'y' => 0,
								'width' => 4,
								'height' => 4
							],
							[
								'type' => 'clock',
								'name' => 'Widget 4 duplicate check',
								'x' => 4,
								'y' => 0,
								'width' => 4,
								'height' => 4
							]
						]
					]
				]
			]
		]);

		self::$dashboardid_with_widgets = $response['dashboardids'][0];
		self::$empty_dashboardid = $response['dashboardids'][1];
		self::$dashboardid_for_update = $response['dashboardids'][2];
	}

	/**
	 * Function that links the template with dashboards to "Empty host" host.
	 */
	public static function prepareHostLinkageToTemplateData() {
		CDataHelper::call('host.update', [
			'hostid' => self::HOST_FOR_TEMPLATE,
			'templates' => [
				[
					'templateid' => self::UPDATE_TEMPLATEID
				]
			]
		]);
	}

	public function testFormTemplateDashboards_Layout() {
		$this->page->login()->open('zabbix.php?action=template.dashboard.list&templateid='.self::UPDATE_TEMPLATEID);
		$this->query('button:Create dashboard')->one()->click();
		$this->checkDialogue('Dashboard properties');
		// TODO: added updateViewport due to unstable test on Jenkins, scroll appears for 0.5 seconds
		// after closing the overlay dialog and incorrect click location of $control_buttons occurs.
		$this->page->updateViewport();

		// Check the default new dashboard state (title, empty, editable).
		$dashboard = CDashboardElement::find()->asDashboard()->one()->waitUntilReady();
		$this->assertEquals('Dashboards', $dashboard->getTitle());
		$this->assertTrue($dashboard->isEditable());
		$this->assertTrue($dashboard->isEmpty());

		$controls = $dashboard->getControls();
		$control_buttons = [
			'id:dashboard-config',		// Dashboard properties menu icon.
			'button:Add',				// Add widget menu button.
			'id:dashboard-add',			// Dashboard actions chevron.
			'button:Save changes',		// Save changes button.
			'link:Cancel'				// Cancel button.
		];

		// Check dashboard controls and their corresponding actions.
		foreach ($control_buttons as $selector) {
			$this->assertTrue($controls->query($selector)->one(false)->isValid());

			switch ($selector) {
				case 'id:dashboard-config':
					// TODO: unstable test on Jenkins, sometimes click does not work properly and overlay does not open
					try {
						$controls->query($selector)->waitUntilClickable()->one()->click();
						COverlayDialogElement::find()->one();
					}
					catch (NoSuchElementException $e) {
						$controls->query($selector)->waitUntilClickable()->one()->click();
					}
					$this->checkDialogue('Dashboard properties');
					break;

				case 'id:dashboard-add':
					$reference_items = [
						'Add widget' => true,
						'Add page' => true,
						'Paste widget' => false,
						'Paste page' => false
					];
					$controls->query($selector)->waitUntilClickable()->one()->click();
					$this->checkPopup($reference_items);
					break;

				case 'button:Add':
					$controls->query($selector)->waitUntilClickable()->one()->click();
					$this->checkDialogue('Add widget');
					break;
			}
		}

		// Check breadcrumbs.
		foreach (['Hierarchy', 'Content menu'] as $aria_label) {
			$this->assertTrue($this->query('xpath://ul[@aria-label='.zbx_dbstr($aria_label).']')->one()->isClickable());
		}

		// Check the page title and its corresponding actions.
		$this->assertEquals(1, $this->query('class:sortable-item')->all()->count());
		$page_button = $this->query('class:selected-tab')->one();
		$this->assertEquals('Page 1', $page_button->getText());
		$page_button->query('xpath:./button')->one()->forceClick();
		$page_popup_items = [
			'Copy' => true,
			'Delete' => false,
			'Properties' => true
		];
		$this->checkPopup($page_popup_items, 'ACTIONS');

		// Close the dashboard and corresponding popups so that the next scenario would start without alerts.
		$this->closeDialogue();
	}

	public static function getWidgetLayoutData() {
		return [
			[
				[
					'type' => 'Clock',
					'fields' => [
						[
							'name' => 'Name',
							'attributes' => [
								'placeholder' => 'default',
								'maxlength' => 255
							]
						],
						[
							'name' => 'Time type',
							'type' => 'dropdown',
							'possible_values' => ['Local time', 'Server time', 'Host time'],
							'value' => 'Local time'
						]
					]
				]
			],
			[
				[
					'type' => CFormElement::RELOADABLE_FILL('Graph (classic)'),
					'fields' => [
						[
							'name' => 'Name',
							'attributes' => [
								'placeholder' => 'default',
								'maxlength' => 255
							]
						],
						[
							'name' => 'Source',
							'type' => 'radio_button',
							'possible_values' => ['Graph', 'Simple graph'],
							'value' => 'Graph'
						],
						[
							'name' => 'Graph',
							'type' => 'multiselect'
						],
						[
							'name' => 'Show legend',
							'type' => 'checkbox',
							'value' => true
						]
					]
				]
			],
			[
				[

					'type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
					'fields' => [
						[
							'name' => 'Name',
							'attributes' => [
								'placeholder' => 'default',
								'maxlength' => 255
							]
						],
						[
							'name' => 'Source',
							'type' => 'radio_button',
							'possible_values' => ['Graph prototype', 'Simple graph prototype'],
							'value' => 'Graph prototype'
						],
						[
							'name' => 'Graph prototype',
							'type' => 'multiselect'
						],
						[
							'name' => 'Show legend',
							'type' => 'checkbox',
							'value' => true
						],
						[
							'name' => 'Columns',
							'value' => 2,
							'attributes' => [
								'maxlength' => 2
							]
						],
						[
							'name' => 'Rows',
							'value' => 1,
							'attributes' => [
								'maxlength' => 2
							]
						]
					]
				]
			],
			[
				[
					'type' => CFormElement::RELOADABLE_FILL('Plain text'),
					'fields' => [
						[
							'name' => 'Name',
							'attributes' => [
								'placeholder' => 'default',
								'maxlength' => 255
							]
						],
						[
							'name' => 'Items',
							'type' => 'multiselect'
						],
						[
							'name' => 'Items location',
							'type' => 'radio_button',
							'possible_values' => ['Left', 'Top'],
							'value' => 'Left'
						],
						[
							'name' => 'Show lines',
							'value' => 25,
							'attributes' => [
								'maxlength' => 3
							]
						],
						[
							'name' => 'Show text as HTML',
							'type' => 'checkbox',
							'value' => false
						]
					]
				]
			],
			[
				[
					'type' => CFormElement::RELOADABLE_FILL('URL'),
					'fields' => [
						[
							'name' => 'Name',
							'attributes' => [
								'placeholder' => 'default',
								'maxlength' => 255
							]
						],
						[
							'name' => 'URL',
							'attributes' => [
								'maxlength' => 255
							]
						]
					]
				]
			]
		];
	}

	/**
	 * Function that checks the layout and the default settings of widget configuration forms.
	 *
	 * @dataProvider getWidgetLayoutData
	 */
	public function testFormTemplateDashboards_WidgetDefaultLayout($data) {
		$this->page->login()->open('zabbix.php?action=template.dashboard.list&templateid='.self::UPDATE_TEMPLATEID);
		$this->query('button:Create dashboard')->one()->click();
		COverlayDialogElement::find()->one()->waitUntilVisible()->close();

		// Select the required type of widget.
		$this->query('button:Add')->one()->waitUntilClickable()->click();
		$widget_dialog = COverlayDialogElement::find()->asForm()->one()->waitUntilReady();
		$widget_dialog->fill(['Type' => $data['type']]);
		COverlayDialogElement::find()->waitUntilReady();

		// Check form fields and their attributes based on field type.
		foreach ($data['fields'] as $field_details) {
			$field = $widget_dialog->getField($field_details['name']);
			$default_value = CTestArrayHelper::get($field_details, 'value', '');

			switch (CTestArrayHelper::get($field_details, 'type', 'input')) {
				case 'input':
				case 'checkbox':
					$this->assertEquals($default_value, $field->getValue());
					if (array_key_exists('attributes', $field_details)) {
						foreach ($field_details['attributes'] as $attribute => $value) {
							$this->assertEquals($value, $field->getAttribute($attribute));
						}
					}
					break;

				case 'multiselect':
					$default_value = '';
					$this->assertEquals($default_value, $field->getValue());
					$this->assertEquals('type here to search', $field->query('xpath:.//input')->one()->getAttribute('placeholder'));
					break;

				case 'dropdown':
					$this->assertEquals($default_value, $field->getValue());
					$this->assertEquals($field_details['possible_values'], $field->getOptions()->asText());
					break;

				case 'radio_button':
					$this->assertEquals($default_value, $field->getValue());
					$this->assertEquals($field_details['possible_values'], $field->getLabels()->asText());
					break;
			}
		}
		$this->assertTrue($widget_dialog->getField('Show header')->getValue());

		// Close editing dashboard so that next test case would not fail with "Unexpected alert" error.
		$this->closeDialogue();
	}

	public static function getDashboardPropertiesData() {
		return [
			[
				[
					'dashboard_properties' => [
						'Name' => 'Empty dashboard'
					]
				]
			],
			[
				[
					'expected' => TEST_BAD,
					'dashboard_properties' => [
						'Name' => ''
					],
					'error_message' => 'Incorrect value for field "name": cannot be empty.'
				]
			],
			[
				[
					'expected' => TEST_BAD,
					'dashboard_properties' => [
						'Name' => '   '
					],
					'error_message' => 'Incorrect value for field "name": cannot be empty.'
				]
			],
			[
				[
					'expected' => TEST_BAD,
					'dashboard_properties' => [
						'Name' => 'Empty Dashboard without widgets'
					],
					'error_message' => 'Dashboard "Empty Dashboard without widgets" already exists.',
					'check_save' => true
				]
			],
			[
				[
					'dashboard_properties' => [
						'Name' => '!@#$%^&*()_+=-09[]{};:\'"',
						'Default page display period' => '10 seconds',
						'Start slideshow automatically' => true
					]
				]
			],
			[
				[
					'dashboard_properties' => [
						'Name' => '    Trailing & leading spaces    ',
						'Start slideshow automatically' => false
					],
					'trim' => 'Name'
				]
			]
		];
	}

	/**
	 * Function that checks validation of the Dashboard properties overlay dialog when creating a dashboard.
	 *
	 * @backupOnce dashboard
	 *
	 * @dataProvider getDashboardPropertiesData
	 */
	public function testFormTemplateDashboards_DashboardPropertiesCreate($data) {
		$this->page->login()->open('zabbix.php?action=template.dashboard.list&templateid='.self::UPDATE_TEMPLATEID);
		$this->query('button:Create dashboard')->one()->click();
		$form = COverlayDialogElement::find()->asForm()->one()->waitUntilVisible();

		$form->fill($data['dashboard_properties']);
		$old_values = $form->getFields()->asValues();
		$form->submit();

		$this->checkSettings($data, $old_values);
	}

	/**
	 * Function that checks validation of Dashboard properties overlay dialog update operations.
	 *
	 * @backupOnce dashboard
	 *
	 * @dataProvider getDashboardPropertiesData
	 */
	public function testFormTemplateDashboards_DashboardPropertiesUpdate($data) {
		$this->page->login()->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$dashboardid_with_widgets);
		$this->query('id:dashboard-config')->one()->waitUntilClickable()->click();
		$form = COverlayDialogElement::find()->asForm()->one()->waitUntilVisible();

		$form->fill($data['dashboard_properties']);
		$old_values = $form->getFields()->asValues();
		$form->submit();

		$this->checkSettings($data, $old_values, 'updated');
	}

	/**
	 * Function that checks that no changes occur after saving a template dashboard without changes.
	 */
	public function testFormTemplateDashboards_SimpleUpdate() {
		$sql = 'SELECT * FROM widget w INNER JOIN dashboard_page dp ON dp.dashboard_pageid=w.dashboard_pageid '.
				'INNER JOIN dashboard d ON d.dashboardid=dp.dashboardid ORDER BY w.widgetid';
		$old_hash = CDBHelper::getHash($sql);

		$this->page->login()->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$dashboardid_with_widgets);
		$this->query('button:Save changes')->one()->waitUntilClickable()->click();

		$this->assertMessage(TEST_GOOD, 'Dashboard updated');
		$this->assertEquals($old_hash, CDBHelper::getHash($sql));
	}

	/**
	 * Function that checks that no changes occur after cancelling a template dashboard update.
	 */
	public function testFormTemplateDashboards_Cancel() {
		$sql = 'SELECT * FROM widget w INNER JOIN dashboard_page dp ON dp.dashboard_pageid=w.dashboard_pageid '.
				'INNER JOIN dashboard d ON d.dashboardid=dp.dashboardid ORDER BY w.widgetid';
		$old_hash = CDBHelper::getHash($sql);
		$fields = [
			'Name' => 'Cancel dashboard update',
			'Default page display period' => '10 minutes',
			'Start slideshow automatically' => false
		];

		$this->page->login()->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$dashboardid_with_widgets);
		$this->query('id:dashboard-config')->one()->waitUntilClickable()->click();
		$form = COverlayDialogElement::find()->asForm()->one()->waitUntilVisible();
		$form->fill($fields);
		$form->submit();

		$this->query('link:Cancel')->one()->waitUntilClickable()->click();
		$this->assertEquals($old_hash, CDBHelper::getHash($sql));
	}

	public static function getWidgetsCreateData() {
		return [
			// Renaming a widget
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => 'Change widget name'
					]
				]
			],
			// Two identical widgets
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => 'Widget 4 duplicate check'
					],
					'duplicate widget' => true
				]
			],
			// Clock widget with no name
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => ''
					]
				]
			],
			// Change time type to Server time
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => 'Clock widget server time',
						'Time type' => CFormElement::RELOADABLE_FILL('Server time')
					]
				]
			],
			// Change time type to Host time and leave item empty
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => 'Clock widget with Host time no item',
						'Time type' => CFormElement::RELOADABLE_FILL('Host time')
					],
					'error_message' => 'Invalid parameter "Item": cannot be empty.'
				]
			],
			// Change time type to Host time and specify item
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => 'Clock widget with Host time',
						'Time type' => CFormElement::RELOADABLE_FILL('Host time'),
						'Item' => 'Item ZBX6663 Second'
					]
				]
			],
			// Widget with trailing and leading spaces in name
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Clock'),
						'Name' => '    Clock widget with trailing and leading spaces    '
					],
					'trim' => 'Name'
				]
			],
			// Graph Classic widget with missing graph
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph (classic)'),
						'Name' => 'Graph widget with empty graph',
						'Source' => 'Graph',
						'Graph' => []
					],
					'error_message' => 'Invalid parameter "Graph": cannot be empty.'
				]
			],
			// Graph Classic widget with missing item
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph (classic)'),
						'Name' => 'Graph widget with empty item',
						'Source' => 'Simple graph',
						'Item' => []
					],
					'error_message' => 'Invalid parameter "Item": cannot be empty.'
				]
			],
			// Graph Classic widget with graph and legend
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph (classic)'),
						'Name' => 'Graph widget with graph and legend',
						'Source' => 'Graph',
						'Graph' => ['Graph ZBX6663 Second'],
						'Show legend' => true
					]
				]
			],
			// Graph Classic widget with Simple graph and without legend
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph (classic)'),
						'Name' => 'Simple graph without legend',
						'Source' => 'Simple graph',
						'Item' => ['Item ZBX6663 Second'],
						'Show legend' => false
					]
				]
			],
			// Graph prototype widget with missing graph prototype
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with empty graph',
						'Source' => 'Graph prototype',
						'Graph prototype' => []
					],
					'error_message' => 'Invalid parameter "Graph prototype": cannot be empty.'
				]
			],
			// Graph prototype widget with missing item prototype
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with empty item prototype',
						'Source' => 'Simple graph prototype',
						'Item prototype' => []
					],
					'error_message' => 'Invalid parameter "Item prototype": cannot be empty.'
				]
			],
			// Graph prototype widget with empty Columns parameter
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with empty Columns (dropped to 0)',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Columns' => ''
					],
					'error_message' => 'Invalid parameter "Columns": value must be one of 1-24.'
				]
			],
			// Graph prototype widget with too high number of Columns
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with too much Columns',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Columns' => 55
					],
					'error_message' => 'Invalid parameter "Columns": value must be one of 1-24.'
				]
			],
			// Graph prototype widget with negative number of Columns
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with too much Columns',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Columns' => '-5'
					],
					'error_message' => 'Invalid parameter "Columns": value must be one of 1-24.'
				]
			],
			// Graph prototype widget with missing number of Rows
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with missing Rows (dropped to 0)',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Rows' => ''
					],
					'error_message' => 'Invalid parameter "Rows": value must be one of 1-16.'
				]
			],
			// Graph prototype widget with number of Rows too high
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with too much rows',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Rows' => 55
					],
					'error_message' => 'Invalid parameter "Rows": value must be one of 1-16.'
				]
			],
			// Graph prototype widget with negative number of Rows
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget with too much rows',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Rows' => '-5'
					],
					'error_message' => 'Invalid parameter "Rows": value must be one of 1-16.'
				]
			],
			// Graph prototype widget with graph prototype, legend, 2 rows and 2 columns
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Graph prototype widget legend',
						'Source' => 'Graph prototype',
						'Graph prototype' => ['GraphPrototype ZBX6663 Second'],
						'Show legend' => true,
						'Columns' => 2,
						'Rows' => 2
					]
				]
			],
			// Graph prototype widget with simple graph prototype, without legend, 2 row and 2 column
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Graph prototype'),
						'Name' => 'Simple Graph prototype without legend',
						'Source' => 'Simple graph prototype',
						'Item prototype' => ['ItemProto ZBX6663 Second'],
						'Show legend' => false,
						'Columns' => 1,
						'Rows' => 1
					]
				]
			],
			// Plain text widget with empty Items parameter
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Plain text'),
						'Name' => 'Plain text widget with empty Items',
						'Items' => []
					],
					'error_message' => 'Invalid parameter "Items": cannot be empty.'
				]
			],
			// Plain text widget with empty Show lines parameter (reset to 0)
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Plain text'),
						'Name' => 'Plain text widget with empty Show lines',
						'Items' => ['Item ZBX6663 Second'],
						'Show lines' => ''
					],
					'error_message' => 'Invalid parameter "Show lines": value must be one of 1-100.'
				]
			],
			// Plain text widget with too high value of Show lines parameter
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Plain text'),
						'Name' => 'Plain text widget with too much lines',
						'Items' => ['Item ZBX6663 Second'],
						'Show lines' => 999
					],
					'error_message' => 'Invalid parameter "Show lines": value must be one of 1-100.'
				]
			],
			// Plain text widget with negative Show lines parameter
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Plain text'),
						'Name' => 'Plain text widget with negative Show lines',
						'Items' => ['Item ZBX6663 Second'],
						'Show lines' => '-9'
					],
					'error_message' => 'Invalid parameter "Show lines": value must be one of 1-100.'
				]
			],
			// Plain text widget with Items location = top and text shown as HTML
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Plain text'),
						'Name' => 'Plain text widget top location HTML',
						'Items' => ['Item ZBX6663 Second'],
						'Show lines' => 9,
						'Items location' => 'Top',
						'Show text as HTML' => true
					]
				]
			],
			// Plain text widget with Items location = left and text shown as plain text
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('Plain text'),
						'Name' => 'Plain text widget left location no HTML',
						'Items' => ['Item ZBX6663 Second'],
						'Show lines' => 9,
						'Items location' => 'Left',
						'Show text as HTML' => false
					]
				]
			],
			// URL widget with empty URL
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('URL'),
						'Name' => 'URL widget with empty URL'
					],
					'error_message' => 'Invalid parameter "URL": cannot be empty.'
				]
			],
			// URL widget with incorrect URL
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('URL'),
						'Name' => 'URL widget with text URL',
						'URL' => 'home_sweet_home'
					]
				]
			],
			// URL widget with trailing and leading spaces in URL
			[
				[
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('URL'),
						'Name' => 'URL widget with trailing and leading spaces in URL',
						'URL' => '    URL    '
					],
					'trim' => 'URL'
				]
			],
			// URL widget with spaces in URL
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'Type' => CFormElement::RELOADABLE_FILL('URL'),
						'Name' => 'URL widget with space in URL',
						'URL' => '     '
					],
					'trim' => 'URL',
					'error_message' => 'Invalid parameter "URL": cannot be empty.'
				]
			]
		];
	}

	/**
	 * Function that validates the Widget configuration form for a template dashboard.
	 *
	 * @dataProvider getWidgetsCreateData
	 */
	public function testFormTemplateDashboards_CreateWidget($data) {
		try {
			$this->page->login()->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$empty_dashboardid);
		}
		catch (UnexpectedAlertOpenException $e) {
			// Sometimes previous test leaves dashboard edit page open.
			$this->page->acceptAlert();
			$this->page->login()->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$empty_dashboardid);
		}

		$this->query('button:Add')->one()->waitUntilClickable()->click();
		$form = COverlayDialogElement::find()->asForm()->one()->waitUntilVisible();
		$form->fill($data['fields']);

		// Trimming is only triggered together with an on-change event which is generated once focus is removed.
		$this->page->removeFocus();
		$old_values = $form->getFields()->filter(CElementFilter::VISIBLE)->asValues();
		$form->submit();

		// In case of the scenario with identical widgets the same widget needs to be added once again.
		if (array_key_exists('duplicate widget', $data)) {
			$this->query('button:Add')->one()->waitUntilClickable()->click();
			$form->invalidate();
			$form->fill($data['fields']);
			$this->page->removeFocus();
			$form->submit();
		}

		$this->checkSettings($data, $old_values, 'updated', 'widget create');
	}

	/**
	 * Function that checks update of template dashboards widgets parameters.
	 *
	 * @dataProvider getWidgetsCreateData
	 */
	public function testFormTemplateDashboards_UpdateWidget($data) {
		$this->page->login()->open('zabbix.php?action=template.dashboard.edit&dashboardid='.self::$dashboardid_for_update);

		$form = CDashboardElement::find()->one()->getWidget(self::$previous_widget_name)->edit();
		COverlayDialogElement::find()->waitUntilReady();
		$form->fill($data['fields']);
		$this->page->removeFocus();
		COverlayDialogElement::find()->waitUntilReady();
		$old_values = $form->getFields()->filter(CElementFilter::VISIBLE)->asValues();
		$form->submit();

		$this->checkSettings($data, $old_values, 'updated', 'widget update');
	}

	/**
	 * Function that checks the layout of a template dashboard with widgets from monitoring hosts view.
	 * The ignore browser errors annotation is required due to the errors coming from the URL opened in the URL widget.
	 *
	 * @ignoreBrowserErrors
	 *
	 * @onBefore prepareHostLinkageToTemplateData
	 */
	public function testFormTemplateDashboards_ViewDashboardOnHost() {
		$this->page->login()->open('zabbix.php?action=host.dashboard.view&hostid='.self::HOST_FOR_TEMPLATE);
		$this->page->waitUntilReady();
		$this->query('id:dashboardid')->asDropdown()->one()->select('Dashboard with all widgets');

		$skip_selectors = [
			'class:clock',
			'class:flickerfreescreen',
			'class:widget-url',
			'xpath://footer'
		];
		$skip_elements = [];

		foreach ($skip_selectors as $identifier) {
			$skip_elements[] = $this->query($identifier)->waitUntilVisible()->one();
		}

		$this->assertScreenshotExcept(null, $skip_elements, 'dashboard_on_host');
	}

	/**
	 * Functions that checks the layout of the "Dashboard properties" and "Add widget" overlay dialogs.
	 *
	 * @param string	$title	The title of the overlay dialog.
	 */
	private function checkDialogue($title) {
		if ($title === 'Dashboard properties') {
			$parameters = [
				'Name' => 'New dashboard',
				'Default page display period' => '30 seconds',
				'Start slideshow automatically' => true
			];
			$buttons = ['Apply', 'Cancel'];
			$display_periods = ['10 seconds', '30 seconds', '1 minute', '2 minutes', '10 minutes', '30 minutes', '1 hour'];
		}
		else {
			$parameters = [
				'Show header' => true
			];
			$buttons = ['Add', 'Cancel'];
		}

		$dialog = COverlayDialogElement::find()->waitUntilReady()->one();
		$form = $dialog->asForm();
		$this->assertEquals($title, $dialog->getTitle());

		$this->assertEquals(2, $dialog->getFooter()->query('button', $buttons)->all()
				->filter(new CElementFilter(CElementFilter::CLICKABLE))->count()
		);

		foreach ($parameters as $name => $value) {
			$this->assertEquals($value, $form->getField($name)->getValue());
		}

		if ($title === 'Dashboard properties') {
			$this->assertEquals($display_periods, $form->getField('Default page display period')->getOptions()->asText());
		}
		else {
			$this->assertEquals(['Clock', 'Graph (classic)', 'Graph prototype', 'Item value', 'Plain text', 'URL'],
					$form->getField('Type')->getOptions()->asText()
			);
		}
		$dialog->close();
	}

	/**
	 * Function that checks the content of popup menu elements on a template dashboard.
	 *
	 * @param array		$items	An array of items and their states in a popup menu.
	 * @param string	$title	The title of the popup menu.
	 */
	private function checkPopup($items, $title = false) {
		$popup = CPopupMenuElement::find()->one()->waitUntilVisible();

		foreach ($items as $item => $enabled) {
			$this->assertTrue($popup->getItem($item)->isEnabled($enabled));
		}

		if ($title) {
			$this->assertEquals([$title], $popup->getTitles()->asText());
		}
	}

	/**
	 * Function that closes an overlay dialog and alert on a template dashboard before proceeding to the next test.
	 */
	private function closeDialogue() {
		$overlay = COverlayDialogElement::find()->one(false);
		if ($overlay->isValid()) {
			$overlay->close();
		}
		$this->query('link:Cancel')->one()->forceClick();

		if ($this->page->isAlertPresent()) {
			$this->page->acceptAlert();
		}
	}

	/**
	 * Function that checks the previously saved dashboard settings form.
	 *
	 * @param array		$data			Data provider.
	 * @param array		$old_values		Values obtained from the configuration form before saving the dashboard.
	 * @param string	$status			Expected successful action that was made to the dashboard after saving it.
	 * @param string	$check			Action that should be checked.
	 */
	private function checkSettings($data, $old_values, $status = 'created', $check = 'dashboard action') {
		if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) {
			if (CTestArrayHelper::get($data, 'check_save')) {
				$this->query('button:Save changes')->one()->click();
			}
			else {
				if (array_key_exists('trim', $data)) {
					$old_values[$data['trim']] = trim($old_values[$data['trim']]);
				}
				$form = COverlayDialogElement::find()->asForm()->one()->waitUntilVisible();
				$this->assertEquals($old_values, $form->getFields()->filter(CElementFilter::VISIBLE)->asValues());
			}
			$this->assertMessage(TEST_BAD, null, $data['error_message']);
			$this->closeDialogue();
		}
		else {
			COverlayDialogElement::ensureNotPresent();
			// Wait for widgets to be present as dashboard is slow when there ame many widgets on it.
			if ($check !== 'dashboard action') {
				if (CTestArrayHelper::get($data, 'trim') === 'Name') {
					$data['fields']['Name'] = trim($data['fields']['Name']);
				}
				$name = ($data['fields']['Name'] === '') ? 'Local' : $data['fields']['Name'];
				CDashboardElement::find()->waitUntilReady()->one()->getWidget($name);
			}
			$this->query('button:Save changes')->one()->click();

			$this->page->waitUntilReady();
			$this->assertMessage(TEST_GOOD, 'Dashboard '.$status);

			// In case of successful widget update rewrite the widget name to be updated for the next scenario.
			if ($check === 'widget update') {
				self::$previous_widget_name = $name;
			}

			// Trim trailing and leading spaces from reference dashboard name if necessary.
			$created_values = $old_values;
			if (array_key_exists('trim', $data)) {
				$created_values[$data['trim']] = trim($created_values[$data['trim']]);
			}

			$dashboard_name = ($check === 'dashboard action')
					? $created_values['Name']
					: (($check === 'widget create') ? 'Empty Dashboard without widgets' : 'Dashboard for widget update');
			$this->query('link', $dashboard_name)->one()->waitUntilClickable()->click();
			$this->page->waitUntilReady();

			if ($check !== 'dashboard action') {
				$reopened_form = CDashboardElement::find()->waitUntilReady()->one()->getWidget($name)->edit();
			}
			else {
				$this->query('id:dashboard-config')->one()->click();
				$reopened_form = COverlayDialogElement::find()->asForm()->one()->waitUntilVisible();
			}

			$this->assertEquals($created_values, $reopened_form->getFields()->filter(CElementFilter::VISIBLE)->asValues());

			$this->closeDialogue();
		}
	}
}