# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Proof summary."""

import voluptuous
import voluptuous.humanize

from cbmc_viewer import templates
from cbmc_viewer import util

################################################################
# Data passed to jinja to generate proof summary from summary.jinja.html

VALID_SUMMARY_DATA = voluptuous.Schema({
    # coverage section of the summary
    'coverage': {
        'overall': voluptuous.Any(
            {
                'percentage': float, # percentage of lines hit
                'hit': int, # lines hit
                'total': int # lines total
            },
            {}
        ),
        'function': [{
            'percentage': float, # percentage of lines hit
            'hit': int, # lines hit
            'total': int, # lines total
            'file_name': str,
            'func_name': str,
            'line_num': int,

        }]
    },
    # warning section of the summary
    'warnings': {
        'expected_missing_function': [str], # function names
        'unexpected_missing_function': [str], # function names
        'other': [str] # warning messages
    },
    # failure sections of the summary
    'failures': {
        'property': [{ # assertion failures
            'prop_name': str,
            'prop_desc': str,
            'file_name': str,
            'func_name': str,
            'line_num': int,
            'func_line': int
        }],
        'loop': [{ # loop unwinding assertion failures
            'loop_name': str,
            'file_name': str,
            'func_name': str,
            'line_num': int
        }],
        'other': [str] # unrecognized failure names
    }
}, required=True)

VALID_SUMMARY = voluptuous.Schema({
    'summary': VALID_SUMMARY_DATA,
    'outdir': str # default output directory for dump()
}, required=True)

################################################################

class Summary:
    """Proof summary.

    The summary merges results produced by cbmc into a single report.
    """

    def __init__(self, coverage, symbols, results, properties, loops,
                 config, outdir='.'):
        # pylint: disable=too-many-arguments

        self.summary = {
            'coverage': {
                'overall': overall_coverage(coverage),
                'function': function_coverage(coverage, symbols)
            },
            'warnings': {
                'expected_missing_function':
                    expected_missing_functions(results, config),
                'unexpected_missing_function':
                    unexpected_missing_functions(results, config),
                'other': other_warnings(results)
            },
            'failures': {
                'property': property_failures(results, properties, symbols),
                'loop': loop_failures(results, loops),
                'other': other_failures(results, properties, loops)
            }
        }
        self.outdir = outdir
        self.validate()

    def __str__(self):
        """Render the proof summary as html."""

        return templates.render_summary(self.summary)

    def validate(self):
        """Validate members of a summary object."""

        return voluptuous.humanize.validate_with_humanized_errors(
            self.__dict__, VALID_SUMMARY
        )

    def dump(self, filename=None, outdir=None):
        """Write the proof summary to a file rendered as html."""

        util.dump(self, filename or "index.html", outdir or self.outdir)

################################################################
# Coverage data

def overall_coverage(coverage):
    """Overall proof coverage."""

    cov = coverage.overall_coverage
    if not cov:
        return {}

    return {
        'percentage': cov['percentage'],
        'hit': cov['hit'],
        'total': cov['total']
    }

def function_coverage(coverage, symbols):
    """Function proof coverage."""

    return [
        {
            'percentage': func_cov['percentage'],
            'hit': func_cov['hit'],
            'total': func_cov['total'],
            'file_name': file_name,
            'func_name': func_name,
            'line_num': symbols.lookup(func_name).get("line") if symbols.lookup(func_name) else 0
        }
        for file_name, file_data in coverage.function_coverage.items()
        for func_name, func_cov in file_data.items()
    ]

################################################################
# Warning data

# TODO: This classification of warnings should be done by make-results

def warnings(results):
    """Proof warnings."""

    prefixes = ["**** WARNING:", "warning:"]

    def strip_prefixes(string, prefixes):
        """Strip warning prefixes from a warning string."""

        for prefix in prefixes:
            if string.startswith(prefix):
                return string[len(prefix):].strip()
        return string

    return [strip_prefixes(warning, prefixes) for warning in results.warning]

def missing_functions(messages):
    """Names of missing functions."""

    prefix = "no body for function"
    length = len(prefix)
    return [warning[length:].strip() for warning in messages
            if warning.startswith(prefix)]

def expected_missing_functions(results, config):
    """Names of missing functions expected to be missing."""

    return [
        function for function in missing_functions(warnings(results))
        if function in config.expected_missing_functions()
    ]

def unexpected_missing_functions(results, config):
    """Names of missing functions not expected to be missing."""

    return [
        function for function in missing_functions(warnings(results))
        if function not in config.expected_missing_functions()
    ]

def other_warnings(results):
    """Warnings unrelated to missing functions."""

    prefix = "no body for function"
    return [warning.strip() for warning in warnings(results)
            if not warning.startswith(prefix)]

################################################################
# Failure data

def property_definition(prop_name, properties, symbols):
    """Details for a property failure."""

    prop_def = properties.lookup(prop_name)
    if prop_def is None:
        return None

    srcloc = prop_def['location']
    return {
        'prop_name': prop_name,
        'prop_desc': prop_def['description'],
        'file_name': srcloc['file'],
        'func_name': srcloc['function'] or '',
        'line_num': srcloc['line'],
        'func_line':
            # Symbol table won't contain functions modeled by CBMC (eg, strcmp)
            # Return a line number 0 for such functions
            (symbols.lookup(srcloc['function']) or {'line': 0})['line']
    }

def loop_definition(prop_name, loops):
    """Details for a loop unwinding assertion failure."""

    srcloc = loops.lookup_assertion(prop_name)
    if srcloc is None:
        return None

    return {
        'loop_name': prop_name,
        'file_name': srcloc['file'],
        'func_name': srcloc['function'],
        'line_num': srcloc['line']
    }

def other_definition(prop_name, properties, loops):
    """Name of an unrecognized failure."""

    if properties.lookup(prop_name) or loops.lookup_assertion(prop_name):
        return None
    return prop_name

def filter_none(items):
    """Remove instances of None from a list of items."""

    return [item for item in items if item is not None]

def property_failures(results, properties, symbols):
    """Details for property failures."""

    failures = results.results[False]
    return filter_none(
        [property_definition(failure, properties, symbols)
         for failure in failures]
    )

def loop_failures(results, loops):
    """Details for loop unwinding assertion failures."""

    failures = results.results[False]
    return filter_none(
        [loop_definition(failure, loops) for failure in failures]
    )

def other_failures(results, properties, loops):
    """Names of unrecognized failures."""

    failures = results.results[False]
    return filter_none(
        [other_definition(failure, properties, loops) for failure in failures]
    )

################################################################
