"""Library for constructing an Mbed TLS test case.
"""

# Copyright The Mbed TLS Contributors
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
#

import binascii
import os
import sys
from typing import Iterable, List, Optional

from . import typing_util

def hex_string(data: bytes) -> str:
    return '"' + binascii.hexlify(data).decode('ascii') + '"'


class MissingDescription(Exception):
    pass

class MissingFunction(Exception):
    pass

class TestCase:
    """An Mbed TLS test case."""

    def __init__(self, description: Optional[str] = None):
        self.comments = [] #type: List[str]
        self.description = description #type: Optional[str]
        self.dependencies = [] #type: List[str]
        self.function = None #type: Optional[str]
        self.arguments = [] #type: List[str]
        self.skip_reason = ''

    def add_comment(self, *lines: str) -> None:
        self.comments += lines

    def set_description(self, description: str) -> None:
        self.description = description

    def set_dependencies(self, dependencies: List[str]) -> None:
        self.dependencies = dependencies

    def set_function(self, function: str) -> None:
        self.function = function

    def set_arguments(self, arguments: List[str]) -> None:
        self.arguments = arguments

    def skip_because(self, reason: str) -> None:
        """Skip this test case.

        It will be included in the output, but commented out.

        This is intended for test cases that are obtained from a
        systematic enumeration, but that have dependencies that cannot
        be fulfilled. Since we don't want to have test cases that are
        never executed, we arrange not to have actual test cases. But
        we do include comments to make it easier to understand the output
        of test case generation.

        reason must be a non-empty string explaining to humans why this
        test case is skipped.
        """
        self.skip_reason = reason

    def check_completeness(self) -> None:
        if self.description is None:
            raise MissingDescription
        if self.function is None:
            raise MissingFunction

    def write(self, out: typing_util.Writable) -> None:
        """Write the .data file paragraph for this test case.

        The output starts and ends with a single newline character. If the
        surrounding code writes lines (consisting of non-newline characters
        and a final newline), you will end up with a blank line before, but
        not after the test case.
        """
        self.check_completeness()
        assert self.description is not None # guide mypy
        assert self.function is not None # guide mypy
        out.write('\n')
        for line in self.comments:
            out.write('# ' + line + '\n')
        prefix = ''
        if self.skip_reason:
            prefix = '## '
            out.write('## # skipped because: ' + self.skip_reason + '\n')
        out.write(prefix + self.description + '\n')
        if self.dependencies:
            out.write(prefix + 'depends_on:' +
                      ':'.join(self.dependencies) + '\n')
        out.write(prefix + self.function + ':' +
                  ':'.join(self.arguments) + '\n')

def write_data_file(filename: str,
                    test_cases: Iterable[TestCase],
                    caller: Optional[str] = None) -> None:
    """Write the test cases to the specified file.

    If the file already exists, it is overwritten.
    """
    if caller is None:
        caller = os.path.basename(sys.argv[0])
    tempfile = filename + '.new'
    with open(tempfile, 'w') as out:
        out.write('# Automatically generated by {}. Do not edit!\n'
                  .format(caller))
        for tc in test_cases:
            tc.write(out)
        out.write('\n# End of automatically generated file.\n')
    os.replace(tempfile, filename)
