# 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 filecmp
import os
import shutil
from collections import defaultdict
from pathlib import Path

from mozperftest.layers import Layer
from mozperftest.utils import NoPerfMetricsError, temp_dir


def copy_tree_update(src_path, dst_path):
    for src_file in src_path.rglob("*"):
        dst_file = dst_path / src_file.relative_to(src_path)
        if src_file.is_dir():
            if not dst_file.exists():
                dst_file.mkdir(parents=True)
        elif not dst_file.exists() or not filecmp.cmp(
            src_file, dst_file, shallow=False
        ):
            dst_file.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src_file, dst_file)


class XPCShellTestError(Exception):
    pass


class XPCShellData:
    def open_data(self, data):
        return {
            "name": "xpcshell",
            "subtest": data["name"],
            "data": [
                {"file": "xpcshell", "value": value, "xaxis": xaxis}
                for xaxis, value in enumerate(data["values"])
            ],
        }

    def transform(self, data):
        return data

    merge = transform


class XPCShell(Layer):
    """Runs an xpcshell test."""

    name = "xpcshell"
    activated = True

    arguments = {
        "cycles": {"type": int, "default": 13, "help": "Number of full cycles"},
        "binary": {
            "type": str,
            "default": None,
            "help": (
                "xpcshell binary path. If not provided, "
                "looks for it in the source tree."
            ),
        },
        "mozinfo": {
            "type": str,
            "default": None,
            "help": (
                "mozinfo binary path. If not provided, looks for it in the obj tree."
            ),
        },
        "xre-path": {"type": str, "default": None, "help": "XRE path."},
        "nodejs": {"type": str, "default": None, "help": "nodejs binary path."},
    }

    def __init__(self, env, mach_cmd):
        super().__init__(env, mach_cmd)
        self.topsrcdir = mach_cmd.topsrcdir
        self._mach_context = mach_cmd._mach_context
        self.python_path = mach_cmd.virtualenv_manager.python_path
        self.topobjdir = mach_cmd.topobjdir
        self.distdir = mach_cmd.distdir
        self.bindir = mach_cmd.bindir
        self.statedir = mach_cmd.statedir
        self.metrics = []
        self.topsrcdir = mach_cmd.topsrcdir

    def setup(self):
        pass

    def run(self, metadata):
        test = Path(metadata.script["filename"])

        # let's grab the manifest
        manifest = Path(test.parent, "xpcshell.ini")
        if not manifest.exists():
            manifest = Path(test.parent, "xpcshell.toml")
            if not manifest.exists():
                raise FileNotFoundError(str(manifest))

        nodejs = self.get_arg("nodejs")
        if nodejs is not None:
            os.environ["MOZ_NODE_PATH"] = nodejs

        import runxpcshelltests

        xpcshell = runxpcshelltests.XPCShellTests(log=self)
        kwargs = {}
        kwargs["testPaths"] = test.name
        # Enable verbose mode to capture structured log output (required for perfMetrics)
        kwargs["verbose"] = True
        binary = self.get_arg("xpcshell_binary")
        if binary is None:
            binary = self.mach_cmd.get_binary_path("xpcshell")
        kwargs["xpcshell"] = binary
        binary = Path(binary)
        mozinfo = self.get_arg("mozinfo")
        if mozinfo is None:
            mozinfo = binary.parent / ".." / "mozinfo.json"
            if not mozinfo.exists():
                mozinfo = Path(self.topobjdir, "mozinfo.json")
        else:
            mozinfo = Path(mozinfo)

        kwargs["mozInfo"] = str(mozinfo)
        kwargs["symbolsPath"] = str(Path(self.distdir, "crashreporter-symbols"))
        kwargs["logfiles"] = True
        kwargs["profileName"] = "firefox"
        plugins = binary.parent / "plugins"
        if not plugins.exists():
            plugins = Path(self.distdir, "plugins")
        kwargs["pluginsPath"] = str(plugins)

        modules = Path(self.topobjdir, "_tests", "modules")
        if not modules.exists():
            modules = binary.parent / "modules"

        kwargs["testingModulesDir"] = str(modules)
        kwargs["utility_path"] = self.bindir
        kwargs["manifest"] = str(manifest)
        kwargs["totalChunks"] = 1
        xre_path = self.get_arg("xre-path")
        if xre_path is not None:
            self.info(f"Copying {xre_path} elements to {binary.parent}")
            copy_tree_update(Path(xre_path), binary.parent)

        http3server = binary.parent / "http3server"
        if http3server.exists():
            kwargs["http3server"] = str(http3server)

        cycles = self.get_arg("cycles", 1)
        self.info(f"Running {cycles} cycles")

        for cycle in range(cycles):
            self.info(f"Cycle {cycle + 1}")
            with temp_dir() as tmp:
                kwargs["tempDir"] = tmp
                if not xpcshell.runTests(kwargs):
                    raise XPCShellTestError()

        self.info("tests done.")

        results = defaultdict(list)
        for m in self.metrics:
            for key, val in m.items():
                results[key].append(val)

        if len(results.items()) == 0:
            raise NoPerfMetricsError("xpcshell")

        metadata.add_result({
            "name": test.name,
            "framework": {"name": "mozperftest"},
            "transformer": "mozperftest.test.xpcshell:XPCShellData",
            "results": [
                {"values": measures, "name": subtest}
                for subtest, measures in results.items()
            ],
        })

        return metadata

    def log_raw(self, data, **kw):
        if data["action"] != "log":
            return
        if data["message"].strip('"') != "perfMetrics":
            self.info(data["message"])
            return
        self.metrics.append(data["extra"])

    def process_output(self, procid, line, command, **kwargs):
        self.info(line)

    def dummy(self, *args, **kw):
        pass

    test_end = suite_start = suite_end = test_start = dummy
