#
#    Copyright (c) 2023 Project CHIP Authors
#    All rights reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#

import ipaddress
from datetime import timedelta

import chip.clusters as Clusters
from chip.clusters.Types import NullValue
from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main, utc_time_in_matter_epoch
from mobly import asserts


class TC_TIMESYNC_2_1(MatterBaseTest):
    async def read_ts_attribute_expect_success(self, endpoint, attribute):
        cluster = Clusters.Objects.TimeSynchronization
        return await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=attribute)

    @async_test_body
    async def test_TC_TIMESYNC_2_1(self):

        endpoint = self.user_params.get("endpoint", 0)

        self.print_step(1, "Commissioning, already done")
        attributes = Clusters.TimeSynchronization.Attributes

        self.print_step(2, "Read Granularity attribute")
        if self.check_pics("TIMESYNC.S.A0001"):
            granularity_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.Granularity)
            asserts.assert_less(granularity_dut, Clusters.TimeSynchronization.Enums.GranularityEnum.kUnknownEnumValue,
                                "Granularity is not in valid range")
        else:
            asserts.assert_true(False, "Granularity is a mandatory attribute and must be present in the PICS file")

        self.print_step(3, "Read TimeSource")
        if self.check_pics("TIMESYNC.S.A0002"):
            time_source = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.TimeSource)
            asserts.assert_less(time_source, Clusters.TimeSynchronization.Enums.TimeSourceEnum.kUnknownEnumValue,
                                "TimeSource is not in valid range")

        self.print_step(4, "Read TrustedTimeSource")
        if self.check_pics("TIMESYNC.S.A0003"):
            trusted_time_source = await self.read_ts_attribute_expect_success(endpoint=endpoint,
                                                                              attribute=attributes.TrustedTimeSource)
            if trusted_time_source is not NullValue:
                asserts.assert_less_equal(trusted_time_source.fabricIndex, 0xFE,
                                          "FabricIndex for the TrustedTimeSource is out of range")
                asserts.assert_greater_equal(trusted_time_source.fabricIndex, 1,
                                             "FabricIndex for the TrustedTimeSource is out of range")
        elif self.check_pics("TIMESYNC.S.F03"):
            asserts.assert_true(False, "TrustedTimeSource is mandatory if the TSC feature (TIMESYNC.S.F03) is supported")

        self.print_step(5, "Read DefaultNTP")
        if self.check_pics("TIMESYNC.S.A0004"):
            default_ntp = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.DefaultNTP)
            if default_ntp is not NullValue:
                asserts.assert_less_equal(len(default_ntp), 128, "DefaultNTP length must be less than 128")
                # Assume this is a valid web address if it has at least one . in the name
                is_web_addr = default_ntp.find('.') != -1
                try:
                    ipaddress.IPv6Address(default_ntp)
                    is_ip_addr = True
                except ipaddress.AddressValueError:
                    is_ip_addr = False
                    pass
                asserts.assert_true(is_web_addr or is_ip_addr, "Returned DefaultNTP value is not a IP address or web address")
        elif self.check_pics("TIMESYNC.S.F01"):
            asserts.assert_true(False, "DefaultNTP is mandatory if the NTPC (TIMESYNC.S.F01) feature is supported")

        self.print_step(6, "Read TimeZone")
        if self.check_pics("TIMESYNC.S.A0005"):
            tz_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.TimeZone)
            asserts.assert_greater_equal(len(tz_dut), 1, "TimeZone must have at least one entry in the list")
            asserts.assert_less_equal(len(tz_dut), 2, "TimeZone may have a maximum of two entries in the list")
            for entry in tz_dut:
                asserts.assert_greater_equal(entry.offset, -43200, "TimeZone offset is out of range")
                asserts.assert_less_equal(entry.offset, 50400, "TimeZone offset is out of range")
                if entry.name:
                    asserts.assert_less_equal(len(entry.name), 64, "TimeZone name is too long")

            asserts.assert_equal(tz_dut[0].validAt, 0, "TimeZone list first entry must have a 0 ValidAt time")
            if len(tz_dut) > 1:
                asserts.assert_not_equal(tz_dut[1].validAt, 0, "TimeZone list second entry must have a non-zero ValidAt time")
        elif self.check_pics("TIMESYNC.S.F00"):
            asserts.assert_true(False, "TimeZone is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")

        self.print_step(7, "Read DSTOffset")
        if self.check_pics("TIMESYNC.S.A0006"):
            dst_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.DSTOffset)
            last_valid_until = -1
            last_valid_starting = -1
            for dst in dst_dut:
                asserts.assert_greater(dst.validStarting, last_valid_starting,
                                       "DSTOffset list must be sorted by ValidStarting time")
                last_valid_starting = dst.validStarting
                asserts.assert_greater_equal(dst.validStarting, last_valid_until,
                                             "DSTOffset list must have every ValidStarting > ValidUntil of the previous entry")
                last_valid_until = dst.validUntil
                if dst.validUntil is NullValue or dst.validUntil is None:
                    asserts.assert_equal(dst, dst_dut[-1], "DSTOffset list must have Null ValidUntil at the end")
        elif self.check_pics("TIMESYNC.S.F00"):
            asserts.assert_true(False, "DSTOffset is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")

        self.print_step(8, "Read UTCTime")
        if self.check_pics("TIMESYNC.S.A0000"):
            utc_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.UTCTime)
            if utc_dut is NullValue:
                asserts.assert_equal(granularity_dut, Clusters.TimeSynchronization.Enums.GranularityEnum.kNoTimeGranularity)
            else:
                asserts.assert_not_equal(granularity_dut, Clusters.TimeSynchronization.Enums.GranularityEnum.kNoTimeGranularity)
                if granularity_dut is Clusters.TimeSynchronization.Enums.GranularityEnum.kMinutesGranularity:
                    toleranace = timedelta(minutes=10)
                else:
                    toleranace = timedelta(minutes=1)
                delta_us = abs(utc_dut - utc_time_in_matter_epoch())
                delta = timedelta(microseconds=delta_us)
                asserts.assert_less_equal(delta, toleranace, "UTC time is not within tolerance of TH")
        else:
            asserts.assert_true(False, "UTCTime is a mandatory attribute and must be present in the PICS file")

        self.print_step(9, "Read LocalTime")
        if self.check_pics("TIMESYNC.S.A0007"):
            utc_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.UTCTime)
            local_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.LocalTime)
            if utc_dut is NullValue:
                asserts.assert_true(local_dut is NullValue, "LocalTime must be Null if UTC time is Null")
            elif len(dst_dut) == 0:
                asserts.assert_true(local_dut is NullValue, "LocalTime must be Null if the DST table is empty")
            else:
                local_calculated = utc_dut + dst_dut[0].offset + tz_dut[0].offset
                delta_us = abs(local_dut, local_calculated)
                delta = timedelta(microseconds=delta_us)
                toleranace = timedelta(minutes=1)
                asserts.assert_less_equal(delta, toleranace, "Local time caluclation is not within tolerance of calculated value")
        elif self.check_pics("TIMESYNC.S.F00"):
            asserts.assert_true(False, "LocalTime is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")

        self.print_step(10, "Read TimeZoneDatabase")
        if self.check_pics("TIMESYNC.S.A0008"):
            tz_db_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.TimeZoneDatabase)
            asserts.assert_less(tz_db_dut, Clusters.TimeSynchronization.Enums.TimeZoneDatabaseEnum.kUnknownEnumValue,
                                "TimeZoneDatabase is not in valid range")
        elif self.check_pics("TIMESYNC.S.F00"):
            asserts.assert_true(False, "TimeZoneDatabase is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")

        self.print_step(11, "Read NTPServerAvailable")
        if self.check_pics("TIMESYNC.S.A0009"):
            # bool typechecking happens in the test read functions, so all we need to do here is do the read
            await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.NTPServerAvailable)
        elif self.check_pics("TIMESYNC.S.F02"):
            asserts.assert_true(False, "NTPServerAvailable is mandatory if the NTPS (TIMESYNC.S.F02) feature is supported")

        self.print_step(12, "Read TimeZoneListMaxSize")
        if self.check_pics("TIMESYNC.S.A000A"):
            size = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.TimeZoneListMaxSize)
            asserts.assert_greater_equal(size, 1, "TimeZoneListMaxSize must be at least 1")
            asserts.assert_less_equal(size, 2, "TimeZoneListMaxSize must be max 2")
        elif self.check_pics("TIMESYNC.S.F00"):
            asserts.assert_true(False, "TimeZoneListMaxSize is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")

        self.print_step(13, "Read DSTOffsetListMaxSize")
        if self.check_pics("TIMESYNC.S.A000B"):
            size = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.DSTOffsetListMaxSize)
            asserts.assert_greater_equal(size, 1, "DSTOffsetListMaxSize must be at least 1")
        elif self.check_pics("TIMESYNC.S.F00"):
            asserts.assert_true(False, "DSTOffsetListMaxSize is mandatory if the TZ (TIMESYNC.S.F00) feature is supported")

        self.print_step(14, "Read SupportsDNSResolve")
        if self.check_pics("TIMESYNC.S.A0004"):
            # bool typechecking happens in the test read functions, so all we need to do here is do the read
            await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.SupportsDNSResolve)
        elif self.check_pics("TIMESYNC.S.F01"):
            asserts.assert_true(False, "SupportsDNSResolve is mandatory if the NTPC (TIMESYNC.S.F01) feature is supported")


if __name__ == "__main__":
    default_matter_test_main()
