<?php
/* Copyright (C) 2010-2014 	Laurent Destailleur  	<eldy@users.sourceforge.net>
 * Copyright (C) 2015	   	Juanjo Menent			<jmenent@2byte.es>
 * Copyright (C) 2023 		Alexandre Janniaux   	<alexandre.janniaux@gmail.com>
 * Copyright (C) 2025		MDW						<mdeweerd@users.noreply.github.com>
 * Copyright (C) 2025       Frédéric France         <frederic.france@free.fr>
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 * or see https://www.gnu.org/
 */

/**
 *      \file       test/phpunit/FunctionsLibTest.php
 *		\ingroup    test
 *      \brief      PHPUnit test
 *		\remarks	To run this script as CLI:  phpunit filename.php
 */

global $conf,$user,$langs,$db,$mysoc;
//define('TEST_DB_FORCE_TYPE','mysql');	// This is to force using mysql driver
//require_once 'PHPUnit/Autoload.php';
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
require_once dirname(__FILE__).'/../../htdocs/core/lib/date.lib.php';
require_once dirname(__FILE__).'/../../htdocs/product/class/product.class.php';
require_once dirname(__FILE__).'/CommonClassTest.class.php';

if (! defined('NOREQUIREUSER')) {
	define('NOREQUIREUSER', '1');
}
if (! defined('NOREQUIREDB')) {
	define('NOREQUIREDB', '1');
}
if (! defined('NOREQUIRESOC')) {
	define('NOREQUIRESOC', '1');
}
if (! defined('NOREQUIRETRAN')) {
	define('NOREQUIRETRAN', '1');
}
if (! defined('NOCSRFCHECK')) {
	define('NOCSRFCHECK', '1');
}
if (! defined('NOTOKENRENEWAL')) {
	define('NOTOKENRENEWAL', '1');
}
if (! defined('NOREQUIREMENU')) {
	define('NOREQUIREMENU', '1'); // If there is no menu to show
}
if (! defined('NOREQUIREHTML')) {
	define('NOREQUIREHTML', '1'); // If we don't need to load the html.form.class.php
}
if (! defined('NOREQUIREAJAX')) {
	define('NOREQUIREAJAX', '1');
}
if (! defined("NOLOGIN")) {
	define("NOLOGIN", '1');       // If this page is public (can be called outside logged session)
}

print "\n".$langs->trans("CurrentTimeZone").' : '.getServerTimeZoneString();
print "\n".$langs->trans("CurrentHour").' : '.dol_print_date(dol_now('gmt'), 'dayhour', 'tzserver');
print "\n";


/**
 * Class for PHPUnit tests
 *
 * @backupGlobals disabled
 * @backupStaticAttributes enabled
 * @remarks	backupGlobals must be disabled to have db,conf,user and lang not erased.
 */
class FunctionsLibTest extends CommonClassTest
{
	/**
	 * setUpBeforeClass
	 *
	 * @return void
	 */
	public static function setUpBeforeClass(): void
	{
		global $conf,$user,$langs,$db;
		//$db->begin();	// This is to have all actions inside a transaction even if test launched without suite.

		if (! function_exists('mb_substr')) {
			print "\n".__METHOD__." function mb_substr must be enabled.\n";
			die(1);
		}

		if (getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT') != 5) {
			print "\n".__METHOD__." bad setup for number of digits for unit amount. Must be 5 for this test.\n";
			die(1);
		}

		if (getDolGlobalInt('MAIN_MAX_DECIMALS_TOT') != 2) {
			print "\n".__METHOD__." bad setup for number of digits for unit amount. Must be 2 for this test.\n";
			die(1);
		}

		print __METHOD__."\n";
	}


	/**
	 * testDolCheckFilters
	 *
	 * @return boolean
	 */
	public function testDolCheckFilters()
	{
		global $conf, $langs, $db;

		// A sql with global parenthesis at level 2
		$error = '';
		$parenthesislevel = 0;
		$sql = '(( ... (a:=:1) .éééé. (b:=:1) ... ))';
		$result = dolCheckFilters($sql, $error, $parenthesislevel);
		$this->assertEquals(2, $parenthesislevel);
		$this->assertTrue($result);

		// A sql with global parenthesis at level 2
		$error = '';
		$parenthesislevel = 0;
		$sql = '(((((a:=:1) ... ) .éééé.. (b:=:1) ..) ... ))';
		$result = dolCheckFilters($sql, $error, $parenthesislevel);
		$this->assertEquals(2, $parenthesislevel);
		$this->assertTrue($result);

		// A sql with global parenthesis at level 2
		$error = '';
		$parenthesislevel = 0;
		$sql = '((... (((a:=:1) ... ( .éééé.. (b:=:1) ..)))))';
		$result = dolCheckFilters($sql, $error, $parenthesislevel);
		$this->assertEquals(2, $parenthesislevel);
		$this->assertTrue($result);

		// A sql with global parenthesis at level 0
		$error = '';
		$parenthesislevel = 0;
		$sql = '(a:=:1) ... (b:=:1) éééé ...';
		$result = dolCheckFilters($sql, $error, $parenthesislevel);
		$this->assertEquals(0, $parenthesislevel);
		$this->assertTrue($result);

		// A sql with bad balance
		$error = '';
		$parenthesislevel = 0;
		$sql = '((((a:=:1) ... (b:=:1) éééé ..))';
		$result = dolCheckFilters($sql, $error, $parenthesislevel);
		$this->assertEquals(0, $parenthesislevel);
		$this->assertFalse($result);

		// A sql with bad balance
		$error = '';
		$parenthesislevel = 0;
		$sql = '(((a:=:1) ... (b:=:1) éééé ..)))';
		$result = dolCheckFilters($sql, $error, $parenthesislevel);
		$this->assertEquals(0, $parenthesislevel);
		$this->assertFalse($result);

		return true;
	}


	/**
	 * testDolForgeExplodeAnd
	 *
	 * @return boolean
	 */
	public function testDolForgeExplodeAnd()
	{
		$tmp = dolForgeExplodeAnd('');
		$this->assertEquals(0, count($tmp));

		$tmp = dolForgeExplodeAnd('(a:=:1)');
		$this->assertEquals('(a:=:1)', $tmp[0]);

		$tmp = dolForgeExplodeAnd('(a:=:1) AND (b:=:2)');
		$this->assertEquals('(a:=:1)', $tmp[0]);
		$this->assertEquals('(b:=:2)', $tmp[1]);

		$tmp = dolForgeExplodeAnd('(a:=:1) AND ((b:=:2) OR (c:=:3))');
		$this->assertEquals('(a:=:1)', $tmp[0]);
		$this->assertEquals('((b:=:2) OR (c:=:3))', $tmp[1]);

		$tmp = dolForgeExplodeAnd('(a:=:1) AND (b:=:2) OR (c:=:3)');
		$this->assertEquals('(a:=:1)', $tmp[0]);
		$this->assertEquals('(b:=:2) OR (c:=:3)', $tmp[1]);

		$tmp = dolForgeExplodeAnd('(a:=:1) OR (b:=:2) AND (c:=:3)');
		$this->assertEquals('(a:=:1) OR (b:=:2)', $tmp[0]);
		$this->assertEquals('(c:=:3)', $tmp[1]);

		$tmp = dolForgeExplodeAnd('(a:=:1) OR ((b:=:2) AND (c:=:3))');
		$this->assertEquals('(a:=:1) OR ((b:=:2) AND (c:=:3))', $tmp[0]);

		$tmp = dolForgeExplodeAnd('((y:=:1) AND (p:=:8))');
		$this->assertEquals('(y:=:1)', $tmp[0]);
		$this->assertEquals('(p:=:8)', $tmp[1]);

		return true;
	}

	/**
	 * testDolForgeSQLCriteriaCallback
	 *
	 * @return boolean
	 */
	public function testDolForgeSQLCriteriaCallback()
	{
		global $conf, $langs, $db;

		// Test using like
		$filter = "(lastname:like:'%aaa%') OR (firstname:like:'%bbb%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((lastname LIKE '%aaa%') OR (firstname LIKE '%bbb%'))", $sql);

		// Test on NOW
		$filter = "(client:!=:8) AND (datefin:>=:'__NOW__')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertStringContainsStringIgnoringCase(" AND ((client <> 8) AND (datefin >= '", $sql);

		// An attempt for SQL injection
		$filter = 'if(now()=sysdate()%2Csleep(6)%2C0)';
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals('Filter error - Bad syntax of the search string', $sql);

		// A real search string
		$filter = '(((statut:=:1) or (entity:in:__AAA__)) and (abc:<:2.0) and (abc:!=:1.23))';
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(' AND ((((statut = 1) or (entity IN (__AAA__))) and (abc < 2) and (abc <> 1.23)))', $sql);

		// A real search string
		$filter = "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.date_creation:<:'2016-01-01 12:30:00') or (t.nature:is:NULL)";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((t.ref LIKE 'SO-%') or (t.date_creation < '20160101') or (t.date_creation < '2016-01-01 12:30:00') or (t.nature IS NULL))", $sql);

		// A real search string
		$filter = "(t.fieldstring:=:'aaa ttt')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((t.fieldstring = 'aaa ttt'))", $sql);

		// Check that parenthesis are NOT allowed inside the last operand. Very important.
		$filter = "(t.fieldint:=:(1,2))";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals("Filter error - Bad syntax of the search string", $sql);

		// Check that ' is escaped into the last operand
		$filter = "(t.fieldstring:=:'aaa'ttt')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);

		if ($db->type == 'mysqli') {
			$this->assertEquals(" AND ((t.fieldstring = 'aaa\'ttt'))", $sql);	// with mysql
		} else {
			$this->assertEquals(" AND ((t.fieldstring = 'aaa''ttt'))", $sql);	// with pgsql
		}

		$filter = "(t.fk_soc:IN:1,2)";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((t.fk_soc IN (1,2)))", $sql);

		$filter = "(t.fk_soc:IN:'1','2=b')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((t.fk_soc IN ('1','2=b')))", $sql);

		// Test use of forbiddenstring
		$errorstr = '';
		$filter = "(t.fk_soc:IN:'1','2=b')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr, 0, 0, 0, array('aaa'));
		$this->assertEquals(" AND ((t.fk_soc IN ('1','2=b')))", $sql);

		$filter = "(t.api_key:IN:'1','2=b')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr, 0, 0, 0, array('aaa'));
		$this->assertEquals(" AND (1=1)", $sql);

		$filter = "(t.fk_soc:IN:'1','2=b')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr, 0, 0, 0, array('fk_soc'));
		$this->assertEquals(" AND (1=1)", $sql);

		// Test that a forbidden field with 2 levels of prefix (dbprefix.table.field) is also forbidden (protection against CVE-2026-89012 bypass)
		$filter = "(dolibarr.u.pass_crypted:like:'ZZZZ%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr);
		$this->assertEquals(" AND (1=1)", $sql);

		$filter = "(DOLIBARR.U.API_KEY:like:'ZZZZ%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr);
		$this->assertEquals(" AND (1=1)", $sql);

		$filter = "(mydb.t.pass_temp:like:'ZZZZ%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr);
		$this->assertEquals(" AND (1=1)", $sql);

		$filter = "(u.pass_temp:like:'ZZZZ%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr);
		$this->assertEquals(" AND (1=1)", $sql);

		// Test that any other field with 2 levels of prefix is neutralized too (no db prefix existence oracle, no SQL error)
		$filter = "(dolibarr.t.ref:like:'SO-%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr);
		$this->assertEquals(" AND (1=1)", $sql);

		// Test that a field forbidden by the caller is also forbidden with 2 levels of prefix
		$filter = "(dolibarr.u.salary:like:'1%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr, 0, 0, 0, array('salary'));
		$this->assertEquals(" AND (1=1)", $sql);

		// Test that valid filters with a single level of prefix still work
		$filter = "(u.api_key:like:'ZZZZ%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr, 0, 0, 0, array('ref'));
		$this->assertEquals(" AND (1=1)", $sql);

		$filter = "(t.ref:like:'SO-%')";
		$sql = forgeSQLFromUniversalSearchCriteria($filter, $errorstr, 0, 0, 0, array('salary'));
		$this->assertEquals(" AND ((t.ref LIKE 'SO-%'))", $sql);


		global $dolibarr_allow_unsecured_select_in_extrafields_filter;

		// If $dolibarr_allow_unsecured_select_in_extrafields_filter is set
		$dolibarr_allow_unsecured_select_in_extrafields_filter = 1;

		$filter = "(t.fk_soc:IN:SELECT rowid FROM llx_societe WHERE fournisseur = 1)";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((t.fk_soc IN (SELECT rowid FROM llx_societe WHERE fournisseur = 1)))", $sql);

		// If $dolibarr_allow_unsecured_select_in_extrafields_filter is unset (default)
		$dolibarr_allow_unsecured_select_in_extrafields_filter = 0;

		$filter = "(t.fk_soc:IN:SELECT rowid FROM llx_societe WHERE fournisseur = 1)";
		$sql = forgeSQLFromUniversalSearchCriteria($filter);
		$this->assertEquals(" AND ((t.fk_soc IN (SELECTrowidFROMllx_societeWHEREfournisseur1)))", $sql);

		return true;
	}


	/**
	 * testDolClone
	 *
	 * @return void
	 */
	public function testDolClone()
	{
		global $db;

		$newproduct1 = new Product($db);
		$newproduct1->initAsSpecimen();

		print __METHOD__." this->savdb has type ".(is_resource($db->db) ? get_resource_type($db->db) : (is_object($db->db) ? 'object' : 'unknown'))."\n";
		print __METHOD__." newproduct1->db->db has type ".(is_resource($newproduct1->db->db) ? get_resource_type($newproduct1->db->db) : (is_object($newproduct1->db->db) ? 'object' : 'unknown'))."\n";
		$this->assertEquals($db->connected, 1, 'Savdb is connected');
		$this->assertNotNull($newproduct1->db->db, 'newproduct1->db is not null');

		$newproductcloned1 = dol_clone($newproduct1);

		print __METHOD__." this->savdb has type ".(is_resource($db->db) ? get_resource_type($db->db) : (is_object($db->db) ? 'object' : 'unknown'))."\n";
		print __METHOD__." newproduct1->db->db has type ".(is_resource($newproduct1->db->db) ? get_resource_type($newproduct1->db->db) : (is_object($newproduct1->db->db) ? 'object' : 'unknown'))."\n";
		$this->assertEquals($db->connected, 1, 'Savdb is connected');
		$this->assertNotNull($newproduct1->db->db, 'newproduct1->db is not null');
	}

	/**
	 * testDolCloneInArray
	 *
	 * @return void
	 */
	public function testDolCloneInArray()
	{
		global $db;

		$newproduct1 = new Product($db);
		$newproduct1->initAsSpecimen();

		$newproductclonedinarray1 = dol_clone_in_array($newproduct1);

		print __METHOD__." newproductclonedinarray1[db] must be null\n";
		$this->assertNull((empty($newproductclonedinarray1['db']) ? null : 'defined'), 'newproductclonedinarray1[db] is null');
		$this->assertNotNull($newproduct1->db->db, 'newproduct1->db is not null');
	}

	/**
	 * testNum2Alpha
	 *
	 * @return void
	 */
	public function testNum2Alpha()
	{
		$result = num2Alpha(0);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals($result, 'A', 'Check num2Alpha 0');

		$result = num2Alpha(5);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals($result, 'F', 'Check num2Alpha 5');

		$result = num2Alpha(26);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals($result, 'AA', 'Check num2Alpha 26');
	}

	/**
	 * testIsValidEmail
	 *
	 * @return void
	 */
	public function testIsValidEmail()
	{
		// Nb of line is same than entry text

		$input = "bidon@bademail";
		$result = isValidEmail($input);
		print __METHOD__." result=".$result."\n";
		$this->assertFalse($result, 'Check isValidEmail '.$input);

		$input = "test@yahoo.com";
		$result = isValidEmail($input);
		print __METHOD__." result=".$result."\n";
		$this->assertTrue($result, 'Check isValidEmail '.$input);

		$input = "The name of sender <test@yahoo.com>";
		$result = isValidEmail($input);
		print __METHOD__." result=".$result."\n";
		$this->assertFalse($result, 'Check isValidEmail '.$input);

		$input = "1234.abcdefg@domainame.com.br";
		$result = isValidEmail($input);
		print __METHOD__." result=".$result."\n";
		$this->assertTrue($result, 'Check isValidEmail '.$input);

		$input = "1234.abcdefg@domainame.entreprises";
		$result = isValidEmail($input);
		print __METHOD__." result=".$result."\n";
		$this->assertTrue($result, 'Check isValidEmail '.$input);
	}

	/**
	 * testIsValidMXRecord
	 *
	 * @return void
	 */
	public function testIsValidMXRecord()
	{
		// Nb of line is same than entry text

		$input = "yahoo.com";
		$result = isValidMXRecord($input);
		print __METHOD__." ".$input." result=".$result."\n";
		$this->assertEquals(1, $result);

		$input = "yhaoo.com";
		$result = isValidMXRecord($input);
		print __METHOD__." ".$input." result=".$result."\n";
		$this->assertEquals(0, $result);

		$input = "dolibarr.fr";
		$result = isValidMXRecord($input);
		print __METHOD__." ".$input." result=".$result."\n";
		$this->assertEquals(0, $result);

		$input = "microsoft.com";
		$result = isValidMXRecord($input);
		print __METHOD__." ".$input." result=".$result."\n";
		$this->assertEquals(1, $result);
	}

	/**
	 * testDolGetFirstLineOfText
	 *
	 * @return void
	 */
	public function testDolGetFirstLineOfText()
	{
		// Nb of line is same than entry text

		$input = "aaaa";
		$result = dolGetFirstLineOfText($input);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa", $result);

		$input = "aaaa\nbbbbbbbbbbbb\n";
		$result = dolGetFirstLineOfText($input, 2);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa\nbbbbbbbbbbbb", $result);

		$input = "aaaa<br>bbbbbbbbbbbb<br>";
		$result = dolGetFirstLineOfText($input, 2);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa<br>\nbbbbbbbbbbbb", $result);

		// Nb of line is lower

		$input = "aaaa\nbbbbbbbbbbbb\ncccccc\n";
		$result = dolGetFirstLineOfText($input);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa...", $result);

		$input = "aaaa<br>bbbbbbbbbbbb<br>cccccc<br>";
		$result = dolGetFirstLineOfText($input);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa...", $result);

		$input = "aaaa\nbbbbbbbbbbbb\ncccccc\n";
		$result = dolGetFirstLineOfText($input, 2);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa\nbbbbbbbbbbbb...", $result);

		$input = "aaaa<br>bbbbbbbbbbbb<br>cccccc<br>";
		$result = dolGetFirstLineOfText($input, 2);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa<br>\nbbbbbbbbbbbb...", $result);

		// Nb of line is higher

		$input = "aaaa<br>bbbbbbbbbbbb<br>cccccc";
		$result = dolGetFirstLineOfText($input, 100);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa<br>\nbbbbbbbbbbbb<br>\ncccccc", $result, 'dolGetFirstLineOfText with nb 100 a');

		$input = "aaaa<br>bbbbbbbbbbbb<br>cccccc<br>";
		$result = dolGetFirstLineOfText($input, 100);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa<br>\nbbbbbbbbbbbb<br>\ncccccc", $result, 'dolGetFirstLineOfText with nb 100 b');

		$input = "aaaa<br>bbbbbbbbbbbb<br>cccccc<br>\n";
		$result = dolGetFirstLineOfText($input, 100);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("aaaa<br>\nbbbbbbbbbbbb<br>\ncccccc", $result, 'dolGetFirstLineOfText with nb 100 c');
	}


	/**
	 * testDolBuildPath
	 *
	 * @return void
	 */
	public function testDolBuildPath()
	{
		/*$tmp=dol_buildpath('/google/oauth2callback.php', 0);
		var_dump($tmp);
		*/

		/*$tmp=dol_buildpath('/google/oauth2callback.php', 1);
		var_dump($tmp);
		*/

		$result = dol_buildpath('/google/oauth2callback.php', 2);
		print __METHOD__." dol_buildpath result=".$result."\n";
		$this->assertStringStartsWith('http', $result);

		$result = dol_buildpath('/google/oauth2callback.php', 3);
		print __METHOD__." dol_buildpath result=".$result."\n";
		$this->assertStringStartsWith('http', $result);
	}


	/**
	 * testGetBrowserInfo
	 *
	 * @return void
	 */
	public function testGetBrowserInfo()
	{
		// MSIE 5.0
		$user_agent = 'Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; KITV4 Wanadoo; KITV5 Wanadoo)';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('ie', $tmp['browsername']);
		$this->assertEquals('5.0', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		// Firefox 0.9.1
		$user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.5a) Gecko/20030728 Mozilla Firefox/0.9.1';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('firefox', $tmp['browsername']);
		$this->assertEquals('0.9.1', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		$user_agent = 'Mozilla/3.0 (Windows 98; U) Opera 6.03  [en]';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('opera', $tmp['browsername']);
		$this->assertEquals('6.03', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		$user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1042.0 Safari/535.21';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('chrome', $tmp['browsername']);
		$this->assertEquals('19.0.1042.0', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		$user_agent = 'chrome (Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11)';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('chrome', $tmp['browsername']);
		$this->assertEquals('17.0.963.56', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		$user_agent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('safari', $tmp['browsername']);
		$this->assertEquals('533.21.1', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		// Internet Explorer 11
		$user_agent = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('ie', $tmp['browsername']);
		$this->assertEquals('11.0', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		// Internet Explorer 11 bis
		$user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; NP06; rv:11.0) like Gecko';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('ie', $tmp['browsername']);
		$this->assertEquals('11.0', $tmp['browserversion']);
		$this->assertEmpty($tmp['phone']);
		$this->assertFalse($tmp['tablet']);
		$this->assertEquals('classic', $tmp['layout']);

		// iPad
		$user_agent = 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('safari', $tmp['browsername']);
		$this->assertEquals('8536.25', $tmp['browserversion']);
		$this->assertEquals('ios', $tmp['browseros']);
		$this->assertEquals('tablet', $tmp['layout']);
		$this->assertEquals('iphone', $tmp['phone']);

		// Lynx
		$user_agent = 'Lynx/2.8.8dev.3 libwww‑FM/2.14 SSL‑MM/1.4.1';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('textbrowser', $tmp['browsername']);
		$this->assertEquals('2.8.8', $tmp['browserversion']);
		$this->assertEquals('unknown', $tmp['browseros']);
		$this->assertEquals('classic', $tmp['layout']);

		// W3M
		$user_agent = 'w3m/1.2.3-git123456';
		$tmp = getBrowserInfo($user_agent);
		$this->assertEquals('textbrowser', $tmp['browsername']);
		$this->assertEquals('1.2.3', $tmp['browserversion']);
		$this->assertEquals('unknown', $tmp['browseros']);
		$this->assertEquals('classic', $tmp['layout']);
	}


	/**
	 * testGetLanguageCodeFromCountryCode
	 *
	 * @return void
	 */
	public function testGetLanguageCodeFromCountryCode()
	{
		global $mysoc;

		$language = getLanguageCodeFromCountryCode('US');
		$this->assertEquals('en_US', $language, 'US');

		$language = getLanguageCodeFromCountryCode('ES');
		$this->assertEquals('es_ES', $language, 'ES');

		$language = getLanguageCodeFromCountryCode('CL');
		$this->assertEquals('es_CL', $language, 'CL');

		$language = getLanguageCodeFromCountryCode('CA');
		$this->assertEquals('en_CA', $language, 'CA');

		$language = getLanguageCodeFromCountryCode('MQ');
		$this->assertEquals('fr_CA', $language);

		$language = getLanguageCodeFromCountryCode('FR');
		$this->assertEquals('fr_FR', $language);

		$language = getLanguageCodeFromCountryCode('BE');
		$this->assertEquals('fr_BE', $language);

		$mysoc->country_code = 'FR';
		$language = getLanguageCodeFromCountryCode('CH');
		$this->assertEquals('fr_CH', $language);

		$mysoc->country_code = 'DE';
		$language = getLanguageCodeFromCountryCode('CH');
		$this->assertEquals('de_CH', $language);

		$language = getLanguageCodeFromCountryCode('DE');
		$this->assertEquals('de_DE', $language);

		$language = getLanguageCodeFromCountryCode('SA');
		$this->assertEquals('ar_SA', $language);

		$language = getLanguageCodeFromCountryCode('SE');
		$this->assertEquals('sv_SE', $language);

		$language = getLanguageCodeFromCountryCode('DK');
		$this->assertEquals('da_DK', $language);
	}

	/**
	 * testDolTextIsHtml
	 *
	 * @return void
	 */
	public function testDolTextIsHtml()
	{
		// True
		$input = '<html>xxx</html>';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with html tag');
		$input = '<body>xxx</body>';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with body tag');
		$input = 'xxx <b>yyy</b> zzz';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with b tag');
		$input = 'xxx <u>yyy</u> zzz';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with u tag');
		$input = 'text with <div>some div</div>';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with div tag');
		$input = 'text with HTML &nbsp; entities';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with entities tag');
		$input = 'xxx<br>';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with entities br');
		$input = 'xxx<br >';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with entities br');
		$input = 'xxx<br style="eee">';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with entities br and attributes');
		$input = 'xxx<br style="eee" >';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with entities br and attributes bis');
		$input = '<h2>abc</h2>';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with entities h2');
		$input = '<img id="abc" src="https://xxx.com/aaa/image.png" />';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with img tag');
		$input = '<a class="azerty" href="https://xxx.com/aaa/image.png" />';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with a tag');
		$input = 'This is a text with&nbsp;html spaces';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with a &nbsp;');
		$input = 'This is a text with accent &eacute;';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with a &eacute;');
		$input = '<i class="abc">xxx</i>';
		$after = dol_textishtml($input);
		$this->assertTrue($after, 'Test with i tag and class;');

		// False
		$input = 'xxx < br>';
		$after = dol_textishtml($input);
		$this->assertFalse($after);
		$input = 'xxx <email@email.com>';	// <em> is html, <em... is not
		$after = dol_textishtml($input);
		$this->assertFalse($after);
		$input = 'xxx <brstyle="ee">';
		$after = dol_textishtml($input);
		$this->assertFalse($after);
		$input = 'This is a text with html comments <!-- comment -->';	// we suppose this is not enough to be html content
		$after = dol_textishtml($input);
		$this->assertFalse($after);

		$input = "A text\nwith a link https://aaa?param=abc&amp;param2=def";
		$after = dol_textishtml($input);
		$this->assertFalse($after);
	}


	/**
	 * testDolHtmlCleanLastBr
	 *
	 * @return boolean
	 */
	public function testDolHtmlCleanLastBr()
	{
		$input = "A string\n";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string", $after);

		$input = "A string first\nA string second\n";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string first\nA string second", $after);

		$input = "A string\n\n\n";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string", $after);

		$input = "A string<br>";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string", $after);

		$input = "A string first<br>\nA string second<br>";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string first<br>\nA string second", $after);

		$input = "A string\n<br type=\"_moz\" />\n";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string", $after);

		$input = "A string\n<br><br />\n\n";
		$after = dol_htmlcleanlastbr($input);
		$this->assertEquals("A string", $after);

		return true;
	}

	/**
	 * testDolConcat
	 *
	 * @return boolean
	 */
	public function testDolConcat()
	{
		$text1 = "A string 1";
		$text2 = "A string 2";	// text 1 and 2 are text, concat need only \n
		$after = dol_concatdesc($text1, $text2);
		$this->assertEquals("A string 1\nA string 2", $after);

		$text1 = "A<br>string 1";
		$text2 = "A string 2";	// text 1 is html, concat need <br>\n
		$after = dol_concatdesc($text1, $text2);
		$this->assertEquals("A<br>string 1<br>\nA string 2", $after);

		$text1 = "A string 1";
		$text2 = "A <b>string</b> 2";	// text 2 is html, concat need <br>\n
		$after = dol_concatdesc($text1, $text2);
		$this->assertEquals("A string 1<br>\nA <b>string</b> 2", $after);

		return true;
	}


	/**
	 * testDolStringNoSpecial
	 *
	 * @return boolean
	 */
	public function testDolStringNoSpecial()
	{
		include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';

		$text = "A string with space and special char like ' or ° and more...\n";
		$after = dol_string_nospecial($text, '_', '', '', 0);
		$this->assertEquals("A_string_with_space_and_special_char_like___or___and_more...\n", $after, "testDolStringNoSpecial 1");

		$text = "A string with space and special char like ' or ° and more...\n";
		$after = dol_string_nospecial($text, '_', '', '', 1);
		$this->assertEquals("A string with space and special char like _ or _ and more...\n", $after, "testDolStringNoSpecial 2");

		$text = 'Bahnhofstraße';
		$after = dolEscapeXML(dol_trunc(dol_string_nospecial(dol_string_unaccent($text), ' '), 70, 'right', 'UTF-8', 1));
		$this->assertEquals("Bahnhofstraße", $after, "testDolStringNoSpecial with german char");

		return true;
	}

	/**
	 * testDolStringNohtmltag
	 *
	 * @return boolean
	 */
	public function testDolStringNohtmltag()
	{
		$text = "A\nstring\n\nand more\n";
		$after = dol_string_nohtmltag($text, 0);
		$this->assertEquals("A\nstring\n\nand more", $after, "test1a");

		$text = "A <b>string<b><br>\n<br>\n\nwith html tag<br>\n";
		$after = dol_string_nohtmltag($text, 0);
		$this->assertEquals("A string\n\n\n\n\nwith html tag", $after, 'test2a 2 br and 3 \n give 5 \n');

		$text = "A <b>string<b><br>\n<br>\n\nwith html tag<br>\n";
		$after = dol_string_nohtmltag($text, 1);
		$this->assertEquals("A string with html tag", $after, 'test2b 2 br and 3 \n give 1 space');

		$text = "A <b>string<b><br>\n<br>\n\nwith html tag<br>\n";
		$after = dol_string_nohtmltag($text, 2);
		$this->assertEquals("A string\n\nwith html tag", $after, 'test2c 2 br and 3 \n give 2 \n');

		$text = "A <b>string<b><br>\r\n<br>\r\n\r\nwith html tag<br>\n";
		$after = dol_string_nohtmltag($text, 2);
		$this->assertEquals("A string\n\nwith html tag", $after, 'test2c 2 br and 3 \r\n give 2 \n');

		$text = "A string<br>Another string";
		$after = dol_string_nohtmltag($text, 0);
		$this->assertEquals("A string\nAnother string", $after, "test4");

		$text = "A string<br>Another string";
		$after = dol_string_nohtmltag($text, 1);
		$this->assertEquals("A string Another string", $after, "test5");

		$text = '<a href="/myurl" title="<u>Afficher projet</u>">ABC</a>';
		$after = dol_string_nohtmltag($text, 1);
		$this->assertEquals("ABC", $after, "test6");

		$text = '<a href="/myurl" title="&lt;u&gt;Afficher projet&lt;/u&gt;">DEF</a>';
		$after = dol_string_nohtmltag($text, 1);
		$this->assertEquals("DEF", $after, "test7");

		$text = '<a href="/myurl" title="<u>A title</u>">HIJ</a>';
		$after = dol_string_nohtmltag($text, 0);
		$this->assertEquals("HIJ", $after, "test8");

		$text = "A <b>string<b>\n\nwith html tag and '<' chars<br>\n";
		$after = dol_string_nohtmltag($text, 0);
		$this->assertEquals("A string\n\nwith html tag and '<' chars", $after, "test9");

		$text = "A <b>string<b>\n\nwith tag with < chars<br>\n";
		$after = dol_string_nohtmltag($text, 1);
		$this->assertEquals("A string with tag with < chars", $after, "test10");

		return true;
	}



	/**
	 * testDolHtmlEntitiesBr
	 *
	 * @return boolean
	 */
	public function testDolHtmlEntitiesBr()
	{
		// Text not already HTML

		$input = "A string\nwith a é, &, < and >.";
		$after = dol_htmlentitiesbr($input, 0);    // Add <br> before \n
		$this->assertEquals("A string<br>\nwith a &eacute;, &amp;, &lt; and &gt;.", $after);

		$input = "A string\nwith a é, &, < and >.";
		$after = dol_htmlentitiesbr($input, 1);    // Replace \n with <br>
		$this->assertEquals("A string<br>with a &eacute;, &amp;, &lt; and &gt;.", $after);

		$input = "A string\nwith a é, &, < and >.\n\n";	// With some \n at end that should be cleaned
		$after = dol_htmlentitiesbr($input, 0);    // Add <br> before \n
		$this->assertEquals("A string<br>\nwith a &eacute;, &amp;, &lt; and &gt;.", $after);

		$input = "A string\nwith a é, &, < and >.\n\n";	// With some \n at end that should be cleaned
		$after = dol_htmlentitiesbr($input, 1);    // Replace \n with <br>
		$this->assertEquals("A string<br>with a &eacute;, &amp;, &lt; and &gt;.", $after);

		// Text already HTML, so &,<,> should not be converted

		$input = "A string<br>\nwith a é, &, < and >.";
		$after = dol_htmlentitiesbr($input);
		$this->assertEquals("A string<br>\nwith a &eacute;, &, < and >.", $after);

		$input = "<li>\nA string with a é, &, < and >.</li>\nAnother string";
		$after = dol_htmlentitiesbr($input);
		$this->assertEquals("<li>\nA string with a &eacute;, &, < and >.</li>\nAnother string", $after);

		$input = "A string<br>\nwith a é, &, < and >.<br>";	// With some <br> at end that should be cleaned
		$after = dol_htmlentitiesbr($input);
		$this->assertEquals("A string<br>\nwith a &eacute;, &, < and >.", $after);

		$input = "<li>\nA string with a é, &, < and >.</li>\nAnother string<br>";	// With some <br> at end that should be cleaned
		$after = dol_htmlentitiesbr($input);
		$this->assertEquals("<li>\nA string with a &eacute;, &, < and >.</li>\nAnother string", $after);

		// TODO Add test with param $removelasteolbr = 0

		return true;
	}


	/**
	 * testDolNbOfLinesBis
	 *
	 * @return boolean
	 */
	public function testDolNbOfLinesBis()
	{
		// This is not a html string so nb of lines depends on \n
		$input = "A string\nwith a é, &, < and > and bold tag.\nThird line";
		$after = dol_nboflines_bis($input, 0);
		$this->assertEquals($after, 3);

		// This is a html string so nb of lines depends on <br>
		$input = "A string\nwith a é, &, < and > and <b>bold</b> tag.\nThird line";
		$after = dol_nboflines_bis($input, 0);
		$this->assertEquals($after, 1);

		// This is a html string so nb of lines depends on <br>
		$input = "A string<br>with a é, &, < and > and <b>bold</b> tag.<br>Third line";
		$after = dol_nboflines_bis($input, 0);
		$this->assertEquals($after, 3);

		return true;
	}


	/**
	 * testDolUnaccent
	 *
	 * @return boolean
	 */
	public function testDolUnaccent()
	{
		// Text not already HTML

		$input = "A string\nwith a à ä é è ë ï ü ö ÿ É É, &, < and >."; // the second É is different than the first one
		$after = dol_string_unaccent($input);
		$this->assertEquals("A string\nwith a a a e e e i u o y E E, &, < and >.", $after);
	}


	/**
	 * testDolUtf8Check
	 *
	 * @return void
	 */
	public function testDolUtf8Check()
	{
		// True
		$result = utf8_check('azerty');
		$this->assertTrue($result);

		$file = dirname(__FILE__).'/textutf8.txt';
		$filecontent = file_get_contents($file);
		$result = utf8_check($filecontent);
		$this->assertTrue($result);

		$file = dirname(__FILE__).'/textiso.txt';
		$filecontent = file_get_contents($file);
		$result = utf8_check($filecontent);
		$this->assertFalse($result);
	}

	/**
	 * testDolAsciiCheck
	 *
	 * @return void
	 */
	public function testDolAsciiCheck()
	{
		// True
		$result = ascii_check('azerty');
		$this->assertTrue($result);

		$result = ascii_check('é');
		$this->assertFalse($result);

		$file = dirname(__FILE__).'/textutf8.txt';
		$filecontent = file_get_contents($file);
		$result = ascii_check($filecontent);
		$this->assertFalse($result);
	}

	/**
	 * testDolTrunc
	 *
	 * @return boolean
	 */
	public function testDolTrunc()
	{
		// Default trunc (will add … if truncation truncation or keep last char if only one char)
		$input = "éeéeéeàa";
		$after = dol_trunc($input, 3);
		$this->assertEquals("éeé…", $after, 'Test A1');
		$after = dol_trunc($input, 2);
		$this->assertEquals("ée…", $after, 'Test A2');
		$after = dol_trunc($input, 1);
		$this->assertEquals("é…", $after, 'Test A3');
		$input = "éeée";
		$after = dol_trunc($input, 3);
		$this->assertEquals("éeée", $after, 'Test B1');
		$after = dol_trunc($input, 2);
		$this->assertEquals("ée…", $after, 'Test B2');
		$after = dol_trunc($input, 1);
		$this->assertEquals("é…", $after, 'Test B3');
		$input = "éeée";
		$after = dol_trunc($input, 3);
		$this->assertEquals("éeée", $after, 'Test C1');
		$after = dol_trunc($input, 2);
		$this->assertEquals("ée…", $after, 'Test C2');
		$after = dol_trunc($input, 1);
		$this->assertEquals("é…", $after, 'Test C3');
		$input = "éeé";
		$after = dol_trunc($input, 3);
		$this->assertEquals("éeé", $after, 'Test C');
		$after = dol_trunc($input, 2);
		$this->assertEquals("éeé", $after, 'Test D');
		$after = dol_trunc($input, 1);
		$this->assertEquals("é…", $after, 'Test E');
		// Trunc with no …
		$input = "éeéeéeàa";
		$after = dol_trunc($input, 3, 'right', 'UTF-8', 1);
		$this->assertEquals("éeé", $after, 'Test F');
		$after = dol_trunc($input, 2, 'right', 'UTF-8', 1);
		$this->assertEquals("ée", $after, 'Test G');
		$input = "éeé";
		$after = dol_trunc($input, 3, 'right', 'UTF-8', 1);
		$this->assertEquals("éeé", $after, 'Test H');
		$after = dol_trunc($input, 2, 'right', 'UTF-8', 1);
		$this->assertEquals("ée", $after, 'Test I');
		$after = dol_trunc($input, 1, 'right', 'UTF-8', 1);
		$this->assertEquals("é", $after, 'Test J');
		$input = "éeéeéeàa";
		$after = dol_trunc($input, 4, 'middle');
		$this->assertEquals("ée…àa", $after, 'Test K');

		return true;
	}

	/**
	 * testDolMkTime
	 *
	 * @return	void
	 */
	public function testDolMkTime()
	{
		global $conf;

		$savtz = date_default_timezone_get();

		// Some test for UTC TZ
		date_default_timezone_set('UTC');

		// Check bad hours
		$result = dol_mktime(25, 0, 0, 1, 1, 1970, 1, 1);    // Error (25 hours)
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('', $result);
		$result = dol_mktime(2, 61, 0, 1, 1, 1970, 1, 1);    // Error (61 minutes)
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('', $result);
		$result = dol_mktime(2, 1, 61, 1, 1, 1970, 1, 1);    // Error (61 seconds)
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('', $result);
		$result = dol_mktime(2, 1, 1, 1, 32, 1970, 1, 1);    // Error (day 32)
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('', $result);
		$result = dol_mktime(2, 1, 1, 13, 1, 1970, 1, 1);    // Error (month 13)
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('', $result);

		$result = dol_mktime(2, 1, 1, 1, 1, 1970, 1);    // 1970-01-01 02:01:01 in GMT area -> 7261
		print __METHOD__." result=".$result."\n";
		$this->assertEquals(7261, $result);

		$result = dol_mktime(2, 0, 0, 1, 1, 1970, 0);                // 1970-01-01 02:00:00 = 7200 in local area Europe/Paris = 3600 GMT
		print __METHOD__." result=".$result."\n";
		$tz = getServerTimeZoneInt('winter');                  // +1 in Europe/Paris at this time (this time is winter)
		$this->assertEquals(7200 - ($tz * 3600), $result);        // 7200 if we are at greenwich winter, 7200-($tz*3600) at local winter

		// Some test for local TZ Europe/Paris
		date_default_timezone_set('Europe/Paris');

		// Check that tz for paris in winter is used
		$result = dol_mktime(2, 0, 0, 1, 1, 1970, 'server');         // 1970-01-01 02:00:00 = 7200 in local area Europe/Paris = 3600 GMT
		print __METHOD__." result=".$result."\n";
		$this->assertEquals(3600, $result);        			 // 7200 if we are at greenwich winter, 3600 at Europe/Paris

		// Check that daylight saving time is used
		$result = dol_mktime(2, 0, 0, 6, 1, 2014, 0);         		// 2014-06-01 02:00:00 = 1401588000-3600(location)-3600(daylight) in local area Europe/Paris = 1401588000 GMT
		print __METHOD__." result=".$result."\n";
		$this->assertEquals(1401588000 - 3600 - 3600, $result);  // 1401588000 are at greenwich summer, 1401588000-3600(location)-3600(daylight) at Europe/Paris summer

		date_default_timezone_set($savtz);
	}


	/**
	 * testDolEscapeJs
	 *
	 * @return	void
	 */
	public function testDolEscapeJs()
	{
		$input = "x&<b>#</b>,\"'";    // " will be converted into '
		$result = dol_escape_js($input);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("x&<b>#</b>,\'\'", $result, "Test mode=0");

		$result = dol_escape_js($input, 1);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("x&<b>#</b>,\"\'", $result, "Test mode=1");

		$result = dol_escape_js($input, 2);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals("x&<b>#</b>,\\\"'", $result, "Test mode=2");
	}


	/**
	 * testDolEscapeHtmlTag
	 *
	 * @return	void
	 */
	public function testDolEscapeHtmlTag()
	{
		print __METHOD__."\n";

		$input = 'x&<b>#</b>,"';    // & and " are converted into html entities, <b> are removed
		$result = dol_escape_htmltag($input);
		$this->assertEquals('x&amp;#,&quot;', $result);

		$input = 'x&<b>#</b>,"';    // & and " are converted into html entities, <b> are not removed
		$result = dol_escape_htmltag($input, 1);
		$this->assertEquals('x&amp;&lt;b&gt;#&lt;/b&gt;,&quot;', $result);

		$input = '<img alt="" src="https://github.githubassets.com/assets/GitHub%20Mark-ea2971cee799.png">';    // & and " are converted into html entities, <b> are not removed
		$result = dol_escape_htmltag($input, 1, 1, 'common', 0, 1);
		$this->assertEquals('<img alt="" src="https://github.githubassets.com/assets/GitHub%20Mark-ea2971cee799.png">', $result);

		$input = '<img src="data:image/png;base64, 123/456+789==" style="height: 123px; width:456px">';    // & and " are converted into html entities, <b> are not removed
		$result = dol_escape_htmltag($input, 1, 1, 'common');
		$this->assertEquals('<img src="data:image/png;base64, 123/456+789==" style="height: 123px; width:456px">', $result);

		$input = '<img src="data:image/png;base64, 123/456+789==" style="height: 123px; width:456px">';    // & and " are converted into html entities, <b> are not removed
		$result = dol_escape_htmltag($input, 1);
		$this->assertEquals('&lt;img src=&quot;data:image/png;base64, 123/456+789==&quot; style=&quot;height: 123px; width:456px&quot;&gt;', $result);

		$input = '<img alt="" src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png">';    // & and " are converted into html entities, <b> are not removed
		$result = dol_escape_htmltag($input, 1, 1, 'common', 0, 1);
		$this->assertEquals('<img alt="" src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png">', $result);


		$input = '<div style="float:left; margin-left:0px; margin-right:5px">
		<img id="sigPhoto" src="https://www.domain.com/aaa.png" style="height:65px; width:65px" />
		</div>
		<div style="margin-left:74px"><strong>A text here</strong> and more<br>
		<a href="mailto:abc+def@domain.com" id="sigEmail" style="color:#428BCA;">abc+def@domain.com</a><br>
		<a href="https://www.another-domain.com" id="sigWebsite" style="color:#428BCA;">https://www.another-domain.com</a><br>
		</div>';
		$result = dol_escape_htmltag($input, 1, 1, 'common');
		$this->assertEquals($input, $result);

		// The same plus the tag 'code'
		$input = '<div style="float:left; margin-left:0px; margin-right:5px">
		<img id="sigPhoto" src="https://www.domain.com/aaa.png" style="height:65px; width:65px" />
		</div>
		<div style="margin-left:74px"><strong>A text here</strong> and more<br>
		<a href="mailto:abc+def@domain.com" id="sigEmail" style="color:#428BCA;">abc+def@domain.com</a><br>
		<a href="https://www.another-domain.com" id="sigWebsite" style="color:#428BCA;">https://www.another-domain.com</a><br>
		<code>abc</code>
		</div>';
		$result = dol_escape_htmltag($input, 1, 1, 'common,code');
		$this->assertEquals($input, $result);
	}


	/**
	 * testDolFormatAddress
	 *
	 * @return	void
	 */
	public function testDolFormatAddress()
	{
		global $conf,$user,$langs,$db;

		$conf = $this->savconf;
		$user = $this->savuser;
		$langs = $this->savlangs;
		$db = $this->savdb;

		print __METHOD__."\n";

		$object = new Societe($db);
		$object->initAsSpecimen();

		$object->country_code = 'FR';
		$address = dol_format_address($object);
		$this->assertEquals("21 jump street\n99999 MyTown", $address);

		$object->country_code = 'GB';
		$address = dol_format_address($object);
		$this->assertEquals("21 jump street\nMyTown, MyState\n99999", $address);

		$object->country_code = 'US';
		$address = dol_format_address($object);
		$this->assertEquals("21 jump street\nMyTown, MyState, 99999", $address);

		$object->country_code = 'AU';
		$address = dol_format_address($object);
		$this->assertEquals("21 jump street\nMyTown, MyState, 99999", $address);

		$object->country_code = 'JP';
		$address = dol_format_address($object);
		$this->assertEquals("21 jump street\nMyState, MyTown 99999", $address);
	}


	/**
	 * testDolPrintPhone
	 *
	 * @return	void
	 */
	public function testDolPrintPhone()
	{
		global $conf,$user,$langs,$db;
		$conf = $this->savconf;
		$user = $this->savuser;
		$langs = $this->savlangs;
		$db = $this->savdb;

		print __METHOD__."\n";

		$object = new Societe($db);
		$object->initAsSpecimen();

		$object->country_code = 'FR';
		$phone = dol_print_phone('1234567890', $object->country_code);
		$this->assertEquals('<span class="paddingright">12&nbsp;34&nbsp;56&nbsp;78&nbsp;90</span>', $phone, 'Phone for FR 1');

		$object->country_code = 'FR';
		$phone = dol_print_phone('1234567890', $object->country_code, 0, 0, 0, '');
		$this->assertEquals('<span class="paddingright">1234567890</span>', $phone, 'Phone for FR 2');

		$object->country_code = 'FR';
		$phone = dol_print_phone('1234567890', $object->country_code, 0, 0, 0, ' ');
		$this->assertEquals('<span class="paddingright">12 34 56 78 90</span>', $phone, 'Phone for FR 3');

		$object->country_code = 'CA';
		$phone = dol_print_phone('1234567890', $object->country_code, 0, 0, 0, ' ');
		$this->assertEquals('<span class="paddingright">(123) 456-7890</span>', $phone, 'Phone for CA 1');

		// Every digit must appear exactly once, in order, whatever the country format
		$object->country_code = 'JO';
		$phone = dol_print_phone('+96212345678', $object->country_code, 0, 0, 0, ' ');
		$this->assertEquals('<span class="paddingright">+962 1 234 56 78</span>', $phone, 'Phone for JO 1');

		$object->country_code = 'PE';
		$phone = dol_print_phone('987654321', $object->country_code, 0, 0, 0, ' ');
		$this->assertEquals('<span class="paddingright">987 654 321</span>', $phone, 'Phone for PE 1');

		$object->country_code = 'PE';
		$phone = dol_print_phone('+5111234567', $object->country_code, 0, 0, 0, ' ');
		$this->assertEquals('<span class="paddingright">+511 123 4567</span>', $phone, 'Phone for PE 2');

		$object->country_code = 'PE';
		$phone = dol_print_phone('+51987654321', $object->country_code, 0, 0, 0, ' ');
		$this->assertEquals('<span class="paddingright">+51 987 654 321</span>', $phone, 'Phone for PE 3');
	}


	/**
	 * testImgPicto
	 *
	 * @return	void
	 */
	public function testImgPicto()
	{
		$s = img_picto('title', 'user');
		print __METHOD__." s=".$s."\n";
		$this->assertStringContainsStringIgnoringCase('fa-user', $s, 'testImgPicto1');

		$s = img_picto('title', 'img.png', 'style="float: right"', 0);
		print __METHOD__." s=".$s."\n";
		$this->assertStringContainsStringIgnoringCase('theme', $s, 'testImgPicto2');
		$this->assertStringContainsStringIgnoringCase('style="float: right"', $s, 'testImgPicto2');

		$s = img_picto('title', '/fullpath/img.png', '', 1);
		print __METHOD__." s=".$s."\n";
		$this->assertEquals('<img src="/fullpath/img.png" alt="" title="title" class="inline-block">', $s, 'testImgPicto3');

		$s = img_picto('title', '/fullpath/img.png', '', true);
		print __METHOD__." s=".$s."\n";
		$this->assertEquals('<img src="/fullpath/img.png" alt="" title="title" class="inline-block">', $s, 'testImgPicto4');

		$s = img_picto('title', 'delete', '', 0, 1);
		print __METHOD__." s=".$s."\n";
		$this->assertEquals(DOL_URL_ROOT.'/theme/eldy/img/delete.png', $s, 'testImgPicto5');
	}

	/**
	 * testDolNow
	 *
	 * @return	void
	 */
	public function testDolNow()
	{
		$now = dol_now('gmt');
		$nowtzserver = dol_now('tzserver');
		print __METHOD__." getServerTimeZoneInt=".(getServerTimeZoneInt('now') * 3600)."\n";
		$this->assertEquals(getServerTimeZoneInt('now') * 3600, ($nowtzserver - $now));
	}



	/**
	 * Data provider for testVerifCond
	 *
	 * @return array<string,array{0:string,1:bool}>
	 */
	public function verifCondDataProvider(): array
	{
		return [
			'Test a true comparison' => ['1==1', true,],
			'Test a false comparison' => ['1==2', false,],
			'Test that the conf property of a module reports true when enabled' => ['isModEnabled("facture")', true,],
			'Test that the conf property of a module reports false when disabled' => ['isModEnabled("moduledummy")', false,],
			'Test that verifConf(0) returns false' => [0, false,],
			'Test that verifConf("0") returns false' => ["0", false,],
			'Test that verifConf("") returns false (special case)' => ['', true,],
		];
	}

	/**
	 * testVerifCond
	 *
	 * @return	void
	 */
	public function testVerifCond()
	{
		foreach ($this->verifCondDataProvider() as $case) {
			list($cond, $expected) = $case;
			if ($expected) {
				$this->assertTrue(verifCond($cond));
			} else {
				$this->assertFalse(verifCond($cond));
			}
		}
	}

	/**
	 * testVerifCondOnMethods
	 *
	 * @return	void
	 */
	public function testVerifCondOnMethods()
	{
		$a = verifCond('$user->hasRight("facture", "read")');
		$this->assertTrue($a);

		$a = verifCond('$user->hasMethodKo("facture", "read")');
		$this->assertFalse($a);
	}


	/**
	 * testGetDefaultTva
	 *
	 * @return	void
	 */
	public function testGetDefaultTva()
	{
		global $conf,$user,$langs,$db;
		$this->savconf = $conf;
		$this->savuser = $user;
		$this->savlangs = $langs;
		$this->savdb = $db;

		// Sellers
		$companyfrnovat = new Societe($db);
		$companyfrnovat->country_code = 'FR';
		$companyfrnovat->tva_assuj = 0;

		$companyfr = new Societe($db);
		$companyfr->country_code = 'FR';
		$companyfr->tva_assuj = 1;
		$companyfr->tva_intra = 'FR9999';

		// Buyers
		$companymc = new Societe($db);
		$companymc->country_code = 'MC';
		$companymc->tva_assuj = 1;
		$companyfr->tva_intra = 'MC9999';

		$companyit = new Societe($db);
		$companyit->country_code = 'IT';
		$companyit->tva_assuj = 1;
		$companyit->tva_intra = 'IT99999';

		$companyde = new Societe($db);
		$companyde->country_code = 'DE';
		$companyde->tva_assuj = 1;
		$companyde->tva_intra = 'DE99999';

		$notcompanyde = new Societe($db);
		$notcompanyde->country_code = 'DE';
		$notcompanyde->tva_assuj = 0;
		$notcompanyde->tva_intra = '';
		$notcompanyde->typent_code = 'TE_PRIVATE';

		$companyus = new Societe($db);
		$companyus->country_code = 'US';
		$companyus->tva_assuj = 1;
		$companyus->tva_intra = '';


		// Test RULE 0 (FR-DE)
		// Not tested

		// Test RULE 1
		print __METHOD__." rule=RULE 1\n";
		$vat = get_default_tva($companyfrnovat, $companymc, 0);
		$this->assertEquals(0, $vat, 'RULE 1');

		// Test RULE 2 (FR-FR)
		print __METHOD__." rule=RULE 2 FR-FR\n";
		$vat = get_default_tva($companyfr, $companyfr, 0);
		$this->assertEquals(20, $vat, 'RULE 2 FR-FR - Check that the dictionary is set to have default VAT to 20');

		// Test RULE 2 (FR-MC)
		print __METHOD__." rule=RULE 2 FR-MC\n";
		$vat = get_default_tva($companyfr, $companymc, 0);
		$this->assertEquals(20, $vat, 'RULE 2 FR-MC');

		// Test RULE 3 (FR-DE company)
		print __METHOD__." rule=RULE 3 FR-DE\n";
		$vat = get_default_tva($companyfr, $companyit, 0);
		$this->assertEquals(0, $vat, 'RULE 3');

		// Test RULE 4 (FR-DE not a company)
		print __METHOD__." rule=RULE 4 FR-DE\n";
		$vat = get_default_tva($companyfr, $notcompanyde, 0);
		$this->assertEquals(20, $vat, 'RULE 4');

		// Test RULE 5 (FR-US)
		print __METHOD__." rule=RULE 5 FR-US\n";
		$vat = get_default_tva($companyfr, $companyus, 0);
		$this->assertEquals(0, $vat, 'RULE 5');


		// We do same tests but with option SERVICE_ARE_ECOMMERCE_200238EC on.
		$conf->global->SERVICE_ARE_ECOMMERCE_200238EC = 1;

		// Test RULE 1 (FR-US)
		print __METHOD__." rule=RULE 1 ECOMMERCE_200238EC FR-US\n";
		$vat = get_default_tva($companyfr, $companyus, 0);
		$this->assertEquals(0, $vat, 'RULE 1 ECOMMERCE_200238EC');

		// Test RULE 2 (FR-FR)
		print __METHOD__." rule=RULE 2 ECOMMERCE_200238EC FR-FR\n";
		$vat = get_default_tva($companyfr, $companyfr, 0);
		$this->assertEquals(20, $vat, 'RULE 2 ECOMMERCE_200238EC');

		// Test RULE 3 (FR-DE company)
		print __METHOD__." rule=RULE 3 ECOMMERCE_200238EC FR-DE company\n";
		$vat = get_default_tva($companyfr, $companyde, 0);
		$this->assertEquals(0, $vat, 'RULE 3 ECOMMERCE_200238EC');

		// Test RULE 4 (FR-DE not a company)
		print __METHOD__." rule=RULE 4 ECOMMERCE_200238EC FR-DE not company\n";
		$vat = get_default_tva($companyfr, $notcompanyde, 0);
		$this->assertEquals(19, $vat, 'RULE 4 ECOMMERCE_200238EC');

		// Test RULE 5 (FR-US)
		print __METHOD__." rule=RULE 5 ECOMMERCE_200238EC FR-US\n";
		$vat = get_default_tva($companyfr, $companyus, 0);
		$this->assertEquals(0, $vat, 'RULE 5 ECOMMERCE_200238EC');
	}

	/**
	 * testGetDefaultTvaForBuyerState
	 *
	 * Covers VATRULE 2: when the buyer department (state/province) has a VAT rule in the
	 * dictionary, it becomes the default VAT rate. Also checks that only active rates of the
	 * current entity are considered (an inactive rate must never be selected).
	 *
	 * @return	void
	 */
	public function testGetDefaultTvaForBuyerState()
	{
		global $conf,$user,$langs,$db;
		$this->savconf = $conf;
		$this->savuser = $user;
		$this->savlangs = $langs;
		$this->savdb = $db;

		// Make sure the ecommerce directive left on by a previous test does not interfere with VATRULE 2
		unset($conf->global->SERVICE_ARE_ECOMMERCE_200238EC);

		// Seller subject to VAT in France
		$companyfr = new Societe($db);
		$companyfr->country_code = 'FR';
		$companyfr->tva_assuj = 1;
		$companyfr->tva_intra = 'FR9999';

		// Find an existing department (state/province) to attach a VAT rule to, and the France country id
		$stateid = 0;
		$sql = "SELECT rowid FROM ".$db->prefix()."c_departements WHERE code_departement = '75'";
		$resql = $db->query($sql);
		if ($resql && $db->num_rows($resql)) {
			$objdep = $db->fetch_object($resql);
			$stateid = (int) $objdep->rowid;
		}
		if (empty($stateid)) {
			$this->markTestSkipped('No department found in c_departements to run the VATRULE 2 test.');
			return;
		}

		$frpaysid = 0;
		$sql = "SELECT rowid FROM ".$db->prefix()."c_country WHERE code = 'FR'";
		$resql = $db->query($sql);
		if ($resql && $db->num_rows($resql)) {
			$objpays = $db->fetch_object($resql);
			$frpaysid = (int) $objpays->rowid;
		}

		// Insert two temporary VAT rules attached to that department:
		//  - an ACTIVE one at 12 that must be selected
		//  - an INACTIVE one at 25 (higher, so it would win the taux DESC sort) that must be ignored
		$db->query("DELETE FROM ".$db->prefix()."c_tva WHERE code IN ('TESTVATACT', 'TESTVATINACT')");
		$db->query("INSERT INTO ".$db->prefix()."c_tva (entity, fk_pays, fk_department_buyer, code, type_vat, taux, use_default, recuperableonly, active) VALUES (1, ".$frpaysid.", ".$stateid.", 'TESTVATACT', 0, 12, 0, 0, 1)");
		$db->query("INSERT INTO ".$db->prefix()."c_tva (entity, fk_pays, fk_department_buyer, code, type_vat, taux, use_default, recuperableonly, active) VALUES (1, ".$frpaysid.", ".$stateid.", 'TESTVATINACT', 0, 25, 0, 0, 0)");

		// Buyer located in that department
		$buyer = new Societe($db);
		$buyer->country_code = 'FR';
		$buyer->tva_assuj = 1;
		$buyer->state_id = $stateid;

		// Case 1: an active department rate exists -> VATRULE 2 returns it, ignoring the inactive (higher) one
		$vatactive = get_default_tva($companyfr, $buyer, 0);

		// Case 2: no active department rate remains -> VATRULE 2 must not fire on the inactive rows
		$db->query("UPDATE ".$db->prefix()."c_tva SET active = 0 WHERE code = 'TESTVATACT'");
		$vatinactive = get_default_tva($companyfr, $buyer, 0);

		// Cleanup fixtures before asserting so a failed assertion never leaves test data behind
		$db->query("DELETE FROM ".$db->prefix()."c_tva WHERE code IN ('TESTVATACT', 'TESTVATINACT')");

		// The active department rate is selected...
		$this->assertStringContainsString('TESTVATACT', $vatactive, 'VATRULE 2 must select the active department VAT rate');
		// ...and the inactive one is never selected (this is the fix)
		$this->assertStringNotContainsString('TESTVATINACT', $vatactive, 'An inactive department VAT rate must not be selected by VATRULE 2');
		// When no active department rate remains, VATRULE 2 does not select the (now inactive) rate either
		$this->assertStringNotContainsString('TESTVATACT', $vatinactive, 'An inactive department VAT rate must not be selected by VATRULE 2');
	}

	/**
	 * testGetDefaultLocalTax
	 *
	 * @return	void
	 */
	public function testGetDefaultLocalTax()
	{
		global $conf,$user,$langs,$db;
		$this->savconf = $conf;
		$this->savuser = $user;
		$this->savlangs = $langs;
		$this->savdb = $db;

		$companyfrnovat = new Societe($db);
		$companyfrnovat->country_code = 'FR';
		$companyfrnovat->tva_assuj = 0;
		$companyfrnovat->localtax1_assuj = 0;
		$companyfrnovat->localtax2_assuj = 0;

		$companyes = new Societe($db);
		$companyes->country_code = 'ES';
		$companyes->tva_assuj = 1;
		$companyes->localtax1_assuj = 1;
		$companyes->localtax2_assuj = 1;

		$companymc = new Societe($db);
		$companymc->country_code = 'MC';
		$companymc->tva_assuj = 1;
		$companymc->localtax1_assuj = 0;
		$companymc->localtax2_assuj = 0;

		$companyit = new Societe($db);
		$companyit->country_code = 'IT';
		$companyit->tva_assuj = 1;
		$companyit->tva_intra = 'IT99999';
		$companyit->localtax1_assuj = 0;
		$companyit->localtax2_assuj = 0;

		$notcompanyit = new Societe($db);
		$notcompanyit->country_code = 'IT';
		$notcompanyit->tva_assuj = 1;
		$notcompanyit->tva_intra = '';
		$notcompanyit->typent_code = 'TE_PRIVATE';
		$notcompanyit->localtax1_assuj = 0;
		$notcompanyit->localtax2_assuj = 0;

		$companyus = new Societe($db);
		$companyus->country_code = 'US';
		$companyus->tva_assuj = 1;
		$companyus->tva_intra = '';
		$companyus->localtax1_assuj = 0;
		$companyus->localtax2_assuj = 0;

		// Test RULE FR-MC
		print __METHOD__." rule=FR-MC\n";
		$vat1 = get_default_localtax($companyfrnovat, $companymc, 1, 0);
		$vat2 = get_default_localtax($companyfrnovat, $companymc, 2, 0);
		$this->assertEquals(0, $vat1);
		$this->assertEquals(0, $vat2);

		// Test RULE ES-ES
		print __METHOD__." rule=ES-ES\n";
		$vat1 = get_default_localtax($companyes, $companyes, 1, 0);
		$vat2 = get_default_localtax($companyes, $companyes, 2, 0);
		$this->assertEquals($vat1, 5.2);
		$this->assertStringStartsWith((string) $vat2, '-19:-15:-9');       // Can be -19 (old version) or '-19:-15:-9' (new setup)

		// Test RULE ES-IT
		print __METHOD__." rule=ES-IT company\n";
		$vat1 = get_default_localtax($companyes, $companyit, 1, 0);
		$vat2 = get_default_localtax($companyes, $companyit, 2, 0);
		$this->assertEquals(0, $vat1);
		$this->assertEquals(0, $vat2);

		// Test RULE ES-not IT
		print __METHOD__." rule=ES-IT not company\n";
		$vat1 = get_default_localtax($companyes, $notcompanyit, 1, 0);
		$vat2 = get_default_localtax($companyes, $notcompanyit, 2, 0);
		$this->assertEquals(0, $vat1);
		$this->assertEquals(0, $vat2);

		// Test RULE FR-IT
		// Not tested

		// Test RULE ES-US
		print __METHOD__." rule=ES-US\n";
		$vat1 = get_default_localtax($companyes, $companyus, 1, 0);
		$vat2 = get_default_localtax($companyes, $companyus, 2, 0);
		$this->assertEquals(0, $vat1);
		$this->assertEquals(0, $vat2);
	}


	/**
	 * testGetLocalTaxByThird
	 *
	 * @return	void
	 */
	public function testGetLocalTaxByThird()
	{
		global $mysoc;

		$mysoc->country_code = 'ES';

		$result = get_localtax_by_third(1);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('5.2', $result);

		$result = get_localtax_by_third(2);
		print __METHOD__." result=".$result."\n";
		$this->assertEquals('-19:-15:-9', $result);
	}


	/**
	 * testDolExplodeIntoArray
	 *
	 * @return	void
	 */
	public function testDolExplodeIntoArray()
	{
		$stringtoexplode = 'AA=B/B.CC=.EE=FF.HH=GG;.';
		$tmp = dolExplodeIntoArray($stringtoexplode, '.', '=');

		print __METHOD__." tmp=".json_encode($tmp)."\n";
		$this->assertEquals('{"AA":"B\/B","CC":"","EE":"FF","HH":"GG;"}', json_encode($tmp));

		$stringtoexplode = "AA=B/B;CC=\n\rEE=FF\nHH=GG;;;\nII=JJ\n";
		$tmp = dolExplodeIntoArray($stringtoexplode, "(\r\n|\n|\r|;)", '=');

		print __METHOD__." tmp=".json_encode($tmp)."\n";
		$this->assertEquals('{"AA":"B\/B","CC":"","EE":"FF","HH":"GG","II":"JJ"}', json_encode($tmp));
	}

	/**
	 * dol_nl2br
	 *
	 * @return void
	 */
	public function testDolNl2Br()
	{

		//String to encode
		$string = "a\na";

		$this->assertEquals(dol_nl2br($string), "a<br>\na");

		//With $forxml parameter
		$this->assertEquals(dol_nl2br($string, 0, 1), "a<br />\na");

		//Replacing \n by br
		$this->assertEquals(dol_nl2br($string, 1), "a<br>a");

		//With $forxml parameter
		$this->assertEquals(dol_nl2br($string, 1, 1), "a<br />a");
	}

	/**
	 * testDolPrice2Num
	 *
	 * @return boolean
	 */
	public function testDolPrice2Num()
	{
		global $langs, $conf;

		$oldlangs = $langs;

		// For US language SeparatorThousand=, and SeparatorDecimal=.
		$newlangs = new Translate('', $conf);
		$newlangs->setDefaultLang('en_US');
		$newlangs->load("main");
		$langs = $newlangs;

		$this->assertEquals(150, price2num('(SELECT/**/CASE/**/WHEN/**/(0<1)/**/THEN/**/SLEEP(5)/**/ELSE/**/SLEEP(0)/**/END)'));

		$this->assertEquals(1000, price2num('1 000.0'));
		$this->assertEquals(1000, price2num('1 000', 'MT'));
		$this->assertEquals(1000, price2num('1 000', 'MU'));

		$this->assertEquals(1000.123456, price2num('1 000.123456'), 'Test 1 000.123456 give 1000.123456 with us language');
		$this->assertEquals(1000.123456, price2num('1,000.123456'), 'Test 1,000.123456 give 1000.123456 with us language');
		$this->assertEquals(1000.123, price2num('1 000.123'), 'Test 1 000.123 give 1000.123 with us language');
		$this->assertEquals(1000.123, price2num('1,000.123'), 'Test 1,000.123 give 1000.123 with us language');

		// Round down
		$this->assertEquals(1000.12, price2num('1 000.123452', 'MT'), 'Error in round down with MT');
		$this->assertEquals(1000.12345, price2num('1 000.123452', 'MU'), "Test MU");

		// Round up
		$this->assertEquals(1000.13, price2num('1 000.125456', 'MT'));
		$this->assertEquals(1000.12546, price2num('1 000.125456', 'MU'), "Test MU");

		$this->assertEquals(1, price2num('1.000'), 'Test 1.000 give 1 with english language');

		// Text can't be converted
		$this->assertEquals('12.4', price2num('12.4$'));
		$this->assertEquals('12.4', price2num('12r.4$'));

		$this->assertEquals('1.023210.00', price2num('1.023,210.00'), 'Test invalid 1.023,210.00 with en_US');
		$this->assertEquals('1023.21000', price2num('1,023.210,00'), 'Test invalid 1,023.210,00 with en_US');

		// For spanish language SeparatorThousand=. and SeparatorDecimal=,
		$newlangs2 = new Translate('', $conf);
		$newlangs2->setDefaultLang('es_ES');
		$newlangs2->load("main");
		$langs = $newlangs2;

		// Test with 3 chars after . or ,
		// If a . is used and there is 3 digits after, it is a thousand separator
		$this->assertEquals(1234, price2num('1.234', '', 2), 'Test 1.234 give 1234 with spanish language if user input');
		$this->assertEquals(1.234, price2num('1,234', '', 2), 'Test 1,234 give 1.234 with spanish language if user input');
		$this->assertEquals(1234, price2num('1 234', '', 2), 'Test 1 234 give 1234 with spanish language if user input');
		$this->assertEquals(-1.234, price2num('-1.234'), 'Test -1.234 give 1.234 with spanish language and not user input (this differs when user input)');
		$this->assertEquals(-1.234, price2num('-1,234'), 'Test -1,234 give 1234 with spanish language and not user input');
		$this->assertEquals(-1234, price2num('-1 234'), 'Test -1 234 give 1234 with spanish language and not user input');
		$this->assertEquals(1111.234, price2num('1.111,234'), 'Test 1.111,234 give 1111.234 with spanish language and not user input');
		$this->assertEquals(21500123, price2num('21.500.123'), 'Test 21.500.123 give 21500123 with spanish language and not user input');
		$this->assertEquals(21500123, price2num('21500.123', 0, 2), 'Test 21500.123 give 21500123 with spanish language if user input');
		$this->assertEquals(21500123, price2num('21 500.123', 0, 2), 'Test 21 500.123 give 21500123 with spanish language if user input');
		$this->assertEquals(21500.123, price2num('21500.123'), 'Test 21500.123 give 21500123 with spanish language and not user input');
		$this->assertEquals(21500.123, price2num('21500,123'), 'Test 21500,123 give 21500.123 with spanish language and not user input');
		// Test with 2 digits (we can guess that , is not a thousand separator)
		$this->assertEquals(21500.12, price2num('21500.12'), 'Test 21500.12 give 21500.12 with spanish language and not user input');
		$this->assertEquals(21500.12, price2num('21500,12'), 'Test 21500,12 give 21500.12 with spanish language and not user input');
		// Test with 3 digits
		$this->assertEquals(12123, price2num('12.123', '', 2), 'Test 12.123 give 12123 with spanish language if user input');
		$this->assertEquals(12.123, price2num('12,123', '', 2), 'Test 12,123 give 12.123 with spanish language if user input');
		$this->assertEquals(1012.123, price2num('1.012,123', '', 2), 'Test 1.012,123 give 1012.123 with spanish language if user input');
		$this->assertEquals(1012.123, price2num('1 012,123', '', 2), 'Test 1 012,123 give 1012.123 with spanish language if user input');
		$this->assertEquals(12.123, price2num('12.123'), 'Test 12.123 give 12.123 with spanish language and not user input');
		$this->assertEquals(12.123, price2num('12,123'), 'Test 12,123 give 12.123 with spanish language and not user input');

		// For french language SeparatorThousand=Space and SeparatorDecimal=,
		$newlangs3 = new Translate('', $conf);
		$newlangs3->setDefaultLang('fr_FR');
		$newlangs3->load("main");
		$langs = $newlangs3;

		$this->assertEquals(1, price2num('1.000', '', 2), 'Test 1.000 give 1 with french language if user input');
		$this->assertEquals(1, price2num('1.000'), 'Test 1.000 give 1 with french language');
		$this->assertEquals(1000, price2num('1 000'), 'Test 1.000 give 1 with french language');
		$this->assertEquals(1.234, price2num('1.234', '', 2), 'Test 1.234 give 1.234 with french language if user input');
		$this->assertEquals(1.234, price2num('1.234'), 'Test 1.234 give 1.234 with french language');
		$this->assertEquals(1.234, price2num('1,234', '', 2), 'Test 1,234 give 1.234 with french language if user input');
		$this->assertEquals(1.234, price2num('1,234'), 'Test 1,234 give 1.234 with french language');
		$this->assertEquals(1111.234, price2num('1 111,234'), 'Test 1 111,234 give 1111.234 with french language');
		$this->assertEquals(21500000, price2num('21500 000'), 'Test 21500 000 give 21500000 with french language');
		$this->assertEquals(21500000, price2num('21 500 000'), 'Test 21 500 000 give 21500000 with french language');
		$this->assertEquals(21500, price2num('21500.00'), 'Test 21500.00 give 21500 with french language');
		$this->assertEquals(21500, price2num('21500,00'), 'Test 21500,00 give 21500 with french language');

		$this->assertEquals(21500, price2num('21.500,00'), 'Test 21.500,00 give 21500 with french language');
		$this->assertEquals('1.023.210.00', price2num('1.023,210.00'), 'Test invalid 1.023,210.00 with french language');
		$this->assertEquals('1.023.210.00', price2num('1,023.210,00'), 'Test invalid 1,023.210,00 with french language');

		$langs = $oldlangs;

		return true;
	}

	/**
	 * testDolGetDate
	 *
	 * @return boolean
	 */
	public function testDolGetDate()
	{
		global $conf;

		$conf->global->MAIN_START_WEEK = 0;

		$tmp = dol_getdate(24 * 60 * 60 + 1, false, 'UTC');		// 2/1/1970 and 1 second = friday
		$this->assertEquals(5, $tmp['wday'], 'Bad value of day in week');

		$conf->global->MAIN_START_WEEK = 1;

		$tmp = dol_getdate(1, false, 'UTC');				// 1/1/1970 and 1 second = thirday
		$this->assertEquals(4, $tmp['wday'], 'Bad value of day in week');

		$tmp = dol_getdate(24 * 60 * 60 + 1, false, 'UTC');		// 2/1/1970 and 1 second = friday
		$this->assertEquals(5, $tmp['wday'], 'Bad value of day in week');

		$tmp = dol_getdate(1, false, "Europe/Paris");						// 1/1/1970 and 1 second = thirday
		$this->assertEquals(1970, $tmp['year']);
		$this->assertEquals(1, $tmp['mon']);
		$this->assertEquals(1, $tmp['mday']);
		$this->assertEquals(4, $tmp['wday']);
		$this->assertEquals(0, $tmp['yday']);
		$this->assertEquals(1, $tmp['hours']);		// We are winter, so we are GMT+1 even during summer
		$this->assertEquals(0, $tmp['minutes']);
		$this->assertEquals(1, $tmp['seconds']);

		$tmp = dol_getdate(15638401, false, "Europe/Paris");					// 1/7/1970 and 1 second = wednesday
		$this->assertEquals(1970, $tmp['year']);
		$this->assertEquals(7, $tmp['mon']);
		$this->assertEquals(1, $tmp['mday']);
		$this->assertEquals(3, $tmp['wday']);
		$this->assertEquals(181, $tmp['yday']);
		$this->assertEquals(1, $tmp['hours']);		// There is no daylight in 1970, so we are GMT+1 even during summer
		$this->assertEquals(0, $tmp['minutes']);
		$this->assertEquals(1, $tmp['seconds']);

		$tmp = dol_getdate(1593561601, false, "Europe/Paris");				// 1/7/2020 and 1 second = wednesday
		$this->assertEquals(2020, $tmp['year']);
		$this->assertEquals(7, $tmp['mon']);
		$this->assertEquals(1, $tmp['mday']);
		$this->assertEquals(3, $tmp['wday']);
		$this->assertEquals(182, $tmp['yday']);		// 182 and not 181, due to the 29th february
		$this->assertEquals(2, $tmp['hours']);		// There is a daylight, so we are GMT+2
		$this->assertEquals(0, $tmp['minutes']);
		$this->assertEquals(1, $tmp['seconds']);

		$tmp = dol_getdate(1, false, 'UTC');						// 1/1/1970 and 1 second = thirday
		$this->assertEquals(1970, $tmp['year']);
		$this->assertEquals(1, $tmp['mon']);
		$this->assertEquals(1, $tmp['mday']);
		$this->assertEquals(4, $tmp['wday']);
		$this->assertEquals(0, $tmp['yday']);
		// We must disable this because on CI, timezone is may be UTC or something else
		//$this->assertEquals(1, $tmp['hours']);	// We are winter, so we are GMT+1 even during summer
		$this->assertEquals(0, $tmp['minutes']);
		$this->assertEquals(1, $tmp['seconds']);

		$tmp = dol_getdate(15638401, false, 'UTC');				// 1/7/1970 and 1 second = wednesday
		$this->assertEquals(1970, $tmp['year']);
		$this->assertEquals(7, $tmp['mon']);
		$this->assertEquals(1, $tmp['mday']);
		$this->assertEquals(3, $tmp['wday']);
		$this->assertEquals(181, $tmp['yday']);
		// We must disable this because on CI, timezone is may be UTC or something else
		//$this->assertEquals(1, $tmp['hours']);	// There is no daylight in 1970, so we are GMT+1 even during summer
		$this->assertEquals(0, $tmp['minutes']);
		$this->assertEquals(1, $tmp['seconds']);

		$tmp = dol_getdate(1593561601, false, 'UTC');				// 1/7/2020 and 1 second = wednesday
		$this->assertEquals(2020, $tmp['year']);
		$this->assertEquals(7, $tmp['mon']);
		$this->assertEquals(1, $tmp['mday']);
		$this->assertEquals(3, $tmp['wday']);
		$this->assertEquals(182, $tmp['yday']);		// 182 and not 181, due to the 29th february
		// We must disable this because on CI, timezone is may be UTC or something else
		//$this->assertEquals(2, $tmp['hours']);	// There is a daylight, so we are GMT+2
		$this->assertEquals(0, $tmp['minutes']);
		$this->assertEquals(1, $tmp['seconds']);

		return true;
	}


	/**
	 * testMakeSubstitutions
	 *
	 * @return boolean
	 */
	public function testMakeSubstitutions()
	{
		global $conf, $langs, $mysoc;
		$langs->load("main");

		// Try simple replacement
		$substit = array("__AAA__" => 'Not used', "__BBB__" => 'Not used', "__CCC__" => "C instead", "DDD" => "D instead");
		$substit += getCommonSubstitutionArray($langs);

		$chaine = 'This is a string with theme constant __[MAIN_THEME]__ and __(DIRECTION)__ and __CCC__ and DDD and __MYCOMPANY_NAME__ and __YEAR__';
		$newstring = make_substitutions($chaine, $substit);
		print __METHOD__." ".$newstring."\n";
		$this->assertEquals($newstring, 'This is a string with theme constant eldy and ltr and C instead and D instead and '.$mysoc->name.' and '.dol_print_date(dol_now(), '%Y', 'gmt'));

		// Try mix HTML not HTML, no change on initial text
		$substit = array("__NOHTML__" => 'No html', "__HTML__" => '<b>HTML</b>');

		$chaine = "This is a text with\nNew line\nThen\n__NOHTML__\nThen\n__HTML__";
		$newstring = make_substitutions($chaine, $substit, $langs);
		print __METHOD__." ".$newstring."\n";
		$this->assertEquals($newstring, "This is a text with\nNew line\nThen\nNo html\nThen\n<b>HTML</b>", 'Test on make_substitutions with conversion of inserted values only');

		// Try mix HTML not HTML, accept to change initial text
		$substit = array("__NOHTML__" => 'No html', "__HTML__" => '<b>HTML</b>');

		$chaine = "This is a text with\nNew line\nThen\n__NOHTML__\nThen\n__HTML__";
		$newstring = make_substitutions($chaine, $substit, $langs, 1);
		print __METHOD__." ".$newstring."\n";
		$this->assertEquals($newstring, "This is a text with<br>\nNew line<br>\nThen<br>\nNo html<br>\nThen<br>\n<b>HTML</b>", 'Test on make_substitutions with full conversion of text accepted');


		// Try mix HTML into not HTML but no replaement is done
		$newstring = make_substitutions('¿Necesitas ayuda para empezar con GLPI?', array('__SENDEREMAIL_SIGNATURE__' => '<br><strong>HTML content</strong>'), $langs, 1);
		print __METHOD__." ".$newstring."\n";
		$this->assertEquals($newstring, '¿Necesitas ayuda para empezar con GLPI?');

		return true;
	}

	/**
	 * testDolStringIsGoodIso
	 *
	 * @return boolean
	 */
	public function testDolStringIsGoodIso()
	{
		global $conf, $langs;

		$chaine = 'This is an ISO string';
		$result = dol_string_is_good_iso($chaine);
		$this->assertEquals($result, 1);

		$chaine = 'This is a not ISO string '.chr(0);
		$result = dol_string_is_good_iso($chaine);
		$this->assertEquals($result, 0);

		return true;
	}

	/**
	 * testUtf8Check
	 *
	 * @return boolean
	 */
	public function testUtf8Check()
	{
		global $conf, $langs;

		$chaine = 'This is an UTF8 string with a é.';
		$result = utf8_check($chaine);
		$this->assertEquals(true, $result);

		$chaine = mb_convert_encoding('This is an UTF8 with a é.', 'ISO-8859-1', 'UTF-8');
		$result = utf8_check($chaine);
		$this->assertEquals(false, $result);

		return true;
	}

	/**
	 * testUtf8Valid
	 *
	 * @return boolean
	 */
	public function testUtf8Valid()
	{
		global $conf, $langs;

		$chaine = 'This is an UTF8 string with a é.';
		$result = utf8_valid($chaine);
		$this->assertEquals(true, $result);

		$chaine = mb_convert_encoding('This is an UTF8 with a é.', 'ISO-8859-1', 'UTF-8');
		$result = utf8_valid($chaine);
		$this->assertEquals(false, $result);

		return true;
	}

	/**
	 * testGetUserRemoteIP
	 *
	 * @return boolean
	 */
	public function testGetUserRemoteIP()
	{
		global $conf, $langs;

		$_SERVER['HTTP_X_FORWARDED_FOR'] = '1.2.3.4';
		$_SERVER['HTTP_CLIENT_IP'] = '5.6.7.8';
		$result = getUserRemoteIP();
		$this->assertEquals($result, '1.2.3.4');

		$_SERVER['HTTP_X_FORWARDED_FOR'] = '1.2.3.4<corrupted>';
		$_SERVER['HTTP_CLIENT_IP'] = '5.6.7.8';
		$result = getUserRemoteIP();
		$this->assertEquals($result, '5.6.7.8');

		$_SERVER['HTTP_X_FORWARDED_FOR'] = '[1:2:3:4]';
		$_SERVER['HTTP_CLIENT_IP'] = '5.6.7.8';
		$result = getUserRemoteIP();
		$this->assertEquals($result, '[1:2:3:4]');

		return true;
	}

	/**
	 * testFetchObjectByElement
	 *
	 * @return boolean
	 */
	public function testFetchObjectByElement()
	{
		global $conf, $langs;

		$result = fetchObjectByElement(0, 'product');

		$this->assertTrue(is_object($result));

		$hasvariantsmodule = array_key_exists('variants', $conf->modules);
		$originalvariantsmodule = $hasvariantsmodule ? $conf->modules['variants'] : null;

		try {
			$conf->modules['variants'] = 1;

			$productattribute = getElementProperties('product_attribute');
			$this->assertSame('variants', $productattribute['module']);
			$this->assertSame('variants/class', $productattribute['classpath']);
			$this->assertSame('ProductAttribute', $productattribute['classfile']);
			$this->assertSame('ProductAttribute', $productattribute['classname']);

			$productattributevalue = getElementProperties('product_attribute_value');
			$this->assertSame('variants', $productattributevalue['module']);
			$this->assertSame('variants/class', $productattributevalue['classpath']);
			$this->assertSame('ProductAttributeValue', $productattributevalue['classfile']);
			$this->assertSame('ProductAttributeValue', $productattributevalue['classname']);
			$this->assertSame('product_attribute', $productattributevalue['parent_element']);

			$this->assertInstanceOf(ProductAttribute::class, fetchObjectByElement(0, 'product_attribute'));
			$this->assertInstanceOf(ProductAttributeValue::class, fetchObjectByElement(0, 'product_attribute_value'));
		} finally {
			if ($hasvariantsmodule) {
				$conf->modules['variants'] = $originalvariantsmodule;
			} else {
				unset($conf->modules['variants']);
			}
		}

		return true;
	}

	/**
	 * testRoundUpToNextMultiple
	 *
	 * @return void
	 */
	public function testRoundUpToNextMultiple()
	{
		$this->assertEquals(roundUpToNextMultiple(39.5), 40);
		$this->assertEquals(roundUpToNextMultiple(40), 40);
		$this->assertEquals(roundUpToNextMultiple(40.4), 45);
		$this->assertEquals(roundUpToNextMultiple(40.5), 45);
		$this->assertEquals(roundUpToNextMultiple(44.5), 45);

		$this->assertEquals(roundUpToNextMultiple(39.5, 10), 40);
		$this->assertEquals(roundUpToNextMultiple(40, 10), 40);
		$this->assertEquals(roundUpToNextMultiple(40.5, 10), 50);
		$this->assertEquals(roundUpToNextMultiple(44.5, 10), 50);

		$this->assertEquals(roundUpToNextMultiple(39.5, 6), 42);
		$this->assertEquals(roundUpToNextMultiple(40, 6), 42);
		$this->assertEquals(roundUpToNextMultiple(40.5, 6), 42);
		$this->assertEquals(roundUpToNextMultiple(44.5, 6), 48);
	}

	/**
	 * testNaturalSearch
	 *
	 * @return void
	 */
	public function testNaturalSearch()
	{
		global $db;

		$s = natural_search("t.field", "abc def");
		$this->assertEquals(" AND (t.field LIKE '%abc%' AND t.field LIKE '%def%')", $s);

		$s = natural_search("t.field", "'abc def' ghi");
		$this->assertEquals(" AND (t.field LIKE '%abc def%' AND t.field LIKE '%ghi%')", $s);

		$s = natural_search("t.field", "abc def,ghi", 3);				// mode 3 is to provide a list of string separated with coma
		$this->assertEquals(" AND (t.field IN ('abc def','ghi'))", $s);

		$s = natural_search("t.field", "'ab\'c' def','ghi', 'jkl'", 3);	// mode 3 is to provide a list of string separated with coma
		$this->assertEquals(" AND (t.field IN ('abc def','ghi','jkl'))", $s);

		$s = natural_search("t.field", "a,b", 3);						// mode 3 is to provide a list of string separated with coma
		$this->assertEquals(" AND (t.field IN ('a','b'))", $s);

		$s = natural_search("t.field", "A'@%B", 3);						// mode 3 is to provide a list of string separated with coma
		$this->assertEquals(" AND (t.field IN ('AB'))", $s);

		/*
		$s = $db->sanitize("a,b", 1);
		var_dump($s);
		$s = $db->sanitize("'a',b", 1);
		var_dump($s);
		$s = $db->sanitize("'a'b',c", 1);
		var_dump($s);
		*/

		$s = natural_search("t.field", "KØB", 3);						// mode 3 is to provide a list of string separated with coma
		$this->assertEquals(" AND (t.field IN ('KØB'))", $s);
	}

	/**
	 * testDolExplodeKeepIfQuotes
	 *
	 * @return void
	 */
	public function testDolExplodeKeepIfQuotes()
	{
		global $db;

		$result = dolExplodeKeepIfQuotes("a b");
		$this->assertEquals("a", $result[0]);
		$this->assertEquals("b", $result[1]);

		$result = dolExplodeKeepIfQuotes("'a' 'b'");
		$this->assertEquals("a", $result[0]);
		$this->assertEquals("b", $result[1]);

		$result = dolExplodeKeepIfQuotes("a 'b' c");
		$this->assertEquals("a", $result[0]);
		$this->assertEquals("b", $result[1]);
		$this->assertEquals("c", $result[2]);

		$result = dolExplodeKeepIfQuotes("a b'c");
		$this->assertEquals("a", $result[0]);
		$this->assertEquals("b'c", $result[1]);

		$result = dolExplodeKeepIfQuotes("a 'b'c");
		$this->assertEquals("a", $result[0]);
		$this->assertEquals("b", $result[1]);
		$this->assertEquals("c", $result[2]);

		$result = dolExplodeKeepIfQuotes("a 'b c'");
		$this->assertEquals("a", $result[0]);
		$this->assertEquals("b c", $result[1]);

		$result = dolExplodeKeepIfQuotes("1 0");
		$this->assertEquals("1", $result[0]);
		$this->assertEquals("0", $result[1]);
	}



	/**
	 * testDolSanitizePathName
	 *
	 * @return void
	 */
	public function testDolSanitizePathName()
	{
		global $conf,$user,$langs,$db;
		$conf = $this->savconf;
		$user = $this->savuser;
		$langs = $this->savlangs;
		$db = $this->savdb;

		$s = '../aéa/bbb ccc/ddd';
		$result = dol_sanitizePathName($s);
		$this->assertEquals('_/aéa/bbb ccc/ddd', $result);

		$s = '../aéa/bbb ccc/ddd';
		$result = dol_sanitizePathName($s, '_', 1);
		$this->assertEquals('_/aea/bbb ccc/ddd', $result);

		$s = 'C:\ccc/d\'d"d$';
		$result = dol_sanitizePathName($s);
		$this->assertEquals('C:\ccc/d\'d_d_', $result);

		$s = 'C:\ccc/d\'d"d$';
		$result = dol_sanitizePathName($s);
		$this->assertEquals('C:\ccc/d\'d_d_', $result);

		$s = '/aaa/bbb -a -b';
		$result = dol_sanitizePathName($s);
		$this->assertEquals('/aaa/bbb _a _b', $result);

		$s = '/aaa/bbb -a -b';
		$result = dol_sanitizePathName($s, '_', 0, 1);
		$this->assertEquals('/aaa/bbb -a -b', $result);
	}
}
