package routines;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class LcagDateTimeTest extends LcagDateTime {
	private static final LcagLogger log = LcagLogger.getLogger(LcagDateTimeTest.class);
	private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:SS");
	private static SimpleDateFormat DATE_FORMAT_CABS = new SimpleDateFormat("ddMMMyy", Locale.ENGLISH);
	private static Date fromDate1;
	private static Date toDate1;
	private static TimeZone tz;

	@BeforeAll
	static void setUpBeforeClass() throws Exception {
		fromDate1 = DATE_FORMAT.parse("30-12-2000 20:00:00");
		toDate1 = DATE_FORMAT.parse("03-01-2001 00:00:00");
		tz = TimeZone.getDefault();
	}

	@AfterAll
	static void tearDownAfterClass() throws Exception {
	}

	@BeforeEach
	void setUp() throws Exception {
	}

	@AfterEach
	void tearDown() throws Exception {
		TimeZone.setDefault(tz);
	}

	@Test
	void testAddDays() throws ParseException {
		Date aDate = toDate("2021-02-28");
		Object result;

		result = addDays(aDate, 0);
		assertEquals(aDate, aDate, "Adding 0 days should return the same date.");

		result = formatDate("yyyy-MM-dd", addDays(aDate, 2));
		assertEquals(result, "2021-03-02", "Adding 2 days to 28. Feb should return Mar, 2nd");

		result = formatDate("yyyy-MM-dd", addDays(aDate, -28));
		assertEquals(result, "2021-01-31", "Adding negative days -28 should return 28 days back in time");

		aDate = toDate("2021-01-01");
		result = formatDate("yyyy-MM-dd", addDays(aDate, -1));
		assertEquals(result, "2020-12-31", "Going one day back at start of year should return last day of year before");

		aDate = toDate("2021-12-31");
		result = formatDate("yyyy-MM-dd", addDays(aDate, 1));
		assertEquals(result, "2022-01-01", "Next day of last day of year should be first day in next year");

		assertThrows(NullPointerException.class, () -> {
			addDays(null, -28);
		}, "Argument must not be null!");
	}

	@Test
	void testAsDurationString() {
		// FIXME: Function still buggy (2021-10-06)
		String result = null;

		result = asDurationString("12:04");
		assertEquals("PT12H04M", result);

		result = asDurationString("+12:04");
		assertEquals("PT12H04M", result);

		result = asDurationString("-12:04");
		assertEquals("-PT12H04M", result);
	}

	@Test
	void testAsDuration() {
		// FIXME: Function still buggy (2021-10-06)
		String result = null;

		result = asDuration("12:04").toString();
		assertEquals("PT12H4M", result);

		result = asDuration("+12:04").toString();
		assertEquals("PT12H4M", result);

		result = asDuration("-12:04").toString();
		assertEquals("PT-12H-4M", result);
	}

	/**
	 * Converts a double value to a UTC time offset String (Used in MAD timezone
	 * conversion)
	 * 
	 * @param d output: Talend or Dom4J or String xml document to be changed
	 * @return Time offset as String [+-]HH:MM
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} Double(d) d: input: xpath lookup string
	 * 
	 *         {example} double2UtcOffset(6) = "+06:00", double2UtcOffset(6.5) =
	 *         "+06:30"
	 */

	public static String double2UtcOffset(Double d) {
		String result = null;

		if (d == null)
			return result;

		if (d <= -24.0 || d >= 24.0)
			throw new IllegalArgumentException(String.format("Argument must be > -24 and < 24, %f given", d));

		if (d != null) {
			String sign = "+";

			if (d < 0) {
				sign = "-";
				d = -d;
			}

			int hours = (int) Math.floor(d);
			int minutes = (int) Math.round((d - hours) * 60.0);

			if (minutes % 15 > 0) {
				log.warn("double2UtcOffset: %.2f is an UNUSUAL offset (not a a multiple of 15 mins) !", d);
			}
			result = sign + String.format("%02d:%02d", hours, minutes);
		}

		return result;
	}

	/**
	 * Checks if the first 2 letters of a string represent a IATA Country Code of a
	 * Country that has more than one time zone.
	 * 
	 * Note: List of Countries is hard-coded
	 * 
	 * @param aCode input: Code to check
	 * @return true if Code is valid as Multi-Timezone Country Code
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} String(aCode) aCode: input: Code to check
	 * 
	 *         {example} isMultiTimezoneCountry("DE:01") = false,
	 *         isMultiTimezoneCountry("DE0") = false isMultiTimezoneCountry("AU2A")
	 *         = true isMultiTimezoneCountry("US:02") = true
	 */
	public static boolean isMultiTimezoneCountry(String aCode) {
		// List of Countries that have multiple Timezones according to IATA Airline
		// Coding Directory (ACD):
		final List<String> multiTzCountries = java.util.Arrays.asList("AU", "BR", "CA", "CD", "CL", "EC", "ES", "FM",
				"GL", "ID", "JO", "KI", "KZ", "MX", "NZ", "PF", "PG", "PT", "RU", "UM", "US", "ZZ");

		if (aCode == null || aCode.length() < 2) {
			throw new IllegalArgumentException(
					String.format("Illegal Code length of '%s': Must start with 2 letter Country Code !", aCode));
		}

		String countryCode = aCode.substring(0, 2);

		boolean result = multiTzCountries.contains(countryCode);

		return result;
	}

	/**
	 * Converts a Timezone Code in MDS format (should by IATA) into MAD format.
	 * Note: 1. Does not transform special values like for countries MX, BR,... 2.
	 * Includes a hard coded list of Country Codes having multiple Timezone Codes
	 * (Created for jobs updating Timezone)
	 * 
	 * @param aCode input: MDS Timezone Code
	 * @return MAD Timezone Code format
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} String(aCode) aCode: input: MDS Timezone Code
	 * 
	 *         {example} convertMDS_TimezoneCodeToMAD("DE:01") = "DE0",
	 *         convertMDS_TimezoneCodeToMAD("PF:01") = "PF1" (PF is multi-tz
	 *         country) convertMDS_TimezoneCodeToMAD("MX:05") = "MX5" (but MAD uses
	 *         MX1B)
	 */
	public static String convertMDS_TimezoneCodeToMAD(String aCode) {
		if (aCode == null || aCode.length() < 5) {
			throw new IllegalArgumentException(
					String.format("Illegal MDS Code '%s': Must have at least 5 characters!", aCode));
		}

		String countryCode = aCode.substring(0, 2);
		String tzNo = aCode.substring(3);
		String result = countryCode;

		if (tzNo.startsWith("0")) {
			if ("01".equals(tzNo) && !isMultiTimezoneCountry(aCode)) {
				// In MAD countries that have just one TZ always use "0" as last character
				result += "0";
			} else {
				result += tzNo.substring(1); // drop leading "0"
			}
		} else {
			result += tzNo;
		}

		return result;
	}

	/**
	 * Converts a Timezone Code in MAD format (should by IATA) into MDS format.
	 * Note: Does not transform special values like for countries MX, BR,...
	 * 
	 * @param aCode input: MAD Timezone Code
	 * @return MDS Timezone Code format
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} String(aCode) aCode: input: MAD Timezone Code
	 * 
	 *         {example} convertMAD_TimezoneCodeToMDS("DE0") = "DE:01",
	 *         convertMAD_TimezoneCodeToMDS("PF1") = "PF:01"
	 *         convertMAD_TimezoneCodeToMDS("MX1A") = "MX:1A" (but MDS uses MX:05)
	 */
	public static String convertMAD_TimezoneCodeToMDS(String aCode) {

		if (aCode == null || aCode.length() < 3) {
			throw new IllegalArgumentException(
					String.format("Illegal MAD Code '%s': Must have at least 3 characters!", aCode));
		}

		String countryCode = aCode.substring(0, 2);
		String tzNo = aCode.substring(2);
		String result = countryCode + ":";

		if ("0".equals(tzNo)) {
			// Code of a Country that has only one Timezone
			result += "01";
		} else if (tzNo.length() == 1) {
			result += "0" + tzNo;
		} else {
			result += tzNo;
		}

		return result;
	}

	/**
	 * Convert a Local Date with Time to UTC using the local time offset to UTC
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} LocalDateTime localDateTime : input: Date with time to convert
	 * {param} String("+04:30") offsetTime : input: Time offset to use, e.g. -04:00,
	 * +04:00, 04:00
	 * 
	 * {example} convertLocalTimeToUTC(aDate, "-03:00")
	 */
	public static OffsetDateTime convertLocalTimeToUTC(LocalDateTime localDateTime, String offsetTime) {
		String[] hoursMins = offsetTime.split(":");

		if (hoursMins.length != 2) {
			throw new IllegalArgumentException(
					String.format("Unsupport Offset Time ('%s'), required format is [+-]HH:MM", offsetTime));
		}

		if (localDateTime == null) {
			localDateTime = LocalDateTime.now();
		}

		if (!offsetTime.startsWith("-") && !offsetTime.startsWith("+"))
			offsetTime = "+" + offsetTime;

		DateTimeFormatter df = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm");
		String dateString = df.format(localDateTime) + ":00:000" + offsetTime;

		DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss:SSSXXXXX");
		OffsetDateTime localOffsetDateTime = OffsetDateTime.parse(dateString, DATE_TIME_FORMATTER);
		OffsetDateTime utcOffsetDateTime = localOffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);

		return utcOffsetDateTime;
	}

	/**
	 * Convert a Date with Time and Time Zone Offset to UTC.
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} OffsetDateTime dateTime : input: Date with time to convert
	 * 
	 * Note: Used in job MDM_subjob_enrich_DST_Time, tJavaRow1
	 */
	public static OffsetDateTime convertToUTC(OffsetDateTime dateTime) {

		if (dateTime == null) {
			dateTime = OffsetDateTime.now();
		}

		OffsetDateTime utcOffsetDateTime = dateTime.withOffsetSameInstant(ZoneOffset.UTC);

		return utcOffsetDateTime;
	}

	/**
	 * Convert a UTC Time to another timzone given by its offset to UTC
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} Date localDateTime : input: Date with time to convert {param}
	 * String("-HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00,
	 * 04:00
	 * 
	 * {example} convertUTCTimeToLocal(aDate, "+03:30")
	 */
	public static OffsetDateTime convertUTCTimeToLocal(Date localDateTime, String offsetTime) {
		String[] hoursMins = offsetTime.split(":");

		if (hoursMins.length != 2) {
			throw new IllegalArgumentException(
					String.format("Unsupport Offset Time ('%s'), required format is [+-]HH:MM", offsetTime));
		}

		if (localDateTime == null) {
			localDateTime = new Date();
		}

		int hours = Integer.parseInt(hoursMins[0]);
		int minutes = Integer.parseInt(hoursMins[1]);

		if (hours < 0)
			minutes = -minutes;

		if (!offsetTime.startsWith("-") && !offsetTime.startsWith("+"))
			offsetTime = "+" + offsetTime;

		SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy'T'HH:mm");
		String dateString = df.format(localDateTime) + ":00:000+00:00";

		DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/uuuu'T'HH:mm:ss:SSSXXXXX");
		OffsetDateTime utcOffsetDateTime = OffsetDateTime.parse(dateString, DATE_TIME_FORMATTER);
		OffsetDateTime localOffsetDateTime = utcOffsetDateTime
				.withOffsetSameInstant(ZoneOffset.ofHoursMinutes(hours, minutes));

		return localOffsetDateTime;
	}

	/**
	 * Adjusts a given date in MDS notation to a regular date. Interprets MDS
	 * special time 23:59 as 00:00 same day and 00:01 as 00:00 same day. MDS uses
	 * 00:01 for DST start time at the same day and 23:59 for DST end time at the
	 * same day. Example: the DST in Iran (IR:01) ends on Sep, 22th 2021 00:00 UTC,
	 * but MDS uses Sep, 22th, 23:59! Note: Used in job MDM_subjob_enrich_DST_Time,
	 * tJavaRow1
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} OffsetDateTime aDate : input: Date to use adjust {returns} Date
	 * assembled from input parameters {example} adjustMDS_Date(aDate whith value
	 * 2021-02-28 23:59) returns a Date for 2021-03-01 00:00:00.000)
	 */
	public static OffsetDateTime adjustMDS_Date(OffsetDateTime aDate) {
		int hours = aDate.getHour();
		int minutes = aDate.getMinute();
		OffsetDateTime result = aDate.withSecond(0).withNano(0);

		if (hours == 0 && minutes == 1) {
			result = result.withMinute(0);
		} else if (hours == 23 && minutes == 59) {
			result = result.plusMinutes(1);
		}

		return result;
	}

	/**
	 * Assembles a DateTime object from a local Date and a local time string HH:mm
	 * that uses MDS special times 23:59 (00:00 next day) and 00:01 (00:00 same
	 * day).
	 * 
	 * {talendTypes} Date
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} LocalDate localDate : input: Date part to use {param} LocalTime
	 * localTime : input: Local Time to append to the date part {param} String
	 * offsetTime : input: Used to define the time zone of the LocalDate and
	 * LocalTime {returns} Date assembled from input parameters {example}
	 * assembleMDS_DateTime(aDate, "23:59", "+00:00") returns the same day at 00:00
	 * as aDate
	 * 
	 * @throws ParseException
	 */
	public static OffsetDateTime assembleMDS_DateTime(LocalDate localDate, LocalTime localTime, String offsetTime)
			throws ParseException {

		if (!offsetTime.startsWith("-") && !offsetTime.startsWith("+"))
			offsetTime = "+" + offsetTime;

		OffsetDateTime dateTime = OffsetDateTime.of(localDate, localTime, ZoneOffset.of(offsetTime));
		OffsetDateTime result = adjustMDS_Date(dateTime);

		return result;
	}

	/**
	 * Created for DST enrichment: Calculates the UTC time from a given local MDS
	 * DST start/end time using the offsetTime given
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} Object resultDateFormat : input: Date format to return, either a
	 * format string or a SimpleDateFormat. {param} LocalDateTime localDateTime:
	 * input: Date with time to convert {param} String("+HH:mm") offsetTime : input:
	 * Time offset to use, e.g. -04:00, +04:00, 04:00
	 * 
	 * @throws ParseException
	 */
	public static String convertLocalTimeToUTC(Object resultDateFormat, LocalDateTime localDateTime, String offsetTime)
			throws ParseException {
		String result = null;

		OffsetDateTime resultDate = convertLocalTimeToUTC(localDateTime, offsetTime);

		result = formatDate(resultDateFormat, resultDate).toUpperCase();
		return result;
	}

	/**
	 * Created for CABS timezone export: Calculates the UTC time from a given local
	 * MDS DST start/end time using the offsetTime given
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} Object resultDateFormat : input: Date format to return, either a
	 * format string or a SimpleDateFormat. {param} Date localDate : input: Date to
	 * convert {param} Object("HH:mm") localTime : input: Local Time to convert as
	 * String, e.g. 02:00 or a Date object to extract this info from {param}
	 * String("+HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00,
	 * 04:00
	 * 
	 * {example} calculate_UTC_TimeFromLocal("ddMMMyy_HHmm", aDate, "23:59",
	 * "+04:30")
	 * 
	 * @throws ParseException
	 */
	public static String convertLocalDateTimeToUTC(Object resultDateFormat, Date localDate, Object localTime,
			String offsetTime) throws ParseException {
		String result = null;
		String localTimeString = null;
		String[] hoursMins = offsetTime.split(":");

		if (hoursMins.length != 2) {
			throw new IllegalArgumentException(
					String.format("Unsupport Local Time ('%s'), required format is HH:mm", localTimeString));
		}

		// ZoneId z = ZoneId.of("UTC"); // causes all TCs to fail
		ZoneId z = ZoneId.systemDefault();
		LocalDate ld = localDate.toInstant().atZone(z).toLocalDate();
		LocalTime lt = null;

		if (localTime instanceof Date) {
			lt = ((Date) localTime).toInstant().atZone(z).toLocalTime();
		} else if (localTime instanceof String) {
			localTimeString = (String) localTime + ":00";

			lt = LocalTime.parse(localTimeString);
		} else {
			throw new IllegalArgumentException("Local time argument type not supported");
		}

		OffsetDateTime localDateTime = assembleMDS_DateTime(ld, lt, offsetTime);
		OffsetDateTime odt = convertToUTC(localDateTime);

		result = formatDate(resultDateFormat, odt).toUpperCase();
		return result;
	}

	/**
	 * Formats a Date into a date/time string.
	 *
	 * @param aFormatter the pattern to format.
	 * @param aDate      the time value to be formatted into a time string.
	 * @return the formatted time string.
	 *
	 *         {talendTypes} String
	 *
	 *         {Category} LCAG-DateTime
	 *
	 *         {param} Object("yyyy-MM-dd HH:mm:ss") aFormatter : the pattern to
	 *         format. May be a formatting String, a java DateFormat or a
	 *         DateTimeFormatter
	 *
	 *         {param} Object(aDate) aDate : the date und time value to be formatted
	 *         into a date time string. May be of Type Date or OffsetDateTime
	 *
	 *         {example} formatDate("yyyy-MM-dd", new Date())
	 */

	public synchronized static String formatDate(Object aFormatter, Object aDate) {
		String result = null;
		DateFormat df = null;
		DateTimeFormatter formatter = null;

		if (aFormatter instanceof String) {
			String fmt = (String) aFormatter;

			// Use the best suited formatter next! Otherwise you may get problems in time
			// zone handling!!
			if (aDate instanceof Date) {
				df = new SimpleDateFormat(fmt.replaceAll("u", "y"), Locale.ENGLISH);
			}
			if (aDate instanceof OffsetDateTime) {
				formatter = DateTimeFormatter.ofPattern(fmt.replaceAll("y", "u"), Locale.ENGLISH);
			}
		} else if (aFormatter instanceof DateFormat) {
			df = (DateFormat) aFormatter;
		} else if (aFormatter instanceof DateTimeFormatter) {
			formatter = (DateTimeFormatter) aFormatter;
		} else {
			throw new IllegalArgumentException("Formatter argument type not supported");
		}

		if (aDate instanceof Date) {
			Date d = (Date) aDate;

			if (df != null) {
				result = df.format(d);
			} else if (formatter != null) {
				result = formatter.format(d.toInstant());
			}
		} else if (aDate instanceof OffsetDateTime) {
			OffsetDateTime d = (OffsetDateTime) aDate;

			if (formatter != null) {
				result = formatter.format(d);
			} else if (df != null) {
				result = df.format(Date.from(d.toInstant()));
			}
		} else {
			throw new IllegalArgumentException("Date argument type not supported");
		}

		return result;
	}

	@Test
	void testIsMultiTimezoneCountry() {
		assertFalse(isMultiTimezoneCountry("DE:01"));
		assertFalse(isMultiTimezoneCountry("DE0"));
		assertTrue(isMultiTimezoneCountry("AU2A"));
		assertTrue(isMultiTimezoneCountry("US:02"));

		assertThrows(IllegalArgumentException.class, () -> {
			isMultiTimezoneCountry(null);
		}, "Argument must not be null!");
		assertThrows(IllegalArgumentException.class, () -> {
			isMultiTimezoneCountry("");
		}, "Argument musts hav at least 2 characters!");
	}

	@Test
	void testConvertMAD_TimezoneCodeToMDS() {
		assertEquals("DE:01", convertMAD_TimezoneCodeToMDS("DE0"), "Country with only one TZ rule");
		assertEquals("PF:01", convertMAD_TimezoneCodeToMDS("PF1"), "Country with multiple TZ rule");
		assertEquals("RU:11", convertMAD_TimezoneCodeToMDS("RU11"), "Country with multiple TZ rule");
		assertEquals("MX:1B", convertMAD_TimezoneCodeToMDS("MX1B"),
				"Special Timezone: Function results in expected but illegal MDS code");

		assertThrows(IllegalArgumentException.class, () -> {
			convertMAD_TimezoneCodeToMDS(null);
		}, "Argument must not be null!");
		assertThrows(IllegalArgumentException.class, () -> {
			convertMAD_TimezoneCodeToMDS("AB");
		}, "Argument musts hav at least 3 characters!");
	}

	@Test
	void testConvertMDS_TimezoneCodeToMAD() {
		assertEquals("DE0", convertMDS_TimezoneCodeToMAD("DE:01"), "Country with only one TZ rule");
		assertEquals("PF1", convertMDS_TimezoneCodeToMAD("PF:01"), "Country with multiple TZ rule");
		assertEquals("US1", convertMDS_TimezoneCodeToMAD("US:01"), "Country with multiple TZ rule");
		assertEquals("US4", convertMDS_TimezoneCodeToMAD("US:04"), "Country with multiple TZ rule");
		assertEquals("RU11", convertMDS_TimezoneCodeToMAD("RU:11"), "Country with multiple TZ rule");
		assertEquals("MX8", convertMDS_TimezoneCodeToMAD("MX:08"),
				"Special Timezone: Function results in expected but illegal MDS code");

		assertThrows(IllegalArgumentException.class, () -> {
			convertMDS_TimezoneCodeToMAD(null);
		}, "Argument must not be null!");
		assertThrows(IllegalArgumentException.class, () -> {
			convertMDS_TimezoneCodeToMAD("AB:4");
		}, "Argument must hav at least 5 characters!");
	}

	@Test
	void testDouble2UtcOffset() {
		assertEquals(double2UtcOffset(6.0), "+06:00");
		assertEquals(double2UtcOffset(-6.0), "-06:00");
		assertEquals(double2UtcOffset(-6.5), "-06:30");
		assertNull(double2UtcOffset(null));

		assertThrows(IllegalArgumentException.class, () -> {
			double2UtcOffset(-24.0);
		}, "Should throw this Exception to prevent strange in MAD Timezone offset conversion data");
		assertThrows(IllegalArgumentException.class, () -> {
			double2UtcOffset(24.0);
		}, "Should throw this Exception to prevent strange in MAD Timezone offset conversion data");
	}

	@Test
	void testUtcOffset2Double() {
		Double d;

		d = 6.0;
		assertEquals(d, utcOffset2Double("+06:00"));
		d = -6.0;
		assertEquals(d, utcOffset2Double("-06:00"));
		d = -6.5;
		assertEquals(d, utcOffset2Double("-06:30"));
		d = -25.0;
		assertEquals(d, utcOffset2Double("-25:00"));
		assertNull(utcOffset2Double(null));

		assertThrows(NumberFormatException.class, () -> {
			utcOffset2Double("06:00");
		}, "Offset without sign is not yet supported");
	}

	@Test
	void testToDate() throws ParseException {
		// Next tests refer to toDate(Object) function:
		Date result = toDate(fromDate1);
		Date cmpDate = DATE_FORMAT.parse("30-12-2000 00:00:00");
		assertEquals(cmpDate, result);

		String s = "";
		result = toDate(s);
		assertNull(result);

		result = toDate("2001-01-03");
		assertEquals(toDate1, result);

		result = toDate("2001-01-03_FileName");
		assertEquals(toDate1, result);

		// Next tests refer to toDate(Sting, format...) function:
		result = toDate("2001-01-03", "yyyy-MM-dd");
		assertEquals(toDate1, result);

		result = toDate("Sun Sep 26 00:00:00 UTC 2021".substring(4), "MMM dd HH:mm:ss z yyyy");
		assertNotNull(result, "Should parse Excel CSV date format");

		result = toDate("2001-01-03 22:22:22", "yyyy-MM-dd");
		assertEquals(toDate1, result, "Should ignore time and reset time to 00:00:00");

		result = toDate("2001-01-03 22:22:22", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss");
		assertEquals(toDate1, result, "Matches first pattern, thus ignores time");

		result = toDate("30-12-2000 20:00:00", "dd-MM-yyyy HH:mm:ss", "yyyy-MM-dd");
		assertEquals(fromDate1, result);

		result = toDate("03.01.2001_Filename", "dd.MM.yyyy HH:mm:ss", "dd.MM.yyyy");
		assertEquals(toDate1, result, "Should match second pattern because no time given");

		result = toDate("FileName_03.01.2001", "dd.MM.yyyy HH:mm:ss", "dd.MM.yyyy");
		assertNull(result, "Does not match because does not start with a date");

		// FIXME: Next testcases still fails (since 2021-07-14)
		result = toDate("2001-01-03", "dd-MM-yyyy", "yyyy-MM-dd");
		assertEquals(toDate1, result, "Should match second format");
	}

	@Test
	void testFormatDate() {
		Date d = toDate("2001-01-03 22:22:22", "yyyy-MM-dd");

		assertEquals("2001-01-03", formatDate("yyyy-MM-dd", d));
		assertEquals("2001-01-03", formatDate(LcagDateTime.MDM_DATE_FORMAT, d));

		LocalDateTime ldt = LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());
		;

		OffsetDateTime odt = convertLocalTimeToUTC(ldt, "-01:00");
		assertEquals("2001-01-03", formatDate(LcagDateTime.MDM_DATE_FORMAT, odt));
	}

	@Test
	void testCountDays() throws ParseException {
		long result;

		result = countDays(fromDate1, toDate1);
		assert result == 5;
		result = countDays("2018-02-02", "2018-02-02");
		assert result == 1;
	}

	/**
	 * Needed in MDM_export_Timezone job:
	 * 
	 * @throws ParseException
	 */
	@Test
	void testFormatDateWithEnglishMonth() throws ParseException {
		Date d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss'Z'").parse("2021-12-28 16:28:00Z");

		String cabsDate = DATE_FORMAT_CABS.format(d).toUpperCase();
		assertEquals("28DEC21", cabsDate, "Must be in English");

		String utcDate = new SimpleDateFormat("yyyy-MMM-dd HH:mm", Locale.ENGLISH).format(d);
		assertEquals("2021-Dec-28 16:28", utcDate, "Must be in English with same time");

		// Test using Talend function:
		utcDate = TalendDate.formatDateLocale("ddMMMyy", d, "en");
		assertEquals("28DEC21", cabsDate, "Must be in English");
	}

	/**
	 * Needed in MDM_export_Timezone job:
	 * 
	 * @throws ParseException
	 */
	@Test
	void testFormatDstDateTimeWithOffset() throws ParseException {
		Date d = null;
		String aDateStr = null;

		// Test UTC =============================================
		TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
		// Test UTC cutting Offset:
		d = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2021-12-28 20:35-04:30");
		assertEquals(d.toString(), "Tue Dec 28 20:35:00 UTC 2021", "Java Default Format");

		// Test UTC with offset:
		d = new SimpleDateFormat("yyyy-MM-dd HH:mmXXX").parse("2021-12-28 20:35-04:30");
		assertEquals(d.toString(), "Wed Dec 29 01:05:00 UTC 2021", "Java Default Format");

		// Test Local TZ conversion =============================
		// Test conversion to DE time:
		d = new SimpleDateFormat("yyyy-MM-dd HH:mmXXX").parse("2021-12-28 20:35-04:30");
		TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin"));
		aDateStr = new SimpleDateFormat("yyyy-MM-dd HH:mmXXX").format(d);
		assertEquals(aDateStr, "2021-12-29 02:05+01:00", "German Winter Time with UTC Offset");

	}

	@Test
	void testAdjustMDS_Date() throws ParseException {
		DateTimeFormatter df = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ssXXXXX");
		DateTimeFormatter dfMDS = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mmXXXXX");
		String result = null;

		OffsetDateTime adjustMDS_Date = adjustMDS_Date(OffsetDateTime.parse("2021-02-28 23:59+00:00", dfMDS));
		result = df.format(adjustMDS_Date);
		assertEquals("2021-03-01 00:00:00Z", result);

		result = df.format(adjustMDS_Date(OffsetDateTime.parse("2021-06-01 00:01Z", dfMDS)));
		assertEquals("2021-06-01 00:00:00Z", result);

		result = df.format(adjustMDS_Date(OffsetDateTime.parse("2021-06-01 17:29:59Z", df)));
		assertEquals("2021-06-01 17:29:00Z", result);

		result = df.format(adjustMDS_Date(OffsetDateTime.parse("2021-06-01 17:29:59+04:00", df)));
		assertEquals("2021-06-01 17:29:00+04:00", result);

		// Test null pointer passing: Should throw this Exception to prevent strange
		// behavior for MDS data
		assertThrows(NullPointerException.class, () -> {
			adjustMDS_Date(null);
		}, "Should throw this Exception to prevent strange behavior for MDS data");
	}

	@Test
	void testAssembleMDS_DateTime() throws ParseException {
		DateTimeFormatter df = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
		LocalDateTime aDate = LocalDateTime.parse("2021-06-01T12:12:12");

		String result = null;

		result = assembleMDS_DateTime(aDate.toLocalDate(), LocalTime.parse("23:59"), "+00:00").format(df);
		assertEquals("2021-06-02 00:00:00", result);

		result = assembleMDS_DateTime(aDate.toLocalDate(), LocalTime.parse("00:01"), "+00:00").format(df);
		assertEquals("2021-06-01 00:00:00", result);
	}

	@Test
	void testConvertToUTC() throws ParseException {
		DateTimeFormatter df = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ssXXXXX");
		DateTimeFormatter dfMins = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mmXXXXX");
		String result = null;

		OffsetDateTime adjustMDS_Date = convertToUTC(OffsetDateTime.parse("2021-02-28 23:59+00:00", dfMins));
		result = df.format(adjustMDS_Date);
		assertEquals("2021-02-28 23:59:00Z", result);

		result = df.format(convertToUTC(OffsetDateTime.parse("2021-06-01 17:29:59+04:00", df)));
		assertEquals("2021-06-01 13:29:59Z", result);

		// Test null pointer passing: Should throw this Exception to prevent strange
		// behavior for MDS data
		OffsetDateTime now = OffsetDateTime.now(); // Returns local German time

		now = OffsetDateTime.now(ZoneId.of("Z")); // Returns UTC time now
		result = dfMins.format(convertToUTC(null));

		String nowString = dfMins.format(now);
		assertEquals(nowString, result, "Passing null pointer should return current time");
	}

	@Test
	void testConvertLocalDateTimeToUTC() throws ParseException {
		String gmtOffset, dstOffset, fromTime, toTime;
		Date fromDate, toDate;
		String result;

		// ==============================================================
		// TEST Airport that switches to UTC TZ, SPC, Puerto de la Cruz, Spain, TZ Code
		// ES:02
		gmtOffset = "00:00";
		dstOffset = "+01:00";

		// Test start of Summer time 29.03.2020 01:00 set to 02:00
		fromDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-03-28");
		fromTime = "01:00"; // MDS uses this value
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-10-31");
		toTime = "02:00"; // MDS uses this value

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", fromDate, fromTime, gmtOffset);
		assertEquals("28MAR21_0100", result);
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("31OCT21_0100", result);

		// TEST Airport that is always in UTC TZ, LHR, England, TZ Code GB:01
		gmtOffset = "00:00";
		dstOffset = "+01:00";

		// Test start of Summer time 28.03.2021 01:00 set to 02:00
		fromDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-03-28");
		fromTime = "01:00"; // MDS uses this value
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-10-31");
		toTime = "02:00"; // MDS uses this value

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", fromDate, fromTime, gmtOffset);
		assertEquals("28MAR21_0100", result);
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("31OCT21_0100", result);

		// Continue TEST of LHR on Day of DST change:
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-10-31");
		toTime = "02:00"; // Time set from 2:00 back one hour to 1:00
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("31OCT21_0100", result);

		toTime = "01:00"; // Time at 1:00 is still Time and maps 00:00 to UTC:
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("31OCT21_0000", result);

		// TEST Airport SCL, Chile, TZ Code CL:01
		gmtOffset = "-04:00";
		dstOffset = "-03:00";
		fromDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-09-05");
		fromTime = "00:01"; // MDS uses this value
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2022-04-01");
		toTime = "23:59"; // MDS uses this value

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", fromDate, fromTime, gmtOffset);
		assertEquals("05SEP21_0400", result);
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("02APR22_0300", result);

		// TEST Airport JFK, USA, TZ Code US:01
		gmtOffset = "-05:00";
		dstOffset = "-04:00";
		fromDate = LcagDateTime.MDM_DATE_FORMAT.parse("2020-03-08");
		fromTime = "02:00"; // 02:00 local time
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2020-11-01");
		toTime = "02:00";

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", fromDate, fromTime, gmtOffset);
		assertEquals("08MAR20_0700", result);
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("01NOV20_0600", result);

		// TEST Airport ALP, Syrien, TZ Code SY:01
		gmtOffset = "+02:00";
		dstOffset = "+03:00";
		fromDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-03-26");
		fromTime = "00:01"; // MDS uses this value
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-10-29");
		toTime = "23:59"; // MDS uses this value

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", fromDate, fromTime, gmtOffset);
		assertEquals("25MAR21_2200", result);
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("29OCT21_2100", result);

		// TEST Airport AJK, Iran, TZ Code IR:01
		gmtOffset = "+03:30";
		dstOffset = "+04:30";
		fromDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-03-22");
		fromTime = "00:01"; // MDS uses this value
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2020-09-22");
		toTime = "23:59"; // MDS uses this value

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", fromDate, fromTime, gmtOffset);
		assertEquals("21MAR21_2030", result);
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		assertEquals("22SEP20_1930", result);

		// ==============================================================
		// TEST End of Summer Time in Countries (that switch at 00:00 UTC (IL, MD):
		// Compare To:
		// https://www.timeanddate.com/worldclock/converter.html?iso=20211031T001500&p1=tz_gmt&p2=83&p3=177&p4=676

		// TEST Airport that ..., TLV, Tel Aviv, Israel, TZ Code IL:01
		gmtOffset = "+02:00";
		dstOffset = "+03:00";

		// Test last day in Summer time 30.10.2021 LT 02:00 set back to 01:00
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-10-31");
		toTime = "01:59";

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		// assertEquals("30OCT21_2359", result);

		toTime = "02:00";
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, gmtOffset);
		// assertEquals("30OCT21_2300", result);

		toTime = "02:01";
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, gmtOffset);
		// assertEquals("31OCT21_0001", result);

		// TEST Airport that ..., KIV, Chisnau, Moldavia, TZ Code MD:01
		gmtOffset = "+02:00";
		dstOffset = "+03:00";

		// Test last day in Summer time 31.10.2021 LT 03:00 set back to 02:00
		toDate = LcagDateTime.MDM_DATE_FORMAT.parse("2021-10-31");
		toTime = "02:45";

		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, dstOffset);
		// assertEquals("30OCT21_2345", result);

		toTime = "03:00";
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, gmtOffset);
		// assertEquals("31OCT21_0000", result);

		toTime = "03:15";
		result = convertLocalDateTimeToUTC("ddMMMyy_HHmm", toDate, toTime, gmtOffset);
		// assertEquals("31OCT21_0015", result);

	}

	@Test
	void testCompareDatePeriods() throws ParseException {
		Date veryOldDate = LcagDateTime.toDate("1965-02-28");
		Date oldDate = LcagDateTime.toDate("2020-02-02");
		Date today = new Date();
		Date futureDate = LcagDateTime.addDays(today, 30);

		// Explanation: "1111" = Period 1, "2222" = Period 2, "=" overlap, "_": gap
		// Testing Period containment (Test pattern : "11222211")

		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* contains */ oldDate, today).CONTAINS,
				"1st Period shoud contain 2nd Period");
		assertTrue(LcagDateTime.compareDatePeriods(oldDate, today, /* is contained in */ veryOldDate,
				futureDate).IS_CONTAINED);
		assertFalse(LcagDateTime.compareDatePeriods(veryOldDate, futureDate,
				/* does not contains at the start */ oldDate, today).CONTAINS_AT_START);

		// Testing Equal periods (Test pattern : "====")
		assertTrue(
				LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate, futureDate).EQUAL);
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate,
				futureDate).CONTAINS);
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate,
				futureDate).CONTAINS_AT_START);
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate,
				futureDate).CONTAINS_AT_END);
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate,
				futureDate).IS_CONTAINED);
		assertTrue(
				LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate, futureDate).OVERLAP);
		assertFalse(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate,
				futureDate).OVERLAPS_WITH_START, "just true if party overlaps");
		assertFalse(LcagDateTime.compareDatePeriods(veryOldDate, futureDate, /* equals */ veryOldDate,
				futureDate).OVERLAPS_WITH_END, "just true if party overlaps");

		// Test overlapping period: (Test pattern : "11==22")
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, today, /* equals */ oldDate, futureDate).OVERLAP);
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, today, /* equals */ oldDate,
				futureDate).OVERLAPS_WITH_START);
		assertFalse(LcagDateTime.compareDatePeriods(veryOldDate, today, /* equals */ oldDate,
				futureDate).OVERLAPS_WITH_END);

		// Testing gaps (Test pattern : "1111____2222"):
		assertTrue(
				LcagDateTime.compareDatePeriods(veryOldDate, oldDate, /* has a gap with */ today, futureDate).HAVE_GAP);
		assertTrue(LcagDateTime.compareDatePeriods(veryOldDate, oldDate, /* has a gap after first */ today,
				futureDate).HAS_GAP_AFTER);

		// Testing earlier, later:
		assertTrue(
				LcagDateTime.compareDatePeriods(veryOldDate, oldDate, /* is earlier than */ today, futureDate).EARLIER); // Test
																															// pattern
																															// :
																															// "1111____2222"
		assertTrue(LcagDateTime.compareDatePeriods(today, futureDate, /* is later than */ veryOldDate, oldDate).LATER); // Test
																														// pattern
																														// :
																														// "2222____1111"
	}

	@Test
	void testDateInbetween() throws ParseException {
		// Function used in job MDM_export_Timezone
		boolean result;
		Date fromDate = toDate("2021-01-02");
		Date toDate = toDate("2021-12-31");

		result = dateInbetween(fromDate, fromDate, fromDate);
		assertTrue(result, "Border Test: A date should be inbetween itself");

		result = dateInbetween(fromDate, toDate("2021-06-15"), toDate);
		assertTrue(result, "A date within a period should be inbetween");

		result = dateInbetween(fromDate, toDate("2021-01-01"), toDate);
		assertFalse(result, "A date before the period should NOT be inbetween");

		result = dateInbetween(fromDate, toDate("2022-01-01"), toDate);
		assertFalse(result, "A date after the period should NOT be inbetween");

		result = dateInbetween(null, toDate("2021-06-15"), toDate);
		assertFalse(result, "Invalid (null-value) Start of period should return false");

		result = dateInbetween(fromDate, toDate("2021-06-15"), null);
		assertFalse(result, "Invalid (null-value) End of period should return false");

		result = dateInbetween(fromDate, null, toDate);
		assertFalse(result, "A null-value date shall never be inbetween a valid date period");
	}

	@Test
	void testSecureParseDate() {
		assertNotNull(secureParseDate("yyyy-MM-dd", "2021-07-22"));
		assertNull(secureParseDate("yyyy-MM-dd", null));
		assertNull(secureParseDate(null, "2021-07-22"));
		assertNull(secureParseDate("Illegal!", "2021-07-22"),
				"Should also return null for string that cannot be parsed as a valid date");
	}

}
