<?php /** @noinspection PhpMissingDocCommentInspection */
/*********************************************************************************
 *
 * TimeTrex is a Workforce Management program developed by
 * TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 * details.
 *
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program; if not, see http://www.gnu.org/licenses or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 *
 *
 * You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
 * #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
 *
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by TimeTrex" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by TimeTrex".
 *
 ********************************************************************************/

/**
 * Class TTSeleniumGlobal
 *
 * weird xpath examples:
 * // //*[starts-with(@id, 'ceil_')]
 */
class TTSeleniumGlobal extends PHPUnit\Extensions\Selenium2TestCase {

	private $default_wait_timeout = 4000;//100000;

	public $width = 1440;
	public $height = 900;

	public function setUp(): void {
		global $selenium_config;
		$this->selenium_config = $selenium_config;

		Debug::text( 'Running setUp(): ', __FILE__, __LINE__, __METHOD__, 10 );

		TTDate::setTimeZone( 'Etc/GMT+8', true ); //Due to being a singleton and PHPUnit resetting the state, always force the timezone to be set.

		$this->setHost( $selenium_config['host'] );
		$this->setPort( (int)$selenium_config['port'] );
		$this->setBrowser( $selenium_config['browser'] );
		$this->setBrowserUrl( $selenium_config['default_url'] );

		$this->setDesiredCapabilities( [ 'chromeOptions' => [ 'args' => [ '--incognito' ], ] ] ); //Use incognito mode to help prevent caching between sessions and saving passwords and such.


		$this->setDesiredCapabilities( [
											   'goog:chromeOptions' => [
													   'w3c' => false,
											   ],
									   ] );
	}

	public function tearDown(): void {
		Debug::text( 'Running tearDown(): ', __FILE__, __LINE__, __METHOD__, 10 );
	}

	function goToLogin( $user ) {
		Debug::text( 'Login to: ' . $this->selenium_config['default_url'] . ' Username: ' . $user, __FILE__, __LINE__, __METHOD__, 10 );
		$this->url( $this->selenium_config['default_url'] );

		sleep( 2.5 ); //have to be sure that Global.js is loaded before we start trying to use it.
		$this->setUnitTestMode( $user );

		$this->waitUntilByCssSelector( '#user_name' );
	}

	function Login( $user, $pass ) {
		//disable the overlay to speed up testing

		$this->goToLogin( $user );

		$this->waitThenClick( '#user_name' );
		$this->keys( $user );
		//$this->keys('demoadmin2');

		$this->waitThenClick( '#password' );
		$this->keys( $pass );

		$this->waitThenClick( '#login_btn' );

		sleep( 1 ); //wait for login
		$this->waitForUIInitComplete();
		$this->waitUntilById( 'topbar-company-logo' ); //the css not() selector is there to differentiate the various calls in the server log.

		//needed as development mode reloads and clears the variables.
		$javascript = [ 'script' => 'Global.setUnitTestMode();', 'args' => [] ];
		$this->execute( $javascript );

		Debug::text( 'Login Complete...', __FILE__, __LINE__, __METHOD__, 10 );
	}

	function Logout() {
		//because we could want to log out from any point
		$this->goToDashboard();
		$this->waitForUIInitComplete();

		$this->waitUntilById( 'profile-button' );
		$this->byId( 'profile-button' )->click();

		$this->waitUntilById( 'profile-menu-Logout' );
		$this->byId( 'profile-menu-Logout' )->click();

		$this->waitUntilById( 'user_name' );
		Debug::text( 'Logout...', __FILE__, __LINE__, __METHOD__, 10 );
	}

	function waitUntilById( $id, $timeout = null ) {
		if ( $timeout == null ) {
			$timeout = $this->default_wait_timeout;
		}

		$this->waitUntil( function () use ( $id ) {
			$javascript = [ 'script' => "$('#overlay.overlay:visible').length", 'args' => [] ];
			if ( $this->execute( $javascript ) == 0 && $this->byId( $id ) ) {

				return true;
			}

			return null;
		}, $timeout );
	}

	function changeLanguageLogin( $language_value ) {
		$this->waitUntilByCssSelector( '#language-selector' );

		$language_selector = $this->byId( 'language-selector' );
		$language_selector->click();

		$language_options = $language_selector->elements(
				$this->using( 'css selector' )->value( 'option' )
		);

		foreach ( $language_options as $language_option ) {
			if ( $language_option->value() == $language_value ) {
				$language_option->click();
				break;
			}
		}
	}

	function changeLanguagePreference( $language_value ) {
		$this->goToDashboard();

		$this->waitUntilById( 'profile-button' );
		$this->byId( 'profile-button' )->click();

		$this->waitUntilById( 'profile-menu-LoginUserPreference' );
		$this->byId( 'profile-menu-LoginUserPreference' )->click();

		$this->waitForUIInitComplete();

		$language_selector = $this->byCssSelector( '#tab_preferences_content_div select:first-of-type' );
		$language_selector->click();

		$language_options = $language_selector->elements(
				$this->using( 'css selector' )->value( 'option' )
		);

		foreach ( $language_options as $language_option ) {
			if ( $language_option->value() == $language_value ) {
				$language_option->click();
				break;
			}
		}

		$this->byId( 'context-button-save' )->click();

		$this->waitForUIInitComplete();
	}

	function waitUntilByCssSelector( $selector, $timeout = null ) {
		if ( $timeout == null ) {
			$timeout = $this->default_wait_timeout;
		}

		$this->waitUntil( function () use ( $selector ) {
			$javascript = [ 'script' => "$('#overlay.overlay:visible').length", 'args' => [] ];
			if ( $this->execute( $javascript ) == 0 && $this->byCssSelector( $selector ) ) {
				return true;
			}

			return null;
		}, $timeout );
	}

	function takeScreenshot( $screenshot_file_name, $create_dir = true ) {
		if ( $create_dir === true ) {
			$dirname = dirname( $screenshot_file_name );
			if ( file_exists( $dirname ) == false ) {
				mkdir( $dirname, 0777, true );
			}
		}

		$this->waitForUIInitComplete();
		// get the mousepointer and focus away from hover effects and flashing cursors
		// these cause significant differences in the screenshots.
		$this->waitUntilByCssSelector( '#powered_by,#copy_right_logo' );
		$this->moveto( $this->byCssSelector( '#powered_by,#copy_right_logo' ) );
		$this->waitUntilByCssSelector( '#powered_by,#copy_right_logo' );

		$retval = file_put_contents( $screenshot_file_name, $this->currentScreenshot() );
		chmod( $screenshot_file_name, 0777 );

		return $retval;
	}

	function getOSUser() {
		if ( function_exists( 'posix_geteuid' ) && function_exists( 'posix_getpwuid' ) ) {
			$user = posix_getpwuid( posix_geteuid() );
			Debug::Text( 'Webserver running as User: ' . $user['name'], __FILE__, __LINE__, __METHOD__, 10 );

			return $user['name'];
		}

		return false;
	}

	function goToDashboard() {
		$this->waitUntilById( 'topbar-company-logo' );
		$this->byId( 'topbar-company-logo' )->byXPath("./..")->click(); //click on the logo parent (href) to go to the dashboard
		//dashboard will reliably use init_complete after everything is loaded.
		$this->waitForUIInitComplete();
	}

	function waitForUIInitComplete() {
		$this->waitForUIInitCompleteLoops = 0;
		$this->waitUntil( function ( $_self_ ) {
			//Global.getUIReadyStatus will be == 2 when the screens are finished loading.

			$javascript = [ 'script' => 'if ( ( typeof Global != "undefined" && Global.getUIReadyStatus() == 2 ) && ( typeof TTPromise != "undefined" && TTPromise.isPendingPromises() == false ) ) { return true; } else { return false; }', 'args' => [] ];
			$js_retval = $this->execute( $javascript );
			Debug::Text( 'waitForUI result: ' . var_export( $js_retval, true ), __FILE__, __LINE__, __METHOD__, 10 );

			if ( isset( $js_retval ) && $js_retval == true ) {
				return true;
			} else {
				$ui_ready_status = $this->execute( [ 'script' => 'if ( typeof Global != "undefined" ) { return Global.getUIReadyStatus(); } else { return null; }', 'args' => [] ] );
				$pending_promises = $this->execute( [ 'script' => 'if ( typeof TTPromise != "undefined" ) { return TTPromise.isPendingPromises(); } else { return null; }', 'args' => [] ] );
				Debug::Text( '  waitForUI UIReadyStatus: ' . var_export( $ui_ready_status, true ) . ' Pending Promises: ' . var_export( $pending_promises, true ), __FILE__, __LINE__, __METHOD__, 10 );

				if ( $_self_->waitForUIInitCompleteLoops > 10 ) {
					//trigger checking promises again to workaround selenium bug where they resolve without firing function
					$this->execute( [ 'script' => 'TTPromise.wait()', 'args' => [] ] );
					Debug::Text( '  waitForUI Triggering TTPromise.wait()... Loops: ' . $this->waitForUIInitCompleteLoops, __FILE__, __LINE__, __METHOD__, 10 );
				}
				$_self_->waitForUIInitCompleteLoops++;

				return null;
			}
		}, 60000 ); //Wait for up to 60 seconds.
	}

	function setUnitTestMode( $username ) {
		$sf = TTnew( 'StationFactory' ); /** @var StationFactory $sf */
		$slf = TTnew( 'StationListFactory' ); /** @var StationListFactory $slf */

		$slf->getByStationId( 'UNITTEST' );
		if ( $slf->getRecordCount() == 0 ) {
			$ulf = TTNew( 'UserListFactory' ); /** @var UserListFactory $ulf */
			$ulf->getByUserName( $username );
			if ( $ulf->getRecordCount() > 0 ) {
				$sf->setCompany( $ulf->getCurrent()->getCompany() );
				$sf->setStatus( 20 );
				$sf->setType( 10 );
				$sf->setDescription( 'Unit Testing Rig' );
				$sf->setStation( 'UNITTEST' );
				$sf->setSource( 'ANY' );
				$sf->setBranchSelectionType( 10 ); //enabled all
				$sf->setDepartmentSelectionType( 10 ); //enabled all
				$sf->setGroupSelectionType( 10 ); //enabled all
				if ( $sf->isValid() ) {
					$sf->Save();
				}
			} else {
				Debug::Text( 'username not found in db', __FILE__, __LINE__, __METHOD__, 10 );
			}
		} else {
			Debug::Text( 'station exists', __FILE__, __LINE__, __METHOD__, 10 );
		}

		//run necessary js for unit tests
		$javascript = [ 'script' => 'Global.setUnitTestMode();', 'args' => [] ];
		$this->execute( $javascript );

		//$path = Environment::getCookieBaseURL();
		//set the same sessionid for all tests
		//$javascript = [ 'script' => "$.cookie( 'StationID', 'UNITTESTS', {expires: 30, path: '$path'} );", 'args' => [] ];
		$javascript = [ 'script' => "Global.setStationID('UNITTEST')", 'args' => [] ];
		$this->execute( $javascript );
	}

	function isThere( $css_selector ) {
		$result = $this->elements( $this->using( 'css selector' )->value( $css_selector ) );
		if ( count( $result ) > 0 ) {
			foreach ( $result as $el ) {
				if ( $el->displayed() && $el->enabled() ) {
					return true;
					break;
				}
			}
		}

		return false;
	}

	function waitThenClick( $selector ) {
		Debug::Text( 'Attempting to click Selector: ' . $selector, __FILE__, __LINE__, __METHOD__, 10 );

		$javascript = [ 'script' => "return $('#overlay.overlay').length", 'args' => [] ];
		$overlay_shown = $this->execute( $javascript );
		if ( $overlay_shown > 0 ) {
			Debug::Text( '  Overlay status check: ' . $overlay_shown, __FILE__, __LINE__, __METHOD__, 10 );
			sleep( 1 );
			$this->waitThenClick( $selector );

			return;
		}

		try {
			if ( ( substr( $selector, 0, 1 ) == '#' && strstr( $selector, ' ' ) == false ) || strstr( $selector, 'menu:' ) == true ) {
				//need to do this because of malformed ids in the top menu causing wating by selector to fail.
				$id = substr( $selector, 1, strlen( $selector ) );
				Debug::Text( '  Waiting on ID: ' . $id, __FILE__, __LINE__, __METHOD__, 10 );
				$this->waitUntilById( $id, 10000 );
				Debug::Text( '  Clicking ID: ' . $id, __FILE__, __LINE__, __METHOD__, 10 );
				$this->byId( $id )->click();
			} else {
				Debug::Text( '  Waiting on selector: ' . $selector, __FILE__, __LINE__, __METHOD__, 10 );
				$this->waitUntilByCssSelector( $selector, 10000 );
				Debug::Text( '  Clicking selector: ' . $selector, __FILE__, __LINE__, __METHOD__, 10 );
				$el = $this->byCssSelector( $selector );
				//Debug::Text( '     Element about to be clicked: Enabled: '. $el->enabled() .' Displayed: '. $el->displayed(), __FILE__, __LINE__, __METHOD__, 10 );
				$el->click();
			}
		} catch ( Exception $e ) {
			$this->takeScreenshot( $this->screenshot_path . DIRECTORY_SEPARATOR . 'waitThenClickException.png', true );
			Debug::Text( 'Click failed on: ' . $selector . ' Screenshot path: ' . $this->screenshot_path, __FILE__, __LINE__, __METHOD__, 10 );
			//$javascript = array('script' => "$('" . $selector . "').click()", 'args' => array());
			//$this->execute( $javascript );
			throw new Exception( $selector . ' - ' . $e->getMessage() );
		}

		Debug::Text( 'Done: ' . $selector, __FILE__, __LINE__, __METHOD__, 10 );
	}

	function getArrayBySelector( $css_selector ) {
		Debug::Text( 'Getting array by selector: ' . $css_selector, __FILE__, __LINE__, __METHOD__, 10 );
		//$this->waitUntilByCssSelector( $css_selector,10000 );

		//http://stackoverflow.com/questions/16637806/select-all-matching-elements-in-phpunit-selenium-2-test-case
		$retval = $this->elements(
				$this->using( 'css selector' )->value( $css_selector )
		);

		if ( isset( $retval ) ) {
			Debug::Text( count( $retval ) . ' RESULTS FOR: ' . $css_selector, __FILE__, __LINE__, __METHOD__, 10 );
//			foreach ( $retval as $el ) {
//				Debug::Text( '  Element: ' . $el->attribute( 'ref' ) .' ID: '. $el->attribute( 'id' ), __FILE__, __LINE__, __METHOD__, 10 );
//			}

			return $retval;
		}

		return [];
	}

	function clickCancel( $menu_id = false ) {
		//TODO look at this
		if ( $menu_id !== false ) {
			$selector = '#' . $menu_id . ' .tticon-cancel_black_24dp';
			$this->waitThenClick( $selector );
			Debug::Text( 'Clicking Cancel [' . $selector . ']', __FILE__, __LINE__, __METHOD__, 10 );
		} else {
			//TODO: Fix this branchs
			$javascript = [ 'script' => "$('#topContainer .ribbon .ribbon-tab-out-side:visible #cancelIcon').click()", 'args' => [] ];
			$this->execute( $javascript );
			Debug::Arr( $javascript, 'Executing  cancelclick with js', __FILE__, __LINE__, __METHOD__, 10 );
		}
	}

	function clickMainMenuItem( $menu_id ) {
		//Only works for main menu horizontal default mode currently.
		$this->waitUntilByCssSelector( '#main-menu' );
		$this->byCssSelector( '#main-menu' )->byId( $menu_id )->click();
		Debug::Text( 'Opening Main Menu Item [' . $menu_id . ']', __FILE__, __LINE__, __METHOD__, 10 );
	}

	function clickProfileMenuItem( $profile_menu_id ) {
		$this->waitForUIInitComplete();

		$this->waitUntilById( 'profile-button' );
		$this->byId( 'profile-button' )->click();

		$this->waitUntilById( $profile_menu_id );
		$this->byId( $profile_menu_id )->click();

		$this->waitForUIInitComplete();
	}
}