# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, # You can obtain one at http://mozilla.org/MPL/2.0/.

import functools
import itertools
import json
import os
import tempfile
from os import path

_EMPTY_REPORT = {
    "tests": 0,
    "failures": 0,
    "disabled": 0,
    "errors": 0,
    "testsuites": {},
}


def merge_gtest_reports(test_reports):
    """
    Logically merges json test reports matching [this
    schema](https://google.github.io/googletest/advanced.html#generating-a-json-report).

    It is assumed that each test will appear in at most one report (rather than
    trying to search and merge each test).

    Arguments:
    * test_reports - an iterator of python-native data (likely loaded from GTest JSON files).
    """
    INTEGER_FIELDS = ["tests", "failures", "disabled", "errors"]
    TESTSUITE_INTEGER_FIELDS = ["tests", "failures", "disabled"]

    def merge_testsuite(target, suite):
        for field in TESTSUITE_INTEGER_FIELDS:
            if field in suite:
                target[field] += suite[field]
        # We assume that each test will appear in at most one report,
        # so just extend the list of tests.
        target["testsuite"].extend(suite["testsuite"])

    def merge_one(current, report):
        for field in INTEGER_FIELDS:
            if field in report:
                current[field] += report[field]
        for suite in report["testsuites"]:
            name = suite["name"]
            if name in current["testsuites"]:
                merge_testsuite(current["testsuites"][name], suite)
            else:
                current["testsuites"][name] = suite
                for field in TESTSUITE_INTEGER_FIELDS:
                    current["testsuites"][name].setdefault(field, 0)
        return current

    merged = functools.reduce(merge_one, test_reports, _EMPTY_REPORT)
    # We had testsuites as a dict for fast lookup when merging, change
    # it back to a list to match the schema.
    merged["testsuites"] = list(merged["testsuites"].values())

    return merged


class AggregatedGTestReport(dict):
    """
    An aggregated gtest report (stored as a `dict`)

    This should be used as a context manager to manage the lifetime of
    temporary storage for reports. If no exception occurs, when the context
    exits the reports will be merged into this dictionary. Thus, the context
    must not be exited before the outputs are written (e.g., by gtest processes
    completing).

    When merging results, it is assumed that each test will appear in at most
    one report (rather than trying to search and merge each test).
    """

    __slots__ = ["result_dir"]

    def __init__(self):
        self.result_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
        super().__init__()
        self.reset()

    def __enter__(self):
        self.result_dir.__enter__()
        return self

    def __exit__(self, *exc_info):
        # Only collect reports if no exception occurred
        if exc_info[0] is None:
            d = self.result_dir.name
            result_files = filter(
                lambda f: path.isfile(f), map(lambda f: path.join(d, f), os.listdir(d))
            )

            def json_from_file(file):
                with open(file) as f:
                    return json.load(f)

            self.update(
                merge_gtest_reports(
                    itertools.chain([self], map(json_from_file, result_files))
                )
            )
        self.result_dir.__exit__(*exc_info)

    def reset(self):
        """Clear all results."""
        self.clear()
        self.update(
            {"tests": 0, "failures": 0, "disabled": 0, "errors": 0, "testsuites": []}
        )

    def gtest_output(self, job_id):
        """
        Create a gtest output string with the given job id (to differentiate
        outputs).
        """
        # Replace `/` with `_` in job_id to prevent nested directories (job_id
        # may be a suite name, which may have slashes for parameterized test
        # suites).
        return f"json:{self.result_dir.name}/{job_id.replace('/', '_')}.json"

    def set_output_in_env(self, env, job_id):
        """
        Sets an environment variable mapping appropriate with the output for
        the given job id.

        Returns the env.
        """
        env["GTEST_OUTPUT"] = self.gtest_output(job_id)
        return env
