<?php

declare(strict_types=1);

namespace tests;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

/*
Not yet covered tests:
- loginGetLocale
- loginGetHeaderColor
- loginGetPages
- loginGetPageLookupList
- loginPageAccessAllowed
- loginGetEuid
*/

/**
 * Test class for ACL\Login
 * @coversDefaultClass \CoreLibs\ACL\Login
 * @testdox \CoreLibs\ACL\Login method tests
 */
final class CoreLibsACLLoginTest extends TestCase
{
	private static \CoreLibs\DB\IO $db;
	private static \CoreLibs\Logging\Logging $log;

	private static string $edit_access_cuid;
	private static string $edit_user_cuid;
	private static string $edit_user_cuuid;

	/**
	 * start DB conneciton, setup DB, etc
	 *
	 * @return void
	 */
	public static function setUpBeforeClass(): void
	{
		if (!extension_loaded('pgsql')) {
			self::markTestSkipped(
				'The PgSQL extension is not available for ACL\Login test.'
			);
		}
		// database/CoreLibsACLLogin_database_create_data.sql
		$load_sql_file = __DIR__
			. DIRECTORY_SEPARATOR
			. 'database/CoreLibsACLLogin_database_create_data.sql';
		if (!is_file($load_sql_file)) {
			self::markTestIncomplete(
				'Missing ACL\Login database load SQL file'
			);
		}
		// ASSUME that DB is on Port 5432 and use DEFAULT in path postgresql
		$db_user = "corelibs_acl_login_test";
		$db_password = "corelibs_acl_login_test";
		$db_name = "corelibs_acl_login_test";
		$db_host = "localhost";
		// run the drop restore script before connecting to the database
		// check exit, if not null then abort
		$command = __DIR__ . DIRECTORY_SEPARATOR
			. "CoreLibsACLLogin_database_prepare.sh "
			. "$load_sql_file "
			. "$db_user "
			. "$db_name "
			. "$db_host ";
		exec($command, $ouput, $result);
		if ($result != 0 || !empty($output[0])) {
			self::markTestIncomplete(
				'Drop/Create ACL\Login database failed with: ' . $result
			);
		}

		// logger is always needed
		// define basic connection set valid and one invalid
		self::$log = new \CoreLibs\Logging\Logging([
			// 'log_folder' => __DIR__ . DIRECTORY_SEPARATOR . 'log',
			'log_folder' => DIRECTORY_SEPARATOR . 'tmp',
			'log_file_id' => 'CoreLibs-ACL-Login-Test',
		]);
		// test database we need to connect do, if not possible this test is skipped
		self::$db = new \CoreLibs\DB\IO(
			[
				'db_name' => $db_name,
				'db_user' => $db_user,
				'db_pass' => $db_password,
				'db_host' => $db_host,
				'db_port' => 5432,
				'db_schema' => 'public',
				'db_type' => 'pgsql',
				'db_encoding' => '',
				'db_ssl' => 'allow', // allow, disable, require, prefer
				'db_debug' => true,
			],
			self::$log
		);
		// ALWAYS drop DB and RECREATE DB
		// dropdb -U corelibs_acl_login_test -h localhost corelibs_acl_login_test
		// createdb -U corelibs_acl_login_test -O corelibs_acl_login_test -E utf8 corelibs_acl_login_test;
		if (!self::$db->dbGetConnectionStatus()) {
			self::markTestSkipped(
				'Cannot connect to valid Test DB for ACL\Login test.'
			);
		}
		// check that edit_user table exist, I assume if this one does,
		// the rest does too
		if (!self::$db->dbShowTableMetaData('edit_user') !== false) {
			self::markTestIncomplete(
				'Cannot find edit_user table in ACL\Login database for testing'
			);
		}
		// always disable max query calls
		self::$db->dbSetMaxQueryCall(-1);
		// insert additional content for testing (locked user, etc)
		$queries = [
			<<<SQL
			INSERT INTO edit_access_data (
				edit_access_id, name, value, enabled
			) VALUES (
				(SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'),
				'test', 'value', 1
			)
			SQL
		];
		foreach ($queries as $query) {
			self::$db->dbExec($query);
		}
		// read edit access cuid, edit user cuid and edit user cuuid
		$row = self::$db->dbReturnRowParams(
			"SELECT cuid FROM edit_access WHERE uid = $1",
			["AdminAccess"]
		);
		self::$edit_access_cuid = $row['cuid'] ?? '';
		if (empty(self::$edit_access_cuid)) {
			self::markTestIncomplete(
				'Cannot read edit access cuid for "AdminAccess".'
			);
		}
		$row = self::$db->dbReturnRowParams(
			"SELECT cuid, cuuid FROM edit_user WHERE username = $1",
			["admin"]
		);
		self::$edit_user_cuid = $row['cuid'] ?? '';
		self::$edit_user_cuuid = $row['cuuid'] ?? '';
		if (empty(self::$edit_user_cuid) || empty(self::$edit_user_cuuid)) {
			self::markTestIncomplete(
				'Cannot read edit user cuid or cuuid for "admin".'
			);
		}

		// define mandatory constant
		// must set
		// TARGET
		define('TARGET', 'test');
		// LOGIN DB SCHEMA

		// SHOULD SET
		// DEFAULT_ACL_LEVEL (d80)

		// OPT:
		// LOGOUT_TARGET
		// PASSWORD_CHANGE
		// PASSWORD_FORGOT

		// LANG:
		// SITE_LOCALE
		// DEFAULT_LOCALE
		// CONTENT_PATH (domain)

		$_SESSION = [];
		global $_SESSION;
	}

	/**
	 * close db
	 *
	 * @return void
	 */
	public static function tearDownAfterClass(): void
	{
		if (self::$db->dbGetConnectionStatus()) {
			self::$db->dbClose();
		}
	}

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function loginProvider(): array
	{
		// 0[mock]   : mock settings/override flag settings
		// 1[get]    : get array IN
		// 2[post]   : post array IN
		//             login_login, login_username, login_password, login_logout
		//             change_password, pw_username, pw_old_password, pw_new_password,
		//             pw_new_password_confirm
		// 3[session]: override session set
		// 4[error]  : expected error code, 0 for all ok, 100 for login page view
		//             note that 1000 (no db), 2000 (no session), 3000 (options set error)
		//             must be tested too
		//             <1000 info, >=1000 critical error
		// 5[return] : expected return array, eg login_error code,
		//             or other info data to match
		$tests = [
			'load, no login' => [
				// error code, only for exceptions
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[],
				[],
				100,
				[
					'login_error' => 0,
					'error_string' => 'Success: <b>No error</b>',
					'error_string_text' => 'Success: No error',
				],
			],
			'load, no login, ajax flag' => [
				// error code, only for exceptions
				[
					'page_name' => 'edit_users.php',
					// for ajax
					'ajax_page' => true,
					'ajax_test_type' => 'parameter',
				],
				[],
				[],
				[],
				100,
				[
					'login_error' => 0,
					'error_string' => 'Success: <b>No error</b>',
					'error_string_text' => 'Success: No error',
					// for ajax
					'action' => 'login',
					'ajax_get_count' => 0,
					'ajax_post_count' => 5,
					'ajax_post_action' => 'login',
				],
			],
			'load, no login, ajax globals' => [
				// error code, only for exceptions
				[
					'page_name' => 'edit_users.php',
					// for ajax
					'ajax_page' => true,
					'ajax_test_type' => 'globals',
				],
				[],
				[],
				[],
				100,
				[
					'login_error' => 0,
					'error_string' => 'Success: <b>No error</b>',
					'error_string_text' => 'Success: No error',
					// for ajax
					'action' => 'login',
					'ajax_get_count' => 0,
					'ajax_post_count' => 5,
					'ajax_post_action' => 'login',
				],
			],
			'load, session eucuuid set only, php error' => [
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[],
				[
					'LOGIN_EUID' => 1,
					'LOGIN_EUCUID' => 'abc',
					'LOGIN_EUCUUID' => '1233456-1234-1234-1234-123456789012',
				],
				2,
				[],
			],
			'load, session eucuuid set, all set' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
				],
				[],
				[],
				[
					'LOGIN_EUID' => 1,
					'LOGIN_EUCUID' => 'abc',
					'LOGIN_EUCUUID' => 'SET_EUCUUID_IN_TEST',
					'LOGIN_USER_NAME' => '',
					'LOGIN_GROUP_NAME' => '',
					'LOGIN_ADMIN' => 1,
					'LOGIN_GROUP_ACL_LEVEL' => -1,
					'LOGIN_PAGES_ACL_LEVEL' => [],
					'LOGIN_USER_ACL_LEVEL' => -1,
					'LOGIN_USER_ADDITIONAL_ACL' => [],
					'LOGIN_GROUP_ADDITIONAL_ACL' => [],
					'LOGIN_UNIT_UID' => [
						'AdminAccess' => '123456789012',
					],
					'LOGIN_UNIT' => [
						'123456789012' => [
							'id' => 1,
							'acl_level' => 80,
							'name' => 'Admin Access',
							'uid' => 'AdminAccess',
							'level' => -1,
							'default' => 0,
							'data' => [
								'test' => 'value',
							],
							'additional_acl' => []
						],
					],
					// 'LOGIN_UNIT_DEFAULT' => '',
					// 'LOGIN_DEFAULT_ACL_LIST' => [],
				],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				],
			],
			// login: all missing
			'login: failed: all missing' => [
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => '',
					'login_password' => '',
				],
				[],
				100,
				[
					'login_error' => 102,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Please enter username and password</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Please enter username and password'
				]
			],
			// login: missing username
			'login: failed: missing username' => [
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => '',
					'login_password' => 'abc',
				],
				[],
				100,
				[
					'login_error' => 102,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Please enter username and password</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Please enter username and password'
				]
			],
			// login: missing password
			'login: failed: missing password' => [
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'abc',
					'login_password' => '',
				],
				[],
				100,
				[
					'login_error' => 102,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Please enter username and password</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Please enter username and password'
				]
			],
			// login: user not found
			'login: failed: user not found' => [
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'abc',
					'login_password' => 'abc',
				],
				[],
				100,
				[
					'login_error' => 1010,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Wrong Username or Password</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Wrong Username or Password'
				]
			],
			// login: invalid password
			//           9999: not valid password encoding
			//           1013: normal password failed
			//           1012: plain password check failed
			'login: failed: invalid password' => [
				[
					'page_name' => 'edit_users.php',
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'abc',
				],
				[],
				100,
				[
					// default password is plain text
					'login_error' => 1012,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Wrong Username or Password</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Wrong Username or Password'
				]
			],
			// login: ok (but deleted)
			'login: ok -> failed: but deleted' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_deleted' => true
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 106,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User is deleted</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User is deleted'
				]
			],
			// login: ok (but not enabled)
			'login: ok -> failed: but not enabled' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_enabled' => true
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 104,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User not enabled</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User not enabled'
				]
			],
			// login: ok (but locked)
			'login: ok -> failed: but locked' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_locked' => true
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 105,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User is locked</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User is locked'
				]
			],
			// login: make user get locked strict
			'login: ok -> failed: get locked, strict' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_get_locked' => true,
					'max_login_error_count' => 2,
					'test_locked_strict' => true,
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				0,
				[
					'lock_run_login_error' => 1012,
					'login_error' => 105,
				]
			],
			// login ok, but in locked period (until)
			'login: ok -> failed: but locked period (until:on)' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_locked_period_until' => 'on'
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 107,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User in locked via date period</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User in locked via date period'
				]
			],
			// login ok, but in locked period (until)
			'login: ok, but locked period (until:off)' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_locked_period_until' => 'off'
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// login ok, but in locked period (after)
			'login: ok -> failed: but locked period (after:on)' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_locked_period_after' => 'on'
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 107,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User in locked via date period</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User in locked via date period'
				]
			],
			// login ok, but in locked period (until, after)
			'login: ok -> failed:, but locked period (until:on, after:on)' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_locked_period_until' => 'on',
					'test_locked_period_after' => 'on'
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 107,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User in locked via date period</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User in locked via date period'
				]
			],
			// login ok, but login user id locked
			'login: ok -> failed:, but loginUserId locked' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_locked' => true
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				100,
				[
					'login_error' => 108,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - User is locked via Login User ID</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - User is locked via Login User ID'
				]
			],
			// login: ok
			'login: ok' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
				],
				[],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// login check via _GET loginUserId
			'login: ok, _GET loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// login check via _POST loginUserId
			'login: ok, _POST loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// login: wrong GET loginUserId
			'login: ok, illegal chars in _GET loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG'
				],
				[
					'loginUserId' => '123$%_/45678¥\-^9~~0$AB&CDEFG',
				],
				[],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			'login: not matching _GET loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG'
				],
				[
					'loginUserId' => 'ABC'
				],
				[],
				[],
				100,
				[
					'login_error' => 1010,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Wrong Username or Password</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Wrong Username or Password'
				]
			],
			// login ok with both _GET loginUserId and _POST login username/password
			'login: ok, _GET loginUserId AND login post user data' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// login with invalid loginUserId but valid username/password
			'login: ok, bad _GET loginUserId AND good login post user data' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => 'ABCS',
				],
				[
					'login_login' => 'Login',
					'login_username' => 'admin',
					'login_password' => 'admin',
				],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// loginUserId check with revalidate on/off
			'login: ok -> failed:, but revalidate trigger, _GET loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_revalidate_after' => 'on',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				100,
				[
					'login_error' => 1101,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Login User ID must be validated</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Login User ID must be validated'
				]
			],
			// loginUserId check with revalidate on/off
			'login: ok, revalidate set (outside), _GET loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_revalidate_after' => 'off',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// loginUserId check with active time from only
			'login: ok -> failed:, _GET loginUserId, but outside valid (from:on) ' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_valid_from' => 'on',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				100,
				[
					'login_error' => 1102,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Login User ID is outside valid date range</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Login User ID is outside valid date range'
				]
			],
			// loginUserId check with inactive time from only
			'login: ok, _GET loginUserId, but outside valid (from:off) ' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_valid_from' => 'off',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			],
			// loginUserId check with active time until only
			'login: ok -> failed:, _GET loginUserId, but outside valid (until:on) ' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_valid_until' => 'on',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				100,
				[
					'login_error' => 1102,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Login User ID is outside valid date range</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Login User ID is outside valid date range'
				]
			],
			// loginUserId check with active time from/until
			'login: ok -> failed:, _GET loginUserId, but outside valid (from:on,until:on) ' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_valid_from' => 'on',
					'test_login_user_id_valid_until' => 'on',
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
				],
				[
					'loginUserId' => '1234567890ABCDEFG',
				],
				[],
				[],
				100,
				[
					'login_error' => 1102,
					'error_string' => '<span style="color: red;">Fatal Error:</span> '
						. '<b>Login Failed - Login User ID is outside valid date range</b>',
					'error_string_text' => 'Fatal Error: '
						. 'Login Failed - Login User ID is outside valid date range'
				]
			],
			// TODO: Test that if we have n day check with login, that after login we can use parameter login again
			'login: ok -> failed -> ok:, _GET loginUserId, but must revalidate, normal login, _GET loginUserId' => [
				[
					'page_name' => 'edit_users.php',
					'edit_access_id' => 1,
					'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'edit_access_uid' => 'AdminAccess',
					'edit_access_data' => 'test',
					'base_access' => 'list',
					'page_access' => 'list',
					'test_login_user_id_revalidate_reset' => true,
					'test_login_user_id' => true,
					'test_username' => 'admin',
					'loginUserId' => '1234567890ABCDEFG',
					// this error is thrown on first login round
					'login_error' => 1101,
					// get post as set sub arrays
					'get' => [
						'loginUserId' => '1234567890ABCDEFG',
					],
					'post' => [
						'login_login' => 'Login',
						'login_username' => 'admin',
						'login_password' => 'admin',
					],
				],
				// all empty get, post, session
				[],
				[],
				[],
				0,
				[
					'login_error' => 0,
					'admin_flag' => true,
					'check_access' => true,
					'check_access_id' => 1,
					'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST',
					'check_access_data' => 'value',
					'base_access' => true,
					'page_access' => true,
				]
			]
			//
			// other:
			// login check edit access id of ID not null and not in array
			// login OK, but during action user gets disabled/deleted/etc
		];

		return $tests;
	}

	/**
	 * main test for acl login
	 *
	 * @dataProvider loginProvider
	 * @testdox ACL\Login Class tests [$_dataName]
	 *
	 * @param  array<string,mixed>  $mock_settings
	 * @param  array<string,string> $get
	 * @param  array<string,string> $post
	 * @param  array<string,mixed>  $session
	 * @param  int                  $error
	 * @param  array<string,mixed>  $expected
	 * @return void
	 */
	public function testACLLoginFlow(
		array $mock_settings,
		array $get,
		array $post,
		array $session,
		int $error,
		array $expected
	): void {
		// reset session
		$_SESSION = [];
		// reset post & get
		$_GET = [];
		$_POST = [];
		// reset global ajax page call
		unset($GLOBALS['AJAX_PAGE']);
		// init session (as MOCK)
		/** @var \CoreLibs\Create\Session&MockObject */
		$session_mock = $this->createPartialMock(
			\CoreLibs\Create\Session::class,
			['getSessionId', 'checkActiveSession', 'sessionDestroy']
		);
		$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST12');
		$session_mock->method('checkActiveSession')->willReturn(true);
		$session_mock->method('sessionDestroy')->will(
			$this->returnCallback(function () {
				global $_SESSION;
				$_SESSION = [];
				return true;
			})
		);

		// set _GET data
		foreach ($get as $get_var => $get_value) {
			$_GET[$get_var] = $get_value;
		}

		// set _POST data
		foreach ($post as $post_var => $post_value) {
			$_POST[$post_var] = $post_value;
		}

		// set ingoing session cuuid if requested
		if (isset($session['LOGIN_EUCUUID']) && $session['LOGIN_EUCUUID'] == 'SET_EUCUUID_IN_TEST') {
			$session['LOGIN_EUCUUID'] = self::$edit_user_cuuid;
		}

		// set _SESSION data
		foreach ($session as $session_var => $session_value) {
			$_SESSION[$session_var] = $session_value;
		}
		/** @var \CoreLibs\ACL\Login&MockObject */
		$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
			->setConstructorArgs([
				self::$db,
				self::$log,
				$session_mock,
				[
					'auto_login' => false,
					'default_acl_level' => 80,
					'logout_target' => '',
					'site_locale' => 'en_US.UTF-8',
					'site_domain' => 'admin',
					'site_encoding' => 'UTF-8',
					'locale_path' => __DIR__ . DIRECTORY_SEPARATOR
						. 'includes' . DIRECTORY_SEPARATOR
						. 'locale' . DIRECTORY_SEPARATOR,
				]
			])
			->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin', 'loginEnhanceHttpSecurity'])
			->getMock();
		$login_mock->expects($this->any())
			->method('loginTerminate')
			->will(
				$this->returnCallback(function ($message, $code) {
					throw new \Exception('', $code);
				})
			);
		$login_mock->expects($this->any())
			->method('loginReadPageName')
			// set from mock settings, or empty if not set at all
			->willReturn($mock_settings['page_name'] ?? '');
		// do not echo out any string here
		$login_mock->expects($this->any())
			->method('loginPrintLogin')
			->willReturnCallback(function () {
			});
		$login_mock->expects($this->any())
			->method('loginEnhanceHttpSecurity')
			->willReturnCallback(function () {
			});

		// if mock_settings: enabled OFF
		// run DB update and set off
		if (!empty($mock_settings['test_enabled'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET enabled = 0 WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (!empty($mock_settings['test_deleted'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET deleted = 1 WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (!empty($mock_settings['test_login_user_id_locked'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET login_user_id_locked = 1 WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (
			!empty($mock_settings['test_locked_period_until']) ||
			!empty($mock_settings['test_locked_period_after'])
		) {
			$q_sub = '';
			if (!empty($mock_settings['test_locked_period_until'])) {
				if ($mock_settings['test_locked_period_until'] == 'on') {
					$q_sub .= "lock_until = NOW() + '1 day'::interval ";
				} elseif ($mock_settings['test_locked_period_until'] == 'off') {
					$q_sub .= "lock_until = NOW() - '1 day'::interval ";
				}
			}
			if (!empty($mock_settings['test_locked_period_after'])) {
				if (!empty($q_sub)) {
					$q_sub .= ", ";
				}
				if ($mock_settings['test_locked_period_after'] == 'on') {
					$q_sub .= "lock_after = NOW() - '1 day'::interval ";
				} elseif ($mock_settings['test_locked_period_after'] == 'off') {
					$q_sub .= "lock_after = NOW() + '1 day'::interval ";
				}
			}
			self::$db->dbExec(
				"UPDATE edit_user SET "
					. $q_sub
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		// test locked already
		if (!empty($mock_settings['test_locked'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET locked = 1 WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		// test get locked
		if (!empty($mock_settings['test_get_locked'])) {
			// enable strict if needed
			if (!empty($mock_settings['test_locked_strict'])) {
				self::$db->dbExec(
					"UPDATE edit_user SET strict = 1 WHERE LOWER(username) = "
						. self::$db->dbEscapeLiteral($post['login_username'])
				);
			}
			// reset any previous login error counts
			self::$db->dbExec(
				"UPDATE edit_user "
					. "SET login_error_count = 0, login_error_date_last = NULL, "
					. "login_error_date_first = NULL "
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
			// check the max login error count and try login until one time before
			// on each run, check that lock count is matching
			// next run (run test) should then fail with user locked IF strict,
			// else fail as normal login
			$login_mock->loginSetMaxLoginErrorCount($mock_settings['max_login_error_count']);
			// temporary wrong password
			$_POST['login_password'] = 'wrong';
			for (
				$run = 1, $max_run = $login_mock->loginGetMaxLoginErrorCount();
				$run <= $max_run;
				$run++
			) {
				try {
					$login_mock->loginMainCall();
				} catch (\Exception $e) {
					// print 'Expected error code: ' . $e->getCode()
					// . ', M:' . $e->getMessage()
					// . ', L:' . $e->getLine()
					// . ', E: ' . $login_mock->loginGetLastErrorCode()
					// . "\n";
					$this->assertEquals(
						$expected['lock_run_login_error'],
						$login_mock->loginGetLastErrorCode(),
						'Assert login error code, exit on lock run'
					);
				}
				// check user error count
				$res = self::$db->dbReturnRow(
					"SELECT login_error_count FROM edit_user "
						. "WHERE LOWER(username) = "
						. self::$db->dbEscapeLiteral($post['login_username'])
				);
				$this->assertEquals(
					$res['login_error_count'],
					$run,
					'Assert equal login error count'
				);
			}
			// set correct password next locked login
			$_POST['login_password'] = $post['login_password'];
		}
		if (!empty($mock_settings['test_login_user_id'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET "
				. "login_user_id = "
				. self::$db->dbEscapeLiteral($mock_settings['loginUserId'])
				. " "
				. "WHERE LOWER(username) = "
				. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
		}
		if (!empty($mock_settings['test_login_user_id_revalidate_after'])) {
			$q_sub = '';
			if ($mock_settings['test_login_user_id_revalidate_after'] == 'on') {
				$q_sub = "login_user_id_last_revalidate = NOW() - '1 day'::interval, "
					. "login_user_id_revalidate_after = '1 day'::interval ";
			} else {
				$q_sub = "login_user_id_last_revalidate = NOW(), "
					. "login_user_id_revalidate_after = '6 day'::interval ";
			}
			self::$db->dbExec(
				"UPDATE edit_user SET "
				. $q_sub
				. "WHERE LOWER(username) = "
				. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
		}
		if (!empty($mock_settings['test_login_user_id_revalidate_reset'])) {
			// init dates data for revalidate frame,
			// set to last revalidate 3 days ago and set revalidate frame to
			// three days
			self::$db->dbExec(
				"UPDATE edit_user SET "
				. "login_user_id_last_revalidate = NOW() - '3 day'::interval, "
				. "login_user_id_revalidate_after = '3 day'::interval "
				. "WHERE LOWER(username) = "
				. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
			$_GET = $mock_settings['get'];
			// login with loginUserId -> fail
			try {
				$login_mock->loginMainCall();
			} catch (\Exception $e) {
				$this->assertEquals(
					$mock_settings['login_error'],
					$login_mock->loginGetLastErrorCode(),
					'loginUserId reset 1: Assert first loginUserId run failes'
				);
			}
			$_GET = [];
			// login with username and password -> reset -> ok
			// set _POST data
			$_POST = $mock_settings['post'];
			try {
				$login_mock->loginMainCall();
				$this->assertEquals(
					0,
					$login_mock->loginGetLastErrorCode(),
					'loginUserId reset 2: Assert username/password login is successful'
				);
			} catch (\Exception $e) {
				// if we end up here we have an issue
				$this->assertTrue(
					false,
					'loginUserId reset 2: FAILED successful login'
				);
			}
			$_POST = [];
			// logut and run normal login with loginUserId
			$_GET = $mock_settings['get'];
		}
		if (
			!empty($mock_settings['test_login_user_id_valid_from']) ||
			!empty($mock_settings['test_login_user_id_valid_until'])
		) {
			$q_sub = '';
			if (!empty($mock_settings['test_login_user_id_valid_from'])) {
				if ($mock_settings['test_login_user_id_valid_from'] == 'on') {
					$q_sub .= "login_user_id_valid_from = NOW() + '1 day'::interval ";
				} elseif ($mock_settings['test_login_user_id_valid_from'] == 'off') {
					$q_sub .= "login_user_id_valid_from = NOW() - '1 day'::interval ";
				}
			}
			if (!empty($mock_settings['test_login_user_id_valid_until'])) {
				if (!empty($q_sub)) {
					$q_sub .= ", ";
				}
				if ($mock_settings['test_login_user_id_valid_until'] == 'on') {
					$q_sub .= "login_user_id_valid_until = NOW() - '1 day'::interval ";
				} elseif ($mock_settings['test_login_user_id_valid_until'] == 'off') {
					$q_sub .= "login_user_id_valid_until = NOW() + '1 day'::interval ";
				}
			}
			self::$db->dbExec(
				"UPDATE edit_user SET "
					. $q_sub
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
		}

		// run test
		try {
			// preset, we cannot set that in the provider
			if (
				isset($expected['check_access_cuid']) &&
				$expected['check_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST'
			) {
				$expected['check_access_cuid'] = self::$edit_access_cuid;
			}
			if (
				isset($mock_settings['edit_access_cuid']) &&
				$mock_settings['edit_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST'
			) {
				$mock_settings['edit_access_cuid'] = self::$edit_access_cuid;
			}
			// if ajax call
			// check if parameter, or globals (old type)
			// else normal call
			if (
				!empty($mock_settings['ajax_test_type']) &&
				$mock_settings['ajax_test_type'] == 'parameter'
			) {
				$login_mock->loginMainCall($mock_settings['ajax_page']);
			} elseif (
				!empty($mock_settings['ajax_test_type']) &&
				$mock_settings['ajax_test_type'] == 'globals'
			) {
				$GLOBALS['AJAX_PAGE'] = $mock_settings['ajax_page'];
				$login_mock->loginMainCall();
			} else {
				$login_mock->loginMainCall();
			}
			// on ok, do post login check based on expected return
			// - loginGetLastErrorCode
			$this->assertEquals(
				$expected['login_error'],
				$login_mock->loginGetLastErrorCode(),
				'Assert login error code'
			);
			// - loginGetPageName
			$this->assertEquals(
				$mock_settings['page_name'],
				$login_mock->loginGetPageName(),
				'Assert page name'
			);
			// - loginCheckPermissions [duplicated from loginrun]
			$this->assertTrue(
				$login_mock->loginCheckPermissions(),
				'Assert true for login permission ok'
			);
			// - loginCheckAccess [use Base, Page below]
			$this->assertEquals(
				$expected['base_access'],
				$login_mock->loginCheckAccess('base', $mock_settings['base_access']),
				'Assert base access, via main method'
			);
			$this->assertEquals(
				$expected['base_access'],
				$login_mock->loginCheckAccess('page', $mock_settings['base_access']),
				'Assert page access, via main method'
			);
			// - loginCheckAccessBase
			$this->assertEquals(
				$expected['base_access'],
				$login_mock->loginCheckAccessBase($mock_settings['base_access']),
				'Assert base access'
			);
			// - loginCheckAccessPage
			$this->assertEquals(
				$expected['page_access'],
				$login_mock->loginCheckAccessPage($mock_settings['page_access']),
				'Assert page access'
			);
			// - loginCheckEditAccessCuid
			$this->assertEquals(
				$expected['check_access'],
				$login_mock->loginCheckEditAccessCuid($mock_settings['edit_access_cuid']),
				'Assert check access'
			);
			// - loginCheckEditAccessValidCuid
			$this->assertEquals(
				$expected['check_access_cuid'],
				$login_mock->loginCheckEditAccessValidCuid($mock_settings['edit_access_cuid']),
				'Assert check access cuid valid'
			);
			// - loginGetEditAccessCuidFromUid
			$this->assertEquals(
				$expected['check_access_cuid'],
				$login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_uid']),
				'Assert check access uid to cuid valid'
			);
			// - loginGetEditAccessCuidFromId
			$this->assertEquals(
				$expected['check_access_cuid'],
				$login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_id']),
				'Assert check access id to cuid valid'
			);
			// Deprecated
			// - loginCheckEditAccess
			$this->assertEquals(
				$expected['check_access'],
				$login_mock->loginCheckEditAccess($mock_settings['edit_access_id']),
				'Assert check access'
			);
			// - loginCheckEditAccessId
			$this->assertEquals(
				$expected['check_access_id'],
				$login_mock->loginCheckEditAccessId((int)$mock_settings['edit_access_id']),
				'Assert check access id valid'
			);
			// - loginGetEditAccessIdFromUid
			$this->assertEquals(
				$expected['check_access_id'],
				$login_mock->loginGetEditAccessIdFromUid($mock_settings['edit_access_uid']),
				'Assert check access uid to id valid'
			);
			// - loginGetEditAccessData
			$this->assertEquals(
				$expected['check_access_data'],
				$login_mock->loginGetEditAccessData(
					$mock_settings['edit_access_uid'],
					$mock_settings['edit_access_data']
				),
				'Assert check access id data value valid'
			);
			// - loginIsAdmin
			$this->assertEquals(
				$expected['admin_flag'],
				$login_mock->loginIsAdmin(),
				'Assert admin flag set'
			);
			// - loginGetAcl
			$this->assertIsArray(
				$login_mock->loginGetAcl(),
				'Assert get acl is array'
			);
			// if loginUserId in _GET or _POST check that it is set
			if (!empty($get['loginUserId']) || !empty($post['loginUserId'])) {
				$this->assertNotEmpty(
					$login_mock->loginGetLoginUserId(),
					'Assert loginUserId is set'
				);
			}
			// TODO: detail match of ACL array (loginGetAcl)

			// .. end with: loginLogoutUser
			// _POST['login_logout'] = 'lgogout
			// $login_mock->loginMainCall();
			// - loginCheckPermissions
			// - loginGetPermissionOkay
		} catch (\Exception $e) {
			/* print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/"
				. ($expected['login_error'] ?? 0) . "\n";
			print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n";
			print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n";
			print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n";
			print "POST exit: " . ($_POST['login_exit'] ?? '{0}') . "\n"; */
			// if this is 100, then we do further error checks
			if (
				$e->getCode() == 100 ||
				!empty($_POST['login_exit']) && $_POST['login_exit'] == 100
			) {
				$this->assertEquals(
					$expected['login_error'],
					$login_mock->loginGetLastErrorCode(),
					'Assert login error code, exit'
				);
				// - loginGetErrorMsg
				$this->assertEquals(
					$expected['error_string'],
					$login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode()),
					'Assert error string, html'
				);
				$this->assertEquals(
					$expected['error_string_text'],
					$login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode(), true),
					'Assert error string, text'
				);
				// - loginGetLoginHTML
				$this->assertStringContainsString(
					'<html lang="',
					$login_mock->loginGetLoginHTML(),
					'Assert login html string exits'
				);
				// check that login script has above error message
				if (!empty($login_mock->loginGetLastErrorCode())) {
					$this->assertStringContainsString(
						$login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode()),
						$login_mock->loginGetLoginHTML(),
						'Assert login error string exits'
					);
				} else {
					$this->assertStringNotContainsString(
						$login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode()),
						$login_mock->loginGetLoginHTML(),
						'Assert login error string does not exit'
					);
				}
				// for ajax type, test post return values
				if ($login_mock->loginGetAjaxFlag()) {
					$this->assertCount(
						$expected['ajax_get_count'],
						$_GET,
						'Assert ajax error _GET is valid count'
					);
					// post has only action, login_exit, login_error,
					// login_error_text and login_html
					// 5 entries
					$this->assertCount(
						$expected['ajax_post_count'],
						$_POST,
						'Assert ajax error _POST is valid count'
					);
					// test post entries
					$this->assertEquals(
						$expected['ajax_post_action'],
						$_POST['action'],
						'Assert ajax _POST action'
					);
					$this->assertEquals(
						$login_mock->loginGetLastErrorCode(),
						$_POST['login_error'],
						'Assert ajax _POST error'
					);
					$this->assertEquals(
						$login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode(), true),
						$_POST['login_error_text'],
						'Assert ajax _POST error text'
					);
					// html login basic check only, content is the same as when
					// read from loginGetLoginHTML()
					$this->assertStringContainsString(
						'<html lang="',
						$_POST['login_html'],
						'Assert ajax _POST html string exits'
					);
				}
			}
			// print "EXCEPTION: " . print_r($e, true) . "\n";
			$this->assertEquals(
				!$login_mock->loginGetAjaxFlag() ?
					$e->getCode() :
					$_POST['login_exit'] ?? 0,
				$error,
				'Expected error code: ' . $e->getCode()
					. ', ' . $e->getMessage()
					. ', ' . $e->getLine()
			);
		}

		// if _POST login set check this is matching
		if (!empty($post['login_login'])) {
			$this->assertTrue(
				$login_mock->loginActionRun(),
				'Assert that post login_login was pressed'
			);
		}

		// always check, even on error or not set
		if (!$login_mock->loginGetLoginUserIdUnclean()) {
			$this->assertEquals(
				$_GET['loginUserId'] ?? $_POST['loginUserId'] ?? '',
				$login_mock->loginGetLoginUserId(),
				'Assert loginUserId matches'
			);
		} else {
			$this->assertTrue(
				$login_mock->loginGetLoginUserIdUnclean(),
				'Assert loginUserId is unclear'
			);
			$this->assertNotEquals(
				$_GET['loginUserId'] ?? $_POST['loginUserId'] ?? '',
				$login_mock->loginGetLoginUserId(),
				'Assert loginUserId does not matche _GET/_POST'
			);
		}
		// check get/post login user id
		$this->assertEquals(
			(!empty($_GET['loginUserId']) ?
				'GET' :
				(!empty($_POST['loginUserId']) ?
					'POST' : '')
			),
			$login_mock->loginGetLoginUserIdSource(),
			'Assert loginUserId source matches'
		);

		// enable user again if flag set
		if (!empty($mock_settings['test_enabled'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET enabled = 1 "
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (!empty($mock_settings['test_deleted'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET deleted = 0 WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (!empty($mock_settings['test_login_user_id_locked'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET login_user_id_locked = 0 WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (
			!empty($mock_settings['test_locked_period_until']) ||
			!empty($mock_settings['test_locked_period_after'])
		) {
			self::$db->dbExec(
				"UPDATE edit_user SET "
					. "lock_until = NULL, "
					. "lock_after = NULL "
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		// reset lock flag
		if (!empty($mock_settings['test_locked'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET locked = 0 "
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		// rest the get locked flow
		if (!empty($mock_settings['test_get_locked'])) {
			self::$db->dbExec(
				"UPDATE edit_user "
					. "SET login_error_count = 0, login_error_date_last = NULL, "
					. "login_error_date_first = NULL, locked = 0, strict = 0 "
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($post['login_username'])
			);
		}
		if (!empty($mock_settings['test_login_user_id'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET "
				. "login_user_id = NULL, "
				// below to rows are automatcially reset
				. "login_user_id_set_date = NULL, "
				. "login_user_id_last_revalidate = NULL "
				. "WHERE LOWER(username) = "
				. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
		}
		if (!empty($mock_settings['test_login_user_id_revalidate_after'])) {
			self::$db->dbExec(
				"UPDATE edit_user SET "
				. "login_user_id_last_revalidate = NULL, "
				. "login_user_id_revalidate_after = NULL "
				. "WHERE LOWER(username) = "
				. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
		}
		if (
			!empty($mock_settings['test_login_user_id_valid_from']) ||
			!empty($mock_settings['test_login_user_id_valid_until'])
		) {
			self::$db->dbExec(
				"UPDATE edit_user SET "
					. "login_user_id_valid_from = NULL, "
					. "login_user_id_valid_until = NULL "
					. "WHERE LOWER(username) = "
					. self::$db->dbEscapeLiteral($mock_settings['test_username'])
			);
		}
	}

	// - loginGetAclList (null, invalid,)

	public function aclListProvider(): array
	{
		// 0: level (int|null)
		// 2: type (string), null for skip (if 0 = null)
		// 1: acl return from level (array)
		// 2: level number to return (must match 0)
		return [
			'null, get full list' => [
				null,
				null,
				[
					0 => [
						'type' => 'none',
						'name' => 'No Access',
					],
					10 => [
						'type' => 'list',
						'name' => 'List',
					],
					20 => [
						'type' => 'read',
						'name' => 'Read',
					],
					30 => [
						'type' => 'mod_trans',
						'name' => 'Translator',
					],
					40 => [
						'type' => 'mod',
						'name' => 'Modify',
					],
					60 => [
						'type' => 'write',
						'name' => 'Create/Write',
					],
					80 => [
						'type' => 'del',
						'name' => 'Delete',
					],
					90 => [
						'type' => 'siteadmin',
						'name' => 'Site Admin',
					],
					100 => [
						'type' => 'admin',
						'name' => 'Admin',
					],
				],
				null
			],
			'valid, search' => [
				20,
				'read',
				[
					'type' => 'read',
					'name' => 'Read'
				],
				20
			],
			'invalid search' => [
				12,
				'foo',
				[],
				false,
			]
		];
	}

	/**
	 * Undocumented function
	 *
	 * @dataProvider aclListProvider()
	 * @testdox ACL\Login list if $level and $type exepcted level is $expected_level [$_dataName]
	 *
	 * @param  int|null      $level
	 * @param  string|null   $type
	 * @param  array         $expected_list
	 * @param  int|null|bool $expected_level
	 * @return void
	 */
	public function testAclLoginList(
		?int $level,
		?string $type,
		array $expected_list,
		$expected_level
	): void {
		$_SESSION = [];
		// init session (as MOCK)
		/** @var \CoreLibs\Create\Session&MockObject */
		$session_mock = $this->createPartialMock(
			\CoreLibs\Create\Session::class,
			['getSessionId', 'checkActiveSession', 'sessionDestroy']
		);
		$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
		$session_mock->method('checkActiveSession')->willReturn(true);
		$session_mock->method('sessionDestroy')->will(
			$this->returnCallback(function () {
				global $_SESSION;
				$_SESSION = [];
				return true;
			})
		);
		/** @var \CoreLibs\ACL\Login&MockObject */
		$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
			->setConstructorArgs([
				self::$db,
				self::$log,
				$session_mock,
				[
					'auto_login' => false,
					'default_acl_level' => 80,
					'logout_target' => '',
					'site_locale' => 'en_US.UTF-8',
					'site_domain' => 'admin',
					'site_encoding' => 'UTF-8',
					'locale_path' => __DIR__ . DIRECTORY_SEPARATOR
						. 'includes' . DIRECTORY_SEPARATOR
						. 'locale' . DIRECTORY_SEPARATOR,
				]
			])
			->onlyMethods(['loginTerminate'])
			->getMock();
		$login_mock->expects($this->any())
			->method('loginTerminate')
			->will(
				$this->returnCallback(function ($message, $code) {
					throw new \Exception('', $code);
				})
			);

		$list = $login_mock->loginGetAclList($level);
		$this->assertIsArray(
			$list,
			'assert get acl list is array'
		);
		$this->assertEquals(
			$expected_list,
			$list
		);
		if ($type !== null) {
			$this->assertEquals(
				$expected_level,
				$login_mock->loginGetAclListFromType($type),
				'assert type is level'
			);
			// only back assert if found
			if (isset($list['type'])) {
				$this->assertEquals(
					$list['type'],
					$type,
					'assert level read type is type'
				);
			}
		}
	}

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function minPasswordCheckProvider(): array
	{
		// 0: set length
		// 1: expected return from set
		// 2: expected set length
		return [
			'set new length' => [
				12,
				true,
				12,
			],
			'set new length, too short' => [
				5,
				false,
				9
			],
			'set new length, too long' => [
				500,
				false,
				9
			]
		];
	}

	/**
	 * check setting minimum password length
	 *
	 * @covers ::loginSetPasswordMinLength
	 * @covers ::loginGetPasswordLenght
	 * @dataProvider minPasswordCheckProvider()
	 * @testdox ACL\Login password min length set $input is $expected_return and matches $expected [$_dataName]
	 *
	 * @param  int  $input
	 * @param  bool $expected_return
	 * @param  int  $expected
	 * @return void
	 */
	public function testACLLoginPasswordMinLenght(int $input, bool $expected_return, int $expected): void
	{
		$_SESSION = [];
		// init session (as MOCK)
		/** @var \CoreLibs\Create\Session&MockObject */
		$session_mock = $this->createPartialMock(
			\CoreLibs\Create\Session::class,
			['getSessionId', 'checkActiveSession', 'sessionDestroy']
		);
		$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
		$session_mock->method('checkActiveSession')->willReturn(true);
		$session_mock->method('sessionDestroy')->will(
			$this->returnCallback(function () {
				global $_SESSION;
				$_SESSION = [];
				return true;
			})
		);
		/** @var \CoreLibs\ACL\Login&MockObject */
		$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
			->setConstructorArgs([
				self::$db,
				self::$log,
				$session_mock,
				[
					'auto_login' => false,
					'default_acl_level' => 80,
					'logout_target' => '',
					'site_locale' => 'en_US.UTF-8',
					'site_domain' => 'admin',
					'site_encoding' => 'UTF-8',
					'locale_path' => __DIR__ . DIRECTORY_SEPARATOR
						. 'includes' . DIRECTORY_SEPARATOR
						. 'locale' . DIRECTORY_SEPARATOR,
				]
			])
			->onlyMethods(['loginTerminate'])
			->getMock();
		$login_mock->expects($this->any())
			->method('loginTerminate')
			->will(
				$this->returnCallback(function ($message, $code) {
					throw new \Exception('', $code);
				})
			);

		// set new min password length
		$this->assertEquals(
			$expected_return,
			$login_mock->loginSetPasswordMinLength($input),
			'assert bool set password min length'
		);
		// check value
		$this->assertEquals(
			$expected,
			$login_mock->loginGetPasswordLenght('min'),
			'assert get password min length'
		);
	}

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function getPasswordLengthProvider(): array
	{
		return [
			'min' => ['min'],
			'lower' => ['lower'],
			'max' => ['max'],
			'upper' => ['upper'],
			'minimum_length' => ['minimum_length'],
			'min_length' => ['min_length'],
			'length' => ['length'],
		];
	}

	/**
	 * check all possible readable password length params
	 *
	 * @covers ::loginGetPasswordLenght
	 * @dataProvider getPasswordLengthProvider()
	 * @testdox ACL\Login get password length $input [$_dataName]
	 *
	 * @param  string $input
	 * @return void
	 */
	public function testACLLoginGetPasswordLength(string $input): void
	{
		$_SESSION = [];
		// init session (as MOCK)
		/** @var \CoreLibs\Create\Session&MockObject */
		$session_mock = $this->createPartialMock(
			\CoreLibs\Create\Session::class,
			['getSessionId', 'checkActiveSession', 'sessionDestroy']
		);
		$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
		$session_mock->method('checkActiveSession')->willReturn(true);
		$session_mock->method('sessionDestroy')->will(
			$this->returnCallback(function () {
				global $_SESSION;
				$_SESSION = [];
				return true;
			})
		);
		/** @var \CoreLibs\ACL\Login&MockObject */
		$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
			->setConstructorArgs([
				self::$db,
				self::$log,
				$session_mock,
				[
					'auto_login' => false,
					'default_acl_level' => 80,
					'logout_target' => '',
					'site_locale' => 'en_US.UTF-8',
					'site_domain' => 'admin',
					'site_encoding' => 'UTF-8',
					'locale_path' => __DIR__ . DIRECTORY_SEPARATOR
						. 'includes' . DIRECTORY_SEPARATOR
						. 'locale' . DIRECTORY_SEPARATOR,
				]
			])
			->onlyMethods(['loginTerminate'])
			->getMock();
		$login_mock->expects($this->any())
			->method('loginTerminate')
			->will(
				$this->returnCallback(function ($message, $code) {
					throw new \Exception('', $code);
				})
			);

		$this->assertMatchesRegularExpression(
			"/^\d+$/",
			(string)$login_mock->loginGetPasswordLenght($input)
		);
	}

	/**
	 * Undocumented function
	 *
	 * @return array
	 */
	public function loginMaxErrorProvider(): array
	{
		return [
			'set valid max failed login' => [
				10,
				true,
				10
			],
			'set valid unlimted' => [
				-1,
				true,
				-1
			],
			'set invalid 0' => [
				0,
				false,
				-1
			],
			'set invalid negative' => [
				-5,
				false,
				-1
			],
		];
	}

	/**
	 * Undocumented function
	 *
	 * @covers ::loginSetMaxLoginErrorCount
	 * @covers ::loginGetMaxLoginErrorCount
	 * @dataProvider loginMaxErrorProvider()
	 * @testdox ACL\Login failed login set/get $input is $expected_return and matches $expected [$_dataName]
	 *
	 * @param  int  $input
	 * @param  bool $expected_return
	 * @param  int  $expected
	 * @return void
	 */
	public function testACLLoginErrorCount(int $input, bool $expected_return, int $expected): void
	{
		$_SESSION = [];
		// init session (as MOCK)
		/** @var \CoreLibs\Create\Session&MockObject */
		$session_mock = $this->createPartialMock(
			\CoreLibs\Create\Session::class,
			['getSessionId', 'checkActiveSession', 'sessionDestroy']
		);
		$session_mock->method('getSessionId')->willReturn('ACLLOGINTEST34');
		$session_mock->method('checkActiveSession')->willReturn(true);
		$session_mock->method('sessionDestroy')->will(
			$this->returnCallback(function () {
				global $_SESSION;
				$_SESSION = [];
				return true;
			})
		);
		/** @var \CoreLibs\ACL\Login&MockObject */
		$login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class)
			->setConstructorArgs([
				self::$db,
				self::$log,
				$session_mock,
				[
					'auto_login' => false,
					'default_acl_level' => 80,
					'logout_target' => '',
					'site_locale' => 'en_US.UTF-8',
					'site_domain' => 'admin',
					'site_encoding' => 'UTF-8',
					'locale_path' => __DIR__ . DIRECTORY_SEPARATOR
						. 'includes' . DIRECTORY_SEPARATOR
						. 'locale' . DIRECTORY_SEPARATOR,
				]
			])
			->onlyMethods(['loginTerminate'])
			->getMock();
		$login_mock->expects($this->any())
			->method('loginTerminate')
			->will(
				$this->returnCallback(function ($message, $code) {
					throw new \Exception('', $code);
				})
			);

		// set new min password length
		$this->assertEquals(
			$expected_return,
			$login_mock->loginSetMaxLoginErrorCount($input),
			'assert bool set max login errors'
		);
		// check value
		$this->assertEquals(
			$expected,
			$login_mock->loginGetMaxLoginErrorCount(),
			'assert get max login errors'
		);
	}
}

// __END__
