<?php
/*
** Zabbix
** Copyright (C) 2001-2022 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__).'/traits/TableTrait.php';
require_once dirname(__FILE__).'/behaviors/CMessageBehavior.php';

/**
 * @backup module
 */

class testPageAdministrationGeneralModules extends CWebTest {

	use TableTrait;

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

	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' => 'шестой модуль',
				'Version' => 'бета 2',
				'Author' => 'Работник Заббикса',
				'Description' => 'Удалить "Reports" из меню верхнего уровня, а так же удалить "Scripts" из секции "Administration".',
				'Status' => 'Disabled'
			]
		];
		// 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));
		}
		// Check that modules are not being loaded until the 'Scan directory' button is pressed.
		$this->assertEquals($this->query('class:nothing-to-show')->one()->getText(), 'No data found.');
		$this->assertEquals('Displaying 0 of 0 found', $this->query('class:table-stats')->one()->getText());
		$this->assertEquals('0 selected', $this->query('id:selected_count')->one()->getText());
		// Check modules table headers.
		$table = $this->query('class:list-table')->asTable()->one();
		$headers = $table->getHeadersText();
		// Remove empty element from headers array.
		array_shift($headers);
		$this->assertSame(['Name', 'Version', 'Author', 'Description', 'Status'], $headers);

		// Load modules.
		$this->loadModules();

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

		$count = CDBHelper::getCount('SELECT moduleid FROM module');
		$this->assertEquals('Displaying '.$count.' of '.$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' => 'module_number_1',
					'Namespace' => 'Example_A',
					'Homepage' => '1st module URL',
					'Enabled' => false
				]
			],
			// Module 2.
			[
				[
					'Name' => '2nd Module name !@#$%^&*()_+',
					'Version' => 'two !@#$%^&*()_+',
					'Author' => '2nd Module author !@#$%^&*()_+',
					'Description' => 'Module description !@#$%^&*()_+',
					'Directory' => 'module_number_2',
					'Namespace' => 'Example_B',
					'Homepage' => '!@#$%^&*()_+',
					'Enabled' => false
				]
			],
			// Module 4.
			[
				[
					'Name' => '4th Module',
					'Version' => '',
					'Author' => '-',
					'Description' => '-',
					'Directory' => 'module_number_4',
					'Namespace' => 'Example_A',
					'Homepage' => '-',
					'Enabled' => false
				]
			],
			// Module 5.
			[
				[
					'Name' => '5th Module',
					'Version' => '',
					'Author' => '-',
					'Description' => 'Adding top-level and sub-level menu',
					'Directory' => 'module_number_5',
					'Namespace' => 'Example_E',
					'Homepage' => '-',
					'Enabled' => false
				]
			],
			// Module 6.
			[
				[
					'Name' => 'шестой модуль',
					'Version' => 'бета 2',
					'Author' => 'Работник Заббикса',
					'Description' => 'Удалить "Reports" из меню верхнего уровня, а так же удалить "Scripts" из секции "Administration".',
					'Directory' => 'module_number_6',
					'Namespace' => 'Example_F',
					'Homepage' => '-',
					'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();
		$this->page->waitUntilReady();
		$form = $this->query('name:module-form')->asForm()->waitUntilVisible()->one();
		// Check value af every field in Module details form.
		foreach ($data as $key => $value) {
			$this->assertEquals($value, $form->getFieldContainer($key)->getText());
		}
	}

	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_title' => 'Cannot update module: 4th Module.',
						'error_details' => 'Identical namespace (Example_A) is used by modules located at '.
								'module_number_1, 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 Screens are removed.
			[
				[
					[
						'module_name' => 'шестой модуль',
						'remove' => true,
						'top_menu_entry' => 'Reports',
						'menu_entry' => 'Screens'
					]
				]
			]
		];
	}

	/**
	 * @backup module
	 * @dataProvider getModuleData
	 * @depends testPageAdministrationGeneralModules_Layout
	 */
	public function testPageAdministrationGeneralModules_EnableDisable($data) {
		$this->page->login()->open('zabbix.php?action=module.list');
		// Enable modules from modules page one by one and check that changes took place.
		$this->enableAndCheckModules($data);
		// Disable the module that was enabled from page and confirm that changes made by the module are reverted.
		$this->disableAndCheckModules($data);
		// Enable modules from module details form one by one and check that changes took place.
		$this->enableAndCheckModules($data, true);
		// Disable the module that was enabled from details form and confirm that changes made by the module are reverted.
		$this->disableAndCheckModules($data, true);
	}

	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' => [
						'2nd Module name !@#$%^&*()_+'
					]
				]
			],
			// Retrieve only Disabled modules.
			[
				[
					'filter' => [
						'Status' => 'Disabled'
					],
					'expected' => [
						'1st Module name',
						'4th Module',
						'5th Module',
						'шестой модуль'
					]
				]
			],
			// 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: 1st Module name.');
		// 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));
	}

	/**
	 * 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 separatelly due to difference in their sorting on Jenkinsand locally.
			$this->assertMessage(TEST_GOOD, 'Modules updated', ['Modules added:', '1st Module name',
					'2nd Module name !@#$%^&*()_+', '4th Module', '5th Module', 'шестой модуль']);
		}
		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.
	 */
	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->assertTrue($this->query($xpath.$module['menu_entry'].'"]')->count() === 0);
			if (array_key_exists('top_menu_entry', $module)) {
				$this->assertTrue($this->query($xpath.$module['top_menu_entry'].'"]')->count() === 0);
			}

			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');

		foreach ($module['menu_entries'] as $entry) {
			$this->query('link', $top_entry)->one()->waitUntilClickable()->click();
			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.
	 */
	private function assertModuleDisabled($module) {
		$xpath = 'xpath://ul[@class="menu-main"]//a[text()="';
		// If module removes a menu entry or top level menu entry, check that entries are back after disabling the module.
		if (CTestArrayHelper::get($module, 'remove', false)) {
			$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 meny 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('Class not found for action '.$entry['action'], $message->getText());
				$this->page->open('zabbix.php?action=module.list');
			}
		}
	}

	/**
	 * Function enables modules from the list in modules page or from module details form, depending on input parameters.
	 * @param array		$data			data array with module details
	 * @param bool		$from_form		flag that determines whether the module is enabled from module details form.
	 */
	private function enableAndCheckModules($data, $from_form = false) {
		foreach ($data as $module) {
			// Change module status from Disabled to Enabled.
			if ($from_form) {
				$this->changeModuleStatusFromForm($module['module_name'], true);
			}
			else {
				$this->changeModuleStatusFromPage($module['module_name'], 'Disabled');
			}
			// In case of negative test check error message and confirm that module wasn't applied.
			if (CTestArrayHelper::get($module, 'expected', TEST_GOOD) === TEST_BAD) {
				$title = $from_form ? 'Cannot update module: ' : 'Cannot enable module: ';
				$this->assertMessage($module['expected'], $title.$module['module_name'].'.', $module['error_details']);
				$this->assertModuleDisabled($module);
				continue;
			}
			// Check message and confirm that changes, made by the enabled module, took place.
			$message = $from_form ? 'Module updated: ' : 'Module enabled: ';
			$this->assertMessage(CTestArrayHelper::get($module, 'expected', TEST_GOOD), $message.$module['module_name'].'.');
			$this->assertModuleEnabled($module);
		}
	}

	/**
	 * Function disables modules from the list in modules page or from module details form, depending on input parameters.
	 * @param array		$data			data array with module details
	 * @param bool		$from_form		flag that determines whether the module is enabled from module details form.
	 */
	private function disableAndCheckModules($data, $from_form = false) {
		foreach ($data as $module) {
			// In case of negative test do nothing.
			if (CTestArrayHelper::get($module, 'expected', TEST_GOOD) === TEST_BAD) {
				continue;
			}
			// Change module status from Enabled to Disabled.
			if ($from_form) {
				$this->changeModuleStatusFromForm($module['module_name'], false);
			}
			else {
				$this->changeModuleStatusFromPage($module['module_name'], 'Enabled');
			}
			// Check message and confirm that changes, made by the module, were revered.
			$message = $from_form ? 'Module updated: ' : 'Module disabled: ';
			$this->assertMessage(TEST_GOOD, $message.$module['module_name'].'.');
			$this->assertModuleDisabled($module);
		}
	}

	/**
	 * 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.
	 */
	private function changeModuleStatusFromForm($name, $enabled) {
		$this->query('link', $name)->waitUntilVisible()->one()->click();
		$this->page->waitUntilReady();

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