import argparse
import bs4
import copy
import datetime
import json
import logging
import os
import re


log = logging.getLogger(__name__)


def parse_arguments():
    command_parser = argparse.ArgumentParser()

    command_parser.add_argument(
        "--out",
        help="name of the output html report",
        action="store",
        dest="out",
        default="merged.html",
        type=str,
    )

    command_parser.add_argument(
        "--verbose",
        "-v",
        help="level of logging verbosity",
        default=3,
        action="count",
    )

    command_parser.add_argument(
        "html_files",
        help="html files generated by pytest-html",
        action="store",
        nargs="*",
        type=str,
    )

    # parse options
    options = command_parser.parse_args()

    return options


class PytestHTMLReportMerger:
    def __init__(self):
        self.base = None

    def _format_time(self, td):
        """convert timedelta to HH:MM:SS

        based on https://stackoverflow.com/a/28503916
        """

        minutes, seconds = divmod(td.total_seconds(), 60)
        hours, minutes = divmod(minutes, 60)
        return "{:02d}:{:02d}:{:02d}".format(int(hours), int(minutes), int(seconds))

    def _parse_summary(self, report):
        # use a regular expression to find a string like:
        # 402 tests took 09:40:47.
        # this string shows up if test cases were run
        element = report.select(".run-count")[0]
        pattern = r"(\d+) tests{0,1} took (\d{2}):(\d{2}):(\d{2})"
        matches = re.search(pattern, element.string)
        if matches is not None:
            total_tests = int(matches.groups()[0])
            (t_hour, t_minute, t_second) = matches.groups()[1:4]
            total_time_delta = datetime.timedelta(
                hours=int(t_hour), minutes=int(t_minute), seconds=int(t_second)
            )
        else:
            # use a regular expression to look for a string like:
            # 0 test took 0 ms.
            # this string shows up if there were no tests run.
            # i think the units will always be ms
            pattern = r"(\d+) tests{0,1} took (\d+)"
            matches = re.search(pattern, element.string)
            if matches is not None:
                total_tests = int(matches.groups()[0])
                t_ms = int(matches.groups()[1])
                total_time_delta = datetime.timedelta(seconds=t_ms * 1000)
            else:
                # TODO:
                # there is a bigger problem with our regular expressions we have
                # to investigate. for now, we don't fail because it is more
                # important that we get the merged results than get the number of
                # tests correct.
                total_tests = 0
                total_time_delta = datetime.timedelta(seconds=0)

        return (total_tests, total_time_delta)

    def process_report(self, report_path):
        # open the first html file
        html_doc = ""
        with open(report_path, "r") as f:
            html_doc = f.read()
        soup = bs4.BeautifulSoup(html_doc, features="html.parser")

        report_name = os.path.basename(report_path)

        # update the base report
        if self.base is None:
            # this is the first report
            self.base = copy.copy(soup)

            # load json data from the base report
            base_data_container = self.base.select("#data-container")[0]
            base_jsonblob = base_data_container.get("data-jsonblob")
            base_data = json.loads(base_jsonblob)

            # update the keys to include the report name
            # this will make sure the keys are unique as we add more reports.
            d = {
                f"{key} - {report_name}": value
                for key, value in base_data["tests"].items()
            }
            base_data["tests"] = d

            # write the json data back to the html element's attribute
            base_data_container["data-jsonblob"] = json.dumps(base_data)

            return

        # parse the summary

        # it would be nice if we could pull the Hours:Minutes:Seconds out of
        # the summary string, but pytest-html has a bug in _format_duration()
        # that allows the seconds to be 60, which datetime.datetime.strptime()
        # cannot parse. when that gets fixed, we can use lines like these
        # matches = re.search(r"(\d+) tests took (\d{2}):(\d{2}):(\d{2})", base_element.string)
        # base_total_time_str = matches.groups()[1]
        # t = datetime.datetime.strptime(base_total_time_str,"%H:%M:%S")

        # parse the number of tests and timings from the base report
        (base_total_tests, base_total_time_delta) = self._parse_summary(self.base)

        # parse the number of tests and timings from the provided report
        (soup_total_tests, soup_total_time_delta) = self._parse_summary(soup)

        # sum up the test count and time deltas
        total_tests = base_total_tests + soup_total_tests
        total_time_delta = base_total_time_delta + soup_total_time_delta
        total_time_str = self._format_time(total_time_delta)

        # save the updated total tests and total time.
        base_element = self.base.select(".run-count")[0]
        base_element.string = f"{total_tests} tests took {total_time_str}."

        # parse the filter counts

        for key in [
            "passed",
            "skipped",
            "failed",
            "error",
            "xfailed",
            "xpassed",
            "rerun",
        ]:
            # find the base's value for the key
            base_elements = self.base.select(f".filters .{key}")
            matches = re.search(r"(\d+)", base_elements[0].string)
            base_value = int(matches.groups()[0])

            # find the soup's value for the key
            soup_elements = soup.select(f".filters .{key}")
            matches = re.search(r"(\d+)", soup_elements[0].string)
            soup_value = int(matches.groups()[0])

            # save the updated count to the base
            base_elements[0].string = re.sub(
                r"\d+", str(base_value + soup_value), base_elements[0].string
            )

            # remove the base's disabled filter if the soup value was not zero
            if base_value == 0 and soup_value > 0:
                ele = self.base.select(f"[data-test-result='{key}']")[0]
                del ele["disabled"]

        # update the base report's results table

        # load json data from the base report
        base_data_container = self.base.select("#data-container")[0]
        base_jsonblob = base_data_container.get("data-jsonblob")
        base_data = json.loads(base_jsonblob)

        # load json data from the provided report
        soup_data_container = soup.select("#data-container")[0]
        soup_jsonblob = soup_data_container.get("data-jsonblob")
        soup_data = json.loads(soup_jsonblob)

        # update the keys to include the report name
        # this will make sure the keys are unique as we add more reports.
        d = {
            f"{key} - {report_name}": value for key, value in soup_data["tests"].items()
        }
        soup_data["tests"] = d

        # copy the tests from the provided report to the base report
        base_data["tests"] = base_data["tests"] | soup_data["tests"]

        # write the tests json data back to the html element's attribute
        base_data_container["data-jsonblob"] = json.dumps(base_data)

    def write_report(self, report_path):
        report_name = os.path.basename(report_path)

        # reset the title in the <head><title> element
        ele = self.base.select("#head-title")[0]
        ele.string = report_name

        # reset the title in the <body><h1> element
        ele = self.base.select("#title")[0]
        ele.string = report_name

        # load json data from the base report
        base_data_container = self.base.select("#data-container")[0]
        base_jsonblob = base_data_container.get("data-jsonblob")
        base_data = json.loads(base_jsonblob)

        # reset the title in the footer's data-jsonblob
        base_data["title"] = report_name

        # write the json data back to the html element's attribute
        base_data_container["data-jsonblob"] = json.dumps(base_data)

        # write to file
        with open(report_path, "w", encoding="utf-8") as f:
            f.write(str(self.base.prettify(formatter="html5")))


def main(arguments):
    # create a report merger object
    report_merger = PytestHTMLReportMerger()

    # process each of the input files
    for infile in arguments.html_files:
        report_merger.process_report(infile)

    # write the merged report to disk
    report_merger.write_report(arguments.out)


def cli():
    arguments = parse_arguments()

    logging.basicConfig(level=int((6 - arguments.verbose) * 10))

    log.debug(f"opts = {arguments}")

    main(arguments)

    log.debug("exiting")
