# Copyright (c) 2018, Arm Limited and affiliates.
# SPDX-License-Identifier: Apache-2.0
#
# 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.

"""
This module contains classes to represent Test Specification interface that
defines the data to be generated by/from a build system to give enough information
to Greentea.
"""

import json


class TestBinary:
    """
    Class representing a Test Binary.
    """

    KW_BIN_TYPE = "binary_type"
    KW_BIN_PATH = "path"

    BIN_TYPE_BOOTABLE = "bootable"
    BIN_TYPE_DEFAULT = BIN_TYPE_BOOTABLE
    SUPPORTED_BIN_TYPES = [BIN_TYPE_BOOTABLE]

    def __init__(self, path, binary_type):
        """
        ctor.

        :param path:
        :param binary_type:
        :return:
        """
        assert binary_type in TestBinary.SUPPORTED_BIN_TYPES, (
            "Binary type %s not supported. Supported types [%s]"
            % (binary_type, ", ".join(TestBinary.SUPPORTED_BIN_TYPES))
        )
        self.__path = path
        self.__flash_method = binary_type

    def get_path(self):
        """
        Gives binary path.
        :return:
        """
        return self.__path


class Test:
    """
    class representing a Test artifact that may contain more than one test binaries.
    """

    KW_TEST_NAME = "name"
    KW_TEST_BINS = "binaries"

    def __init__(self, name, default_flash_method=None):
        """
        ctor.

        :param name:
        :param default_flash_method:
        :return:
        """
        self.__name = name
        self.__default_flash_method = default_flash_method
        self.__binaries_by_flash_method = {}

    def get_name(self):
        """
        Gives test name.

        :return:
        """
        return self.__name

    def get_binary(self, binary_type=TestBinary.BIN_TYPE_DEFAULT):
        """
        Gives a test binary of specific flash type.

        :param binary_type:
        :return:
        """
        return self.__binaries_by_flash_method.get(binary_type, None)

    def parse(self, test_json):
        """
        Parse json contents into object.

        :param test_json:
        :return:
        """
        assert Test.KW_TEST_BINS in test_json, "Test spec should contain key `binaries`"
        for binary in test_json[Test.KW_TEST_BINS]:
            mandatory_keys = [TestBinary.KW_BIN_PATH]
            assert set(mandatory_keys).issubset(
                set(binary.keys())
            ), "Binary spec should contain key [%s]" % ",".join(mandatory_keys)
            fm = binary.get(TestBinary.KW_BIN_TYPE, self.__default_flash_method)
            assert fm is not None, "Binary type not specified in build and binary spec."
            tb = TestBinary(binary[TestBinary.KW_BIN_PATH], fm)
            self.__binaries_by_flash_method[fm] = tb

    def add_binary(self, path, binary_type):
        """
        Add binary to the test.

        :param path:
        :param binary_type:
        :return:
        """
        self.__binaries_by_flash_method[binary_type] = TestBinary(path, binary_type)


class TestBuild:
    """
    class for Test build.
    """

    KW_TEST_BUILD_NAME = "name"
    KW_PLATFORM = "platform"
    KW_TOOLCHAIN = "toolchain"
    KW_BAUD_RATE = "baud_rate"
    KW_BUILD_BASE_PATH = "base_path"
    KW_TESTS = "tests"
    KW_BIN_TYPE = "binary_type"

    def __init__(
        self, name, platform, toolchain, baud_rate, base_path, default_flash_method=None
    ):
        """
        ctor.

        :param name:
        :param platform:
        :param toolchain:
        :param baud_rate:
        :param base_path:
        :param default_flash_method:
        :return:
        """
        self.__name = name
        self.__platform = platform
        self.__toolchain = toolchain
        self.__baud_rate = baud_rate
        self.__base_path = base_path
        self.__default_flash_method = default_flash_method
        self.__tests = {}

    def get_name(self):
        """
        Gives build name.

        :return:
        """
        return self.__name

    def get_platform(self):
        """
        Gives mbed classic platform name.

        :return:
        """
        return self.__platform

    def get_toolchain(self):
        """
        Gives toolchain

        :return:
        """
        return self.__toolchain

    def get_baudrate(self):
        """
        Gives baud rate.

        :return:
        """
        return self.__baud_rate

    def get_path(self):
        """
        Gives path.

        :return:
        """
        return self.__base_path

    def get_tests(self):
        """
        Gives tests dict keyed by test name.

        :return:
        """
        return self.__tests

    def parse(self, build_spec):
        """
        Parse Test build json.

        :param build_spec:
        :return:
        """
        assert TestBuild.KW_TESTS in build_spec, (
            "Build spec should contain key '%s'" % TestBuild.KW_TESTS
        )
        for name, test_json in build_spec[TestBuild.KW_TESTS].items():
            test = Test(name, default_flash_method=self.__default_flash_method)
            test.parse(test_json)
            self.__tests[name] = test

    def add_test(self, name, test):
        """
        Add test.

        :param name:
        :param test:
        :return:
        """
        self.__tests[name] = test


class TestSpec:
    """
    Test specification. Contains Builds.
    """

    KW_BUILDS = "builds"
    test_spec_filename = "runtime_load"

    def __init__(self, test_spec_filename=None):
        """
        ctor.

        :return:
        """
        self.__target_test_spec = {}
        if test_spec_filename:
            self.test_spec_filename = test_spec_filename
            self.load(self.test_spec_filename)

    def load(self, test_spec_filename):
        """
        Load test spec directly from file

        :param test_spec_filename: Name of JSON file with TestSpec to load
        :return: Treu if load was successful
        """
        try:
            with open(test_spec_filename, "r") as f:
                self.parse(json.load(f))
        except Exception as e:
            print("TestSpec::load('%s')" % test_spec_filename, str(e))
            return False

        self.test_spec_filename = test_spec_filename
        return True

    def parse(self, spec):
        """
        Parse test spec json.

        :param spec:
        :return:
        """
        assert TestSpec.KW_BUILDS, (
            "Test spec should contain key '%s'" % TestSpec.KW_BUILDS
        )
        for build_name, build in spec[TestSpec.KW_BUILDS].items():
            mandatory_keys = [
                TestBuild.KW_PLATFORM,
                TestBuild.KW_TOOLCHAIN,
                TestBuild.KW_BAUD_RATE,
                TestBuild.KW_BUILD_BASE_PATH,
            ]
            assert set(mandatory_keys).issubset(set(build.keys())), (
                "Build spec should contain keys [%s]. It has [%s]"
                % (",".join(mandatory_keys), ",".join(build.keys()))
            )
            platform = build[TestBuild.KW_PLATFORM]
            toolchain = build[TestBuild.KW_TOOLCHAIN]

            # If there is no 'name' property in build, we will use build key
            # as build name
            name = build.get(TestBuild.KW_TEST_BUILD_NAME, build_name)

            tb = TestBuild(
                name,
                platform,
                toolchain,
                build[TestBuild.KW_BAUD_RATE],
                build[TestBuild.KW_BUILD_BASE_PATH],
                build.get(TestBuild.KW_BIN_TYPE, None),
            )
            tb.parse(build)
            self.__target_test_spec[name] = tb

    def get_test_builds(self, filter_by_names=None):
        """
        Gives test builds.
        :param filter_by_names: List of names of builds you want to filter
        in your result
        :return:
        """
        result = []
        if filter_by_names:
            assert type(filter_by_names) is list
            for tb in self.__target_test_spec.values():
                if tb.get_name() in filter_by_names:
                    result.append(tb)
        else:
            # When filtering by name is not defined we will return all builds objects
            result = list(self.__target_test_spec.values())
        return result

    def get_test_build(self, build_name):
        """
        Gives test build with given name.

        :param build_name:
        :return:
        """
        return self.__target_test_spec.get(build_name, None)

    def add_test_builds(self, name, test_build):
        """
        Add test build.

        :param name:
        :param test_build:
        :return:
        """
        self.__target_test_spec[name] = test_build
