<?php
/**
 * Copyright 2011-2020 Nick Korbel
 *
 * This file is part of Booked Scheduler.
 *
 * Booked Scheduler 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.
 *
 * Booked Scheduler 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 Booked Scheduler.  If not, see <http://www.gnu.org/licenses/>.
 */

require_once(ROOT_DIR . 'lib/Common/namespace.php');

class DateTests extends TestBase
{
	private $tz;
	private $datestring;

	public function setUp(): void
	{
		parent::setup();

		$this->datestring = date(DATE_W3C, time());
		$this->tz = new DateTimeZone('UTC');
	}

	public function testCanGetNow()
	{
		$format = 'd m y H:i:s';

		Date::_ResetNow();
		$now = Date::Now();
		$datenow = new DateTime(date(Date::SHORT_FORMAT, time()));

		$this->assertEquals($datenow->format($format), $now->Format($format));
	}

	public function testCanAddDays()
	{
		$format = 'd m y H:i:s';

		$hour = 11;
		$minute = 40;
		$second = 22;
		$month = 3;
		$day = 21;
		$year = 2007;

		$string = "$year-$month-$day $hour:$minute:$second";
		$now = new Date($string);

		$expectedTS = mktime($hour, $minute, $second, $month, $day + 20, $year);

		$twentyDaysDate = $now->AddDays(20);

		$this->assertEquals($expectedTS, $twentyDaysDate->Timestamp());
	}

	public function testCanSubtractDays()
	{
		$format = 'd m y H:i:s';

		$hour = 11;
		$minute = 40;
		$second = 22;
		$month = 3;
		$day = 21;
		$year = 2007;

		$now = new Date("$year-$month-$day $hour:$minute:$second");

		$expectedTS = mktime($hour, $minute, $second, $month, $day - 20, $year);
		$twentyDaysDate = $now->AddDays(-20);

		$this->assertEquals($expectedTS, $twentyDaysDate->Timestamp());
	}

	public function testCanGetAsDateTime()
	{
		$now = new Date($this->datestring);
		$datenow = new DateTime($this->datestring);

		$parts = date_parse($datenow->format(DATE_W3C));

		$exptected = mktime(
				$parts['hour'],
				$parts['minute'],
				$parts['second'],
				$parts['month'],
				$parts['day'],
				$parts['year']
		);

		$this->assertEquals($exptected, $now->Timestamp());
	}

	public function testCanConvertToTimezone()
	{
		$format = 'd m y H:i:s';

		$now = new Date($this->datestring);

		$datetime = new DateTime($this->datestring);
		$datetime->setTimezone(new DateTimeZone('US/Eastern'));

		$expected = $datetime->format($format);
		$adjusted = $now->ToTimezone("US/Eastern");

		$this->assertEquals($expected, $adjusted->Format($format));
	}

	public function testDateGetsAdjustedIntoProvidedTimezone()
	{
		$format = 'd m y H:i:s';
		$tzName = 'US/Eastern';
		$baseTz = new DateTimeZone($tzName);

		$actual = new Date($this->datestring, $tzName);

		$datetime = new DateTime($this->datestring, $baseTz);

		$expected = $datetime->format($format);

		$this->assertEquals($expected, $actual->Format($format));
	}

	public function testGmtConvertsDateToGMT()
	{
		$format = 'd m y H:i:s';

		$date = new Date($this->datestring);

		$datetime = new DateTime($this->datestring);
		$datetime->setTimezone(new DateTimeZone('UTC'));

		$expected = $datetime->format($format);

		$this->assertEquals($expected, $date->ToUtc()->Format($format));
	}

	public function testDatePartsAreParsedCorrectly()
	{
		$hour = 11;
		$minute = 40;
		$second = 22;
		$month = 3;
		$day = 21;
		$year = 2007;

		$date = new Date("$year-$month-$day $hour:$minute:$second", 'US/Central');

		$this->assertEquals($hour, $date->Hour());
		$this->assertEquals($minute, $date->Minute());
		$this->assertEquals($second, $date->Second());
		$this->assertEquals($month, $date->Month());
		$this->assertEquals($day, $date->Day());
		$this->assertEquals($year, $date->Year());


		$adjusted = $date->ToTimezone('US/Eastern');

		$this->assertEquals($hour + 1, $adjusted->Hour());
		$this->assertEquals($minute, $adjusted->Minute());
		$this->assertEquals($second, $adjusted->Second());
		$this->assertEquals($month, $adjusted->Month());
		$this->assertEquals($day, $adjusted->Day());
		$this->assertEquals($year, $adjusted->Year());
	}

	public function testToDatabaseConvertsToGmtThenFormats()
	{
		$databaseformat = 'Y-m-d H:i:s';
		$date = new Date($this->datestring);

		$datetime = new DateTime($this->datestring);
		$datetime->setTimezone(new DateTimeZone('UTC'));

		$expected = $datetime->format($databaseformat);

		$this->assertEquals($expected, $date->ToDatabase());
	}

	public function testCanCreateTimeInServerTimezone()
	{
		$hour = 10;
		$min = 22;
		$sec = 21;

		$time = new Time($hour, $min, $sec);

		$this->assertEquals("$hour:$min:$sec", $time->ToString());
		$this->assertEquals($hour, $time->Hour());
		$this->assertEquals($min, $time->Minute());
		$this->assertEquals($sec, $time->Second());

		$time = new Time($hour, $min);

		$this->assertEquals("$hour:$min:00", $time->ToString());
		$this->assertEquals($hour, $time->Hour());
		$this->assertEquals($min, $time->Minute());
		$this->assertEquals(0, $time->Second());
	}

	public function testTimeCreatedInEasternCanBeConvertedToCentral()
	{
		$east = 'America/New_York';
		$central = 'America/Chicago';
		$hour = 10;
		$min = 10;
		$sec = 10;

		$time = Date::Create(2001, 1, 1, $hour, $min, $sec, $east);
		$converted = $time->ToTimezone($central);

		$this->assertEquals($hour - 1, $converted->Hour());
		$this->assertEquals($min, $converted->Minute());
		$this->assertEquals($sec, $converted->Second());

		$time = Date::Create(2001, 1, 1, 0, $min, $sec, $east);
		$converted = $time->ToTimezone($central);
		$this->assertEquals(23, $converted->Hour());

		$time = Date::Create(2001, 1, 1, 1, $min, $sec, $east);
		$converted = $time->ToTimezone($central);
		$this->assertEquals(0, $converted->Hour());
	}

	public function testCanParseTimeFromString()
	{
		$time = Time::Parse('10:11:12', 'UTC');

		$this->assertEquals(10, $time->Hour());
		$this->assertEquals(11, $time->Minute());
		$this->assertEquals(12, $time->Second());
		$this->assertEquals('UTC', $time->Timezone());

		$time = Time::Parse('13:11:12', 'UTC');

		$this->assertEquals(13, $time->Hour());
		$this->assertEquals(11, $time->Minute());
		$this->assertEquals(12, $time->Second());
		$this->assertEquals('UTC', $time->Timezone());

		$time = Time::Parse('10:11:12 PM', 'UTC');

		$this->assertEquals(22, $time->Hour());
		$this->assertEquals(11, $time->Minute());
		$this->assertEquals(12, $time->Second());
		$this->assertEquals('UTC', $time->Timezone());

		$time = Time::Parse('10:11:12 AM', 'UTC');

		$this->assertEquals(10, $time->Hour());
		$this->assertEquals(11, $time->Minute());
		$this->assertEquals(12, $time->Second());
		$this->assertEquals('UTC', $time->Timezone());

		$time = Time::Parse('10:11 AM', 'UTC');

		$this->assertEquals(10, $time->Hour());
		$this->assertEquals(11, $time->Minute());
		$this->assertEquals(00, $time->Second());
		$this->assertEquals('UTC', $time->Timezone());
	}

	public function testTimesCanBeCompared()
	{
		$date = Date::Parse('2010-01-01');
		$early = Time::Parse('10:11');
		$late = Time::Parse('12:11');

		$this->assertEquals(-1, $early->Compare($late, $date));
		$this->assertEquals(1, $late->Compare($early, $date));

		$early2 = Time::Parse('10:11', 'US/Central');
		$late2 = Time::Parse('10:11', 'US/Pacific');

		$this->assertEquals(-1, $early2->Compare($late2, $date));
	}

	public function testCanCompareDateOnlyEquality()
	{
		$date1 = Date::Parse('2008-01-01 11:00:00', 'US/Central');
		$date2 = Date::Parse('2008-01-01 11:00:00', 'US/Eastern');

		$this->assertTrue($date1->DateEquals($date2));

		$date1 = Date::Parse('2008-01-01 00:00:00', 'US/Central');
		$date2 = Date::Parse('2008-01-01 00:00:00', 'US/Eastern');

		$this->assertFalse($date1->DateEquals($date2));
	}

	public function testCreateBuildsDateObjectCorectly()
	{
		$date = Date::Create(2008, 10, 9, 8, 7, 6, 'US/Central');

		$this->assertEquals(2008, $date->Year());
		$this->assertEquals(10, $date->Month());
		$this->assertEquals(9, $date->Day());
		$this->assertEquals(8, $date->Hour());
		$this->assertEquals(7, $date->Minute());
		$this->assertEquals(6, $date->Second());
		$this->assertEquals('US/Central', $date->Timezone());
	}

	public function testCanCompareDateRelativity()
	{
		$date1 = Date::Parse('2008-01-01 11:00:00', 'US/Central');
		$date2 = Date::Parse('2008-01-01 11:00:00', 'US/Eastern');

		$this->assertEquals(0, $date1->DateCompare($date2));

		$date1 = Date::Parse('2008-01-01 00:00:00', 'US/Central');
		$date2 = Date::Parse('2008-01-01 00:00:00', 'US/Eastern');

		$this->assertEquals(1, $date1->DateCompare($date2), 'midnight eastern is 11pm central');

		$date1 = Date::Parse('2008-01-01 00:00:00', 'US/Central');
		$date2 = Date::Parse('2008-01-01 22:00:00', 'US/Pacific');

		$this->assertEquals(-1, $date1->DateCompare($date2), 'midnight pacific is 2 am central');
	}

	public function GetDateReturnsDateAsOfMidnight()
	{
		$date = new Date('2009-10-10 10:10:10');
		$onlyDate = $date->GetDate();

		$this->assertEquals(0, $onlyDate->Hour());
		$this->assertEquals(0, $onlyDate->Minute());
		$this->assertEquals(0, $onlyDate->Second());
	}

	public function testDateIsWithinRange()
	{
		$begin = Date::Create(2008, 9, 9, 10, 11, 12, 'UTC');
		$end = Date::Create(2008, 10, 9, 10, 11, 12, 'UTC');

		$range = new DateRange($begin, $end);

		$within = $begin->AddDays(10);
		$notWithin = $begin->AddDays(-10);
		$exactStart = $begin;
		$exactEnd = $end;

		$this->assertTrue($range->Contains($within));
		$this->assertTrue($range->Contains($exactStart));
		$this->assertTrue($range->Contains($exactEnd));
		$this->assertFalse($range->Contains($notWithin));
	}

	public function testDateRangeIsWithinRange()
	{
		$begin = Date::Create(2008, 9, 9, 10, 11, 12, 'UTC');
		$end = Date::Create(2008, 10, 9, 10, 11, 12, 'UTC');

		$range = new DateRange($begin, $end);

		$within = new DateRange($begin->AddDays(10), $end->AddDays(-10));
		$notWithin = new DateRange($begin->AddDays(-10), $end->AddDays(-1));

		$exact = new DateRange($begin, $end);

		$this->assertTrue($range->ContainsRange($within));
		$this->assertTrue($range->ContainsRange($exact));
		$this->assertFalse($range->ContainsRange($notWithin));
	}

	public function testDateRangeReturnsAllDatesForRangeWithoutTime()
	{
		$begin = Date::Create(2008, 9, 9, 10, 11, 12, 'UTC');
		$end = Date::Create(2008, 9, 12, 10, 11, 12, 'UTC');

		$range = new DateRange($begin, $end);

		$expected[] = $begin->GetDate();
		$expected[] = $begin->AddDays(1)->GetDate();
		$expected[] = $begin->AddDays(2)->GetDate();
		$expected[] = $begin->AddDays(3)->GetDate();

		$actual = $range->Dates();

//		foreach ($expected as $d)
//		{
//			echo $d->ToString();
//			echo "\n";
//		}
//
//		echo "\n";
//
//		foreach ($actual as $d)
//		{
//			echo $d->ToString();
//			echo "\n";
//		}

//		$this->assertEquals($expected, $actual);
		$this->assertEquals(count($expected), count($actual));
		$this->assertTrue($expected[0]->Equals($actual[0]), "Dates[0] are not equal");
		$this->assertTrue($expected[1]->Equals($actual[1]), "Dates[1] are not equal");
		$this->assertTrue($expected[2]->Equals($actual[2]), "Dates[2] are not equal");
		$this->assertTrue($expected[3]->Equals($actual[3]), "Dates[3] are not equal");
	}

	public function testCanGetDifferenceBetweenTwoDates()
	{
		$date1 = Date::Parse('2011-01-01 12:15:45', 'UTC');
		$date2 = $date1->AddDays(1);

		$diff = $date1->GetDifference($date2);

		$secondsInOneDay = 60 * 60 * 24;

		$this->assertEquals($diff->TotalSeconds(), $secondsInOneDay, '2nd date is one day greater than 1st date');
		$this->assertEquals(1, $diff->Days());
		$this->assertEquals(0, $diff->Hours());
		$this->assertEquals(0, $diff->Minutes());

		$newDate = $date1->ApplyDifference($diff);
		$this->assertTrue($date2->Equals($newDate));

		$laterDate = Date::Parse('2011-07-26 05:30:00', 'America/New_York');
		$earlierDate = Date::Parse('2011-07-22 17:40:41', 'America/Chicago');

		$diff = $laterDate->GetDifference($earlierDate);
		$shouldBeEarlier = $laterDate->ApplyDifference($diff);

		$this->assertTrue($earlierDate->Equals($shouldBeEarlier));
	}

	public function testCanGetDifferenceBetweenTwoTimes()
	{
		$date1 = Date::Parse('2011-01-01 12:15:45', 'utc');
		$date2 = Date::Parse('2011-01-01 13:16:46', 'utc');

		$diff = $date1->GetDifference($date2);

		$this->assertEquals(0, $diff->Days());
		$this->assertEquals(1, $diff->Hours());
		$this->assertEquals(1, $diff->Minutes());
	}

	public function testCanGetDifferenceFromTime()
	{
		$seconds = (12 * 60 * 60) + (60 * 35);
		$str1 = "0d12h35m";
		$diff1 = DateDiff::FromTimeString($str1);

		$this->assertEquals($seconds, $diff1->TotalSeconds());
		$this->assertEquals(12, $diff1->Hours());
		$this->assertEquals(35, $diff1->Minutes());
		$this->assertEquals(0, $diff1->Days());
		$this->assertEquals("12 hours 35 minutes", $diff1->__toString());

		$seconds2 = (4 * 24 * 60 * 60) + (12 * 60 * 60) + (60 * 35);
		$str2 = "4d12h35m";
		$diff2 = DateDiff::FromTimeString($str2);

		$this->assertEquals($seconds2, $diff2->TotalSeconds());
		$this->assertEquals(12, $diff2->Hours());
		$this->assertEquals(35, $diff2->Minutes());
		$this->assertEquals(4, $diff2->Days());
		$this->assertEquals("4 days 12 hours 35 minutes", $diff2->__toString());

		$diff3 = DateDiff::FromTimeString("25h0m");
		$this->assertEquals((25 * 60 * 60), $diff3->TotalSeconds());

		$this->assertEquals(0, DateDiff::FromTimeString("dhm")->TotalSeconds());
		$this->assertEquals(0, DateDiff::FromTimeString("hm")->TotalSeconds());
		$this->assertEquals(0, DateDiff::FromTimeString("dm")->TotalSeconds());
	}

	public function testDateRangeOverlapsIfStartsWithinOrEndsWithin()
	{
		$dr1 = DateRange::Create('2011-01-01 12:00:00', '2011-01-02 11:00:00', 'UTC');
		$dr2 = DateRange::Create('2011-01-01 12:00:00', '2011-01-02 10:00:00', 'UTC');

		$this->assertTrue($dr1->Overlaps($dr2));
	}

	public function testDateRangeOverlapsIfStartsAndEndsExactlyTheSameTimes()
	{
		$dr1 = DateRange::Create('2011-01-01 12:00:00', '2011-01-02 11:00:00', 'UTC');
		$dr2 = DateRange::Create('2011-01-01 12:00:00', '2011-01-02 11:00:00', 'UTC');

		$this->assertTrue($dr1->Overlaps($dr2));
	}

	public function testDateRangeDoesNotOverlapIfStartsAtEnd()
	{
		$dr1 = DateRange::Create('2011-01-01 12:00:00', '2011-01-02 11:00:00', 'UTC');
		$dr2 = DateRange::Create('2011-01-02 11:00:00', '2011-01-04 10:00:00', 'UTC');

		$this->assertFalse($dr1->Overlaps($dr2));
	}

	public function testDateRangeOccursOnDateIfAnyDateStartsOrEnds()
	{
		$range = DateRange::Create('2011-01-01 12:00:00', '2011-01-03 00:00:00', 'UTC');
		$d1 = Date::Parse('2011-01-02 02:02:02', 'UTC');
		$d2 = Date::Parse('2011-01-01 23:59:59', 'UTC');
		$d3 = Date::Parse('2011-01-03 00:00:00', 'UTC');

		$this->assertTrue($range->OccursOn($d1));
		$this->assertTrue($range->OccursOn($d2));
		$this->assertFalse($range->OccursOn($d3));
	}

	public function testNullDateTests()
	{
		$null = NullDate::Instance();

		$this->assertNull($null->ToDatabase());
		$this->assertEquals($null, $null->ToTimezone('anything'));
	}

	public function testCanParseExactWithTimezone()
	{
		$d = Date::Parse('2012-04-06 12:02:03', 'America/New_York');
		$iso = $d->ToIso();

		$d2 = Date::ParseExact($iso, 'America/New_York');

		$this->assertTrue($d->Equals($d2), $d->ToUtc()->ToString() . ' ' . $d2->ToString());
	}

	public function testJan31Bug()
	{
		$d = new Date('2013-01-31', 'America/Chicago');
		$this->assertEquals(31, $d->Day());
		$this->assertEquals(1, $d->Month());
		$this->assertEquals(2013, $d->Year());
	}

	public function testTimeIntervalParsesEmptyValue()
	{
		$interval = TimeInterval::Parse('dhm');

		$this->assertEquals(0, $interval->TotalSeconds());
	}

	public function testWhenComparingDatePartOnly_UseTimeOfSourceDate()
	{
		$d1 = new Date('2014-09-14 02:00:00', 'America/Chicago');
		$d2 = new Date('2014-09-14 00:00:00', 'Europe/Berlin');

		$this->assertFalse($d1->DateEquals($d2));

		$d1 = new Date('2014-09-14 00:00:00', 'America/Chicago');
		$d2 = new Date('2014-09-14 07:00:00', 'Europe/Berlin');

		$this->assertTrue($d1->DateEquals($d2));

		$d1 = new Date('2014-09-14 02:00:00', 'America/Chicago');
		$d2 = new Date('2014-09-15 00:00:00', 'Europe/Berlin');

		$this->assertTrue($d1->DateEquals($d2));

//		$d1 = new Date('2014-09-14 02:00:00', 'America/Chicago');
//		$d2 = new Date('2014-09-14 22:00:00', 'Europe/Berlin');
//
//		$this->assertFalse($d1->DateEquals($d2));
	}

	public function testCountsWeekdaysAndWeekends()
	{
		$timezone = 'America/Chicago';
		$begin = Date::Parse('2014-11-01 12:00', $timezone);
		$end = Date::Parse('2014-11-20 08:00', $timezone);
		$range = new DateRange($begin, $end, $timezone);

		$this->assertEquals(14, $range->NumberOfWeekdays());
		$this->assertEquals(6, $range->NumberOfWeekendDays());
	}

	public function testOneWeekday()
	{
		$timezone = 'America/Chicago';
		$begin = Date::Parse('2014-11-03 12:00', $timezone);
		$end = Date::Parse('2014-11-03 13:00', $timezone);
		$range = new DateRange($begin, $end, $timezone);

		$this->assertEquals(1, $range->NumberOfWeekdays());
		$this->assertEquals(0, $range->NumberOfWeekendDays());
	}

	public function testOneWeekendDay()
	{
		$timezone = 'America/Chicago';
		$begin = Date::Parse('2014-11-01 12:00', $timezone);
		$end = Date::Parse('2014-11-01 13:00', $timezone);
		$range = new DateRange($begin, $end, $timezone);

		$this->assertEquals(0, $range->NumberOfWeekdays());
		$this->assertEquals(1, $range->NumberOfWeekendDays());
	}

	public function testWhenEndDateIsAtMidnight_DoNotCountIt()
	{
		$timezone = 'America/Chicago';
		$begin = Date::Parse('2014-11-01 12:00', $timezone);
		$end = Date::Parse('2014-11-02 00:00', $timezone);
		$range = new DateRange($begin, $end, $timezone);

		$this->assertEquals(0, $range->NumberOfWeekdays());
		$this->assertEquals(1, $range->NumberOfWeekendDays());
	}

	public function testWebServiceDateDropsUsesIfSetTimezoneOffset()
	{
		$date = WebServiceDate::GetDate('2014-09-14T06:00:00-0000', $this->fakeUser);

		$this->assertEquals($date->Hour(), 6);
		$this->assertEquals($date->Timezone(), 'UTC');
	}

	public function testWebServiceParsesJustDate()
	{
		$date = WebServiceDate::GetDate('2014-09-14', $this->fakeUser);

		$this->assertEquals($date->Hour(), 0);
		$this->assertEquals($date->Timezone(), $this->fakeUser->Timezone);
	}

	public function testWebServiceParsesJustDateAndTime()
	{
		$date = WebServiceDate::GetDate('2014-09-14 06:00:00', $this->fakeUser);

		$this->assertEquals($date->Hour(), 6);
		$this->assertEquals($date->Timezone(), $this->fakeUser->Timezone);
	}

	public function testDateRangeReturnsAllDatesTimesForRange()
	{
		$begin = Date::Create(2008, 9, 9, 10, 11, 12, 'UTC');
		$end = Date::Create(2008, 9, 12, 10, 11, 12, 'UTC');

		$range = new DateRange($begin, $end);

		$expected[] = $begin;
		$expected[] = $begin->AddDays(1)->GetDate();
		$expected[] = $begin->AddDays(2)->GetDate();
		$expected[] = $end;

		$actual = $range->DateTimes();

		$this->assertEquals(count($expected), count($actual));
		$this->assertTrue($expected[0]->Equals($actual[0]), "Dates[0] are not equal");
		$this->assertTrue($expected[1]->Equals($actual[1]), "Dates[1] are not equal");
		$this->assertTrue($expected[2]->Equals($actual[2]), "Dates[2] are not equal");
		$this->assertTrue($expected[3]->Equals($actual[3]), "Dates[3] are not equal");
	}
}