// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
// 
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the 
// Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.

using System;
using System.Linq;
using Duplicati.Library.Utility;
using NUnit.Framework;

namespace Duplicati.UnitTest;

public class TimeZoneHelperTests : BasicSetupHelper
{
    private static string CETName = OperatingSystem.IsWindows() 
        // For some reason CET is not recognized on Windows
        // It has no effect outside the tests, as the string is not
        // present outside the tests
        ? "Central European Standard Time"
        : "CET";
    
    [Test]
    [Category("TimeZoneHelper")]
    public void GetTimeZoneFromAbbreviationTest()
    {
        var timeZone = TimeZoneHelper.FindTimeZone("EST");
        Assert.NotNull(timeZone);

        timeZone = TimeZoneHelper.FindTimeZone("Pacific Standard Time");
        Assert.NotNull(timeZone);

        timeZone = TimeZoneHelper.FindTimeZone(CETName);
        Assert.NotNull(timeZone);
    }

    [Test]
    [Category("TimeZoneHelper")]
    public void GetTimeZonesTest()
    {
        var timeZones = TimeZoneHelper.GetTimeZones();
        Assert.That(timeZones.Any(x => x.CurrentUtcOffset.Hours == -5));
        Assert.That(timeZones.Any(x => x.CurrentUtcOffset.Hours == -8));
        Assert.That(timeZones.Any(x => x.CurrentUtcOffset.Hours == 1));
        Assert.That(timeZones.Any(x => x.CurrentUtcOffset.Hours == 2));
    }

    [Test]
    [Category("TimeZoneHelper")]
    public void CheckTestSystemTimeZonesAreCorrect()
    {
        var timeZone = TimeZoneHelper.FindTimeZone(CETName);
        var localTime = new DateTimeOffset(2024, 3, 31, 1, 59, 59, timeZone.BaseUtcOffset);

        // Check that the system time is correct
        Assert.AreEqual(1, timeZone.GetUtcOffset(localTime).Hours);
        Assert.AreEqual(2, timeZone.GetUtcOffset(localTime.AddSeconds(2)).Hours);
    }

    [Test]
    [Category("TimeZoneHelper")]
    public void CheckAddIsStableOverDSTForward()
    {
        var timeZone = TimeZoneHelper.FindTimeZone(CETName);
        var localTime = new DateTimeOffset(2024, 3, 31, 1, 59, 59, timeZone.BaseUtcOffset);

        var utcTime = localTime.ToUniversalTime().DateTime;

        var adjustedTimeSeconds = timeZone.DSTAwareAddSeconds(utcTime, 2);
        var adjustedTimeHours = timeZone.DSTAwareAddHours(utcTime, 1);
        var adjustedTimeDays = timeZone.DSTAwareAddDays(utcTime, 1);
        Assert.AreEqual(utcTime.AddHours(-1).AddSeconds(2), adjustedTimeSeconds);
        Assert.AreEqual(utcTime.AddHours(-1).AddHours(1), adjustedTimeHours);
        Assert.AreEqual(utcTime.AddHours(-1).AddDays(1), adjustedTimeDays);
    }

    [Test]
    [Category("TimeZoneHelper")]
    public void CheckAddIsStableOverDSTBackward()
    {
        var timeZone = TimeZoneHelper.FindTimeZone(CETName);
        var dstOffset = timeZone.GetUtcOffset(new DateTimeOffset(2024, 10, 26, 14, 0, 0, timeZone.BaseUtcOffset));
        var localTime = new DateTimeOffset(2024, 10, 27, 2, 59, 59, dstOffset);

        var utcTime = localTime.ToUniversalTime().DateTime;

        var adjustedTimeSeconds = timeZone.DSTAwareAddSeconds(utcTime, 2);
        var adjustedTimeHours = timeZone.DSTAwareAddHours(utcTime, 1);
        var adjustedTimeDays = timeZone.DSTAwareAddDays(utcTime, 1);
        Assert.AreEqual(utcTime.AddHours(1).AddSeconds(2), adjustedTimeSeconds);
        Assert.AreEqual(utcTime.AddHours(1).AddHours(1), adjustedTimeHours);
        Assert.AreEqual(utcTime.AddHours(1).AddDays(1), adjustedTimeDays);
    }

    [Test]
    [Category("TimeZoneHelper")]
    public void CheckRepeatScheduleIsStableOverDSTForward()
    {
        var timeZone = TimeZoneHelper.FindTimeZone(CETName);
        var localTime = new DateTimeOffset(2024, 3, 30, 14, 0, 0, timeZone.BaseUtcOffset);
        var localTimeOfDay = localTime.TimeOfDay;

        var utcTime = localTime.ToUniversalTime().DateTime;

        var adjustedTimeSeconds = Timeparser.DSTAwareParseTimeInterval("2s", utcTime, timeZone, false);
        var adjustedTimeHours = Timeparser.DSTAwareParseTimeInterval("1h", utcTime, timeZone, false);
        var adjustedTimeDays = Timeparser.DSTAwareParseTimeInterval("1D", utcTime, timeZone, true);

        // Non-day-based times are not corrected for DST
        Assert.AreEqual(utcTime.AddSeconds(2), adjustedTimeSeconds);
        Assert.AreEqual(utcTime.AddHours(1), adjustedTimeHours);
        // Day-based times are corrected for DST
        Assert.AreEqual(utcTime.AddDays(1).AddHours(-1), adjustedTimeDays);

        // Since we adjust and keep the time of day, the time should be the same
        var adjustedLocalTimeDays = TimeZoneInfo.ConvertTimeFromUtc(adjustedTimeDays, timeZone);
        var adjustedLocalTimeOfDay = adjustedLocalTimeDays.TimeOfDay;
        Assert.AreEqual(localTimeOfDay, adjustedLocalTimeOfDay);

        var origTimeSeconds = timeZone.DSTAwareAddSeconds(adjustedTimeSeconds, -2);
        var origTimeHours = timeZone.DSTAwareAddHours(adjustedTimeHours, -1);
        var origTimeDays = timeZone.DSTAwareAddDays(adjustedTimeDays, -1);
        Assert.AreEqual(utcTime, origTimeSeconds);
        Assert.AreEqual(utcTime, origTimeHours);
        Assert.AreEqual(utcTime, origTimeDays);

        // Since we adjust and keep the time of day, the time should be the same, even when going back
        adjustedLocalTimeDays = TimeZoneInfo.ConvertTimeFromUtc(adjustedTimeDays, timeZone);
        adjustedLocalTimeOfDay = adjustedLocalTimeDays.TimeOfDay;
        Assert.AreEqual(localTimeOfDay, adjustedLocalTimeOfDay);
    }

    [Test]
    [Category("TimeZoneHelper")]
    public void CheckRepeatScheduleIsStableOverDSTBackward()
    {
        var timeZone = TimeZoneHelper.FindTimeZone(CETName);
        var dstOffset = timeZone.GetUtcOffset(new DateTimeOffset(2024, 10, 26, 14, 0, 0, timeZone.BaseUtcOffset));
        var localTime = new DateTimeOffset(2024, 10, 26, 14, 0, 0, dstOffset);
        var crossTime = new DateTimeOffset(2024, 10, 27, 2, 59, 59, dstOffset);
        var localTimeOfDay = localTime.TimeOfDay;

        var utcTime = localTime.ToUniversalTime().DateTime;
        var utcCrossTime = crossTime.ToUniversalTime().DateTime;

        var adjustedTimeSeconds = Timeparser.DSTAwareParseTimeInterval("2s", utcCrossTime, timeZone, false);
        var adjustedTimeHours = Timeparser.DSTAwareParseTimeInterval("1h", utcCrossTime, timeZone, false);
        var adjustedTimeDays = Timeparser.DSTAwareParseTimeInterval("1D", utcTime, timeZone, true);

        // Non-day-based times are not corrected for DST
        Assert.AreEqual(utcCrossTime.AddSeconds(2), adjustedTimeSeconds);
        Assert.AreEqual(utcCrossTime.AddHours(1), adjustedTimeHours);
        // Day-based times are corrected for DST
        Assert.AreEqual(utcTime.AddHours(1).AddDays(1), adjustedTimeDays);

        // Since we adjust and keep the time of day, the time should be the same
        var adjustedLocalTimeDays = TimeZoneInfo.ConvertTimeFromUtc(adjustedTimeDays, timeZone);
        var adjustedLocalTimeOfDay = adjustedLocalTimeDays.TimeOfDay;
        Assert.AreEqual(localTimeOfDay, adjustedLocalTimeOfDay);

        var origTimeDays = timeZone.DSTAwareAddDays(adjustedTimeDays, -1);
        Assert.AreEqual(utcTime, origTimeDays);

        // Since we adjust and keep the time of day, the time should be the same, even when going back
        adjustedLocalTimeDays = TimeZoneInfo.ConvertTimeFromUtc(adjustedTimeDays, timeZone);
        adjustedLocalTimeOfDay = adjustedLocalTimeDays.TimeOfDay;
        Assert.AreEqual(localTimeOfDay, adjustedLocalTimeOfDay);
    }
}
