# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import collections
import json
import re

from rally.common.io import junit
from rally import consts
from rally.ui import utils as ui_utils
from rally.verification import reporter


SKIP_RE = re.compile(r"Skipped until Bug: ?(?P<bug_number>\d+) is resolved.")
LP_BUG_LINK = "https://launchpad.net/bugs/%s"
TIME_FORMAT = consts.TimeFormat.ISO8601


@reporter.configure("json")
class JSONReporter(reporter.VerificationReporter):
    """Generates verification report in JSON format.

    An example of the report (All dates, numbers, names appearing in this
    example are fictitious. Any resemblance to real things is purely
    coincidental):

      .. code-block:: json

        {"verifications": {
            "verification-uuid-1": {
                "status": "finished",
                "skipped": 1,
                "started_at": "2001-01-01T00:00:00",
                "finished_at": "2001-01-01T00:05:00",
                "tests_duration": 5,
                "run_args": {
                    "pattern": "set=smoke",
                    "xfail_list": {"some.test.TestCase.test_xfail":
                                       "Some reason why it is expected."},
                    "skip_list": {"some.test.TestCase.test_skipped":
                                      "This test was skipped intentionally"},
                },
                "success": 1,
                "expected_failures": 1,
                "tests_count": 3,
                "failures": 0,
                "unexpected_success": 0
            },
            "verification-uuid-2": {
                "status": "finished",
                "skipped": 1,
                "started_at": "2002-01-01T00:00:00",
                "finished_at": "2002-01-01T00:05:00",
                "tests_duration": 5,
                "run_args": {
                    "pattern": "set=smoke",
                    "xfail_list": {"some.test.TestCase.test_xfail":
                                       "Some reason why it is expected."},
                    "skip_list": {"some.test.TestCase.test_skipped":
                                      "This test was skipped intentionally"},
                },
                "success": 1,
                "expected_failures": 1,
                "tests_count": 3,
                "failures": 1,
                "unexpected_success": 0
            }
         },
         "tests": {
            "some.test.TestCase.test_foo[tag1,tag2]": {
                "name": "some.test.TestCase.test_foo",
                "tags": ["tag1","tag2"],
                "by_verification": {
                    "verification-uuid-1": {
                        "status": "success",
                        "duration": "1.111"
                    },
                    "verification-uuid-2": {
                        "status": "success",
                        "duration": "22.222"
                    }
                }
            },
            "some.test.TestCase.test_skipped[tag1]": {
                "name": "some.test.TestCase.test_skipped",
                "tags": ["tag1"],
                "by_verification": {
                    "verification-uuid-1": {
                        "status": "skipped",
                        "duration": "0",
                        "details": "Skipped until Bug: 666 is resolved."
                    },
                    "verification-uuid-2": {
                        "status": "skipped",
                        "duration": "0",
                        "details": "Skipped until Bug: 666 is resolved."
                    }
                }
            },
            "some.test.TestCase.test_xfail": {
                "name": "some.test.TestCase.test_xfail",
                "tags": [],
                "by_verification": {
                    "verification-uuid-1": {
                        "status": "xfail",
                        "duration": "3",
                        "details": "Some reason why it is expected.\\n\\n"
                            "Traceback (most recent call last): \\n"
                            "  File "fake.py", line 13, in <module>\\n"
                            "    yyy()\\n"
                            "  File "fake.py", line 11, in yyy\\n"
                            "    xxx()\\n"
                            "  File "fake.py", line 8, in xxx\\n"
                            "    bar()\\n"
                            "  File "fake.py", line 5, in bar\\n"
                            "    foo()\\n"
                            "  File "fake.py", line 2, in foo\\n"
                            "    raise Exception()\\n"
                            "Exception"
                    },
                    "verification-uuid-2": {
                        "status": "xfail",
                        "duration": "3",
                        "details": "Some reason why it is expected.\\n\\n"
                            "Traceback (most recent call last): \\n"
                            "  File "fake.py", line 13, in <module>\\n"
                            "    yyy()\\n"
                            "  File "fake.py", line 11, in yyy\\n"
                            "    xxx()\\n"
                            "  File "fake.py", line 8, in xxx\\n"
                            "    bar()\\n"
                            "  File "fake.py", line 5, in bar\\n"
                            "    foo()\\n"
                            "  File "fake.py", line 2, in foo\\n"
                            "    raise Exception()\\n"
                            "Exception"
                    }
                }
            },
            "some.test.TestCase.test_failed": {
                "name": "some.test.TestCase.test_failed",
                "tags": [],
                "by_verification": {
                    "verification-uuid-2": {
                        "status": "fail",
                        "duration": "4",
                        "details": "Some reason why it is expected.\\n\\n"
                            "Traceback (most recent call last): \\n"
                            "  File "fake.py", line 13, in <module>\\n"
                            "    yyy()\\n"
                            "  File "fake.py", line 11, in yyy\\n"
                            "    xxx()\\n"
                            "  File "fake.py", line 8, in xxx\\n"
                            "    bar()\\n"
                            "  File "fake.py", line 5, in bar\\n"
                            "    foo()\\n"
                            "  File "fake.py", line 2, in foo\\n"
                            "    raise Exception()\\n"
                            "Exception"
                        }
                    }
                }
            }
        }

    """

    @classmethod
    def validate(cls, output_destination):
        """Validate destination of report.

        :param output_destination: Destination of report
        """
        # nothing to check :)
        pass

    def _generate(self):
        """Prepare raw report."""

        verifications = collections.OrderedDict()
        tests = {}

        for v in self.verifications:
            verifications[v.uuid] = {
                "started_at": v.created_at.strftime(TIME_FORMAT),
                "finished_at": v.updated_at.strftime(TIME_FORMAT),
                "status": v.status,
                "run_args": v.run_args,
                "tests_count": v.tests_count,
                "tests_duration": v.tests_duration,
                "skipped": v.skipped,
                "success": v.success,
                "expected_failures": v.expected_failures,
                "unexpected_success": v.unexpected_success,
                "failures": v.failures,
            }

            for test_id, result in v.tests.items():
                if test_id not in tests:
                    # NOTE(ylobankov): It is more convenient to see test ID
                    #                  at the first place in the report.
                    tags = sorted(result.get("tags", []), reverse=True,
                                  key=lambda tag: tag.startswith("id-"))
                    tests[test_id] = {"tags": tags,
                                      "name": result["name"],
                                      "by_verification": {}}

                tests[test_id]["by_verification"][v.uuid] = {
                    "status": result["status"],
                    "duration": result["duration"]
                }

                reason = result.get("reason", "")
                if reason:
                    match = SKIP_RE.match(reason)
                    if match:
                        link = LP_BUG_LINK % match.group("bug_number")
                        reason = re.sub(match.group("bug_number"), link,
                                        reason)
                traceback = result.get("traceback", "")
                sep = "\n\n" if reason and traceback else ""
                d = (reason + sep + traceback.strip()) or None
                if d:
                    tests[test_id]["by_verification"][v.uuid]["details"] = d

        return {"verifications": verifications, "tests": tests}

    def generate(self):
        raw_report = json.dumps(self._generate(), indent=4)

        if self.output_destination:
            return {"files": {self.output_destination: raw_report},
                    "open": self.output_destination}
        else:
            return {"print": raw_report}


@reporter.configure("html")
class HTMLReporter(JSONReporter):
    """Generates verification report in HTML format."""
    INCLUDE_LIBS = False

    # "T" separator of ISO 8601 is not user-friendly enough.
    TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

    def generate(self):
        report = self._generate()
        uuids = report["verifications"].keys()
        show_comparison_note = False

        for test in report["tests"].values():
            # make as much as possible processing here to reduce processing
            # at JS side
            test["has_details"] = False
            for test_info in test["by_verification"].values():
                if "details" not in test_info:
                    test_info["details"] = None
                elif not test["has_details"]:
                    test["has_details"] = True

            durations = []
            # iter by uuids to store right order for comparison
            for uuid in uuids:
                if uuid in test["by_verification"]:
                    durations.append(test["by_verification"][uuid]["duration"])
                    if float(durations[-1]) < 0.001:
                        durations[-1] = "0"
                        # not to display such little duration in the report
                        test["by_verification"][uuid]["duration"] = ""

                    if len(durations) > 1 and not (
                            durations[0] == "0" and durations[-1] == "0"):
                        # compare result with result of the first verification
                        diff = float(durations[-1]) - float(durations[0])
                        result = "%s (" % durations[-1]
                        if diff >= 0:
                            result += "+"
                        result += "%s)" % diff
                        test["by_verification"][uuid]["duration"] = result

            if not show_comparison_note and len(durations) > 2:
                # NOTE(andreykurilin): only in case of comparison of more
                #   than 2 results of the same test we should display a note
                #   about the comparison strategy
                show_comparison_note = True

        template = ui_utils.get_template("verification/report.html")
        context = {"uuids": list(uuids),
                   "verifications": report["verifications"],
                   "tests": report["tests"],
                   "show_comparison_note": show_comparison_note}

        raw_report = template.render(data=json.dumps(context),
                                     include_libs=self.INCLUDE_LIBS)

        # in future we will support html_static and will need to save more
        # files
        if self.output_destination:
            return {"files": {self.output_destination: raw_report},
                    "open": self.output_destination}
        else:
            return {"print": raw_report}


@reporter.configure("html-static")
class HTMLStaticReporter(HTMLReporter):
    """Generates verification report in HTML format with embedded JS/CSS."""
    INCLUDE_LIBS = True


@reporter.configure("junit-xml")
class JUnitXMLReporter(reporter.VerificationReporter):
    """Generates verification report in JUnit-XML format.

    An example of the report (All dates, numbers, names appearing in this
    example are fictitious. Any resemblance to real things is purely
    coincidental):

      .. code-block:: xml

        <testsuites>
          <!--Report is generated by Rally 0.8.0 at 2002-01-01T00:00:00-->
          <testsuite id="verification-uuid-1"
                     tests="9"
                     time="1.111"
                     errors="0"
                     failures="3"
                     skipped="0"
                     timestamp="2001-01-01T00:00:00">
            <testcase classname="some.test.TestCase"
                      name="test_foo"
                      time="8"
                      timestamp="2001-01-01T00:01:00" />
            <testcase classname="some.test.TestCase"
                      name="test_skipped"
                      time="0"
                      timestamp="2001-01-01T00:02:00">
              <skipped>Skipped until Bug: 666 is resolved.</skipped>
            </testcase>
            <testcase classname="some.test.TestCase"
                      name="test_xfail"
                      time="3"
                      timestamp="2001-01-01T00:03:00">
              <!--It is an expected failure due to: something-->
              <!--Traceback:
        HEEELP-->
            </testcase>
            <testcase classname="some.test.TestCase"
                      name="test_uxsuccess"
                      time="3"
                      timestamp="2001-01-01T00:04:00">
              <failure>
                  It is an unexpected success. The test should fail due to:
                  It should fail, I said!
              </failure>
            </testcase>
          </testsuite>
          <testsuite id="verification-uuid-2"
                     tests="99"
                     time="22.222"
                     errors="0"
                     failures="33"
                     skipped="0"
                     timestamp="2002-01-01T00:00:00">
            <testcase classname="some.test.TestCase"
                      name="test_foo"
                      time="8"
                      timestamp="2001-02-01T00:01:00" />
            <testcase classname="some.test.TestCase"
                      name="test_failed"
                      time="8"
                      timestamp="2001-02-01T00:02:00">
              <failure>HEEEEEEELP</failure>
            </testcase>
            <testcase classname="some.test.TestCase"
                      name="test_skipped"
                      time="0"
                      timestamp="2001-02-01T00:03:00">
              <skipped>Skipped until Bug: 666 is resolved.</skipped>
            </testcase>
            <testcase classname="some.test.TestCase"
                      name="test_xfail"
                      time="4"
                      timestamp="2001-02-01T00:04:00">
              <!--It is an expected failure due to: something-->
              <!--Traceback:
        HEEELP-->
            </testcase>
          </testsuite>
        </testsuites>

    """

    @classmethod
    def validate(cls, output_destination):
        pass

    def generate(self):
        report = junit.JUnitXML()

        for v in self.verifications:
            test_suite = report.add_test_suite(
                id=v.uuid,
                time=str(v.tests_duration),
                timestamp=v.created_at.strftime(TIME_FORMAT)
            )
            test_suite.setup_final_stats(
                tests=str(v.tests_count),
                skipped=str(v.skipped),
                failures=str(v.failures + v.unexpected_success)
            )

            tests = sorted(v.tests.values(),
                           key=lambda t: (t.get("timestamp", ""), t["name"]))
            for result in tests:
                class_name, name = result["name"].rsplit(".", 1)

                test_id = [tag[3:] for tag in result.get("tags", [])
                           if tag.startswith("id-")]

                test_case = test_suite.add_test_case(
                    id=(test_id[0] if test_id else None),
                    time=result["duration"], name=name, classname=class_name,
                    timestamp=result.get("timestamp"))

                if result["status"] == "success":
                    # nothing to add
                    pass
                elif result["status"] == "uxsuccess":
                    test_case.mark_as_uxsuccess(
                        result.get("reason"))
                elif result["status"] == "fail":
                    test_case.mark_as_failed(
                        result.get("traceback", None))
                elif result["status"] == "xfail":
                    trace = result.get("traceback", None)
                    test_case.mark_as_xfail(
                        result.get("reason", None),
                        f"Traceback:\n{trace}" if trace else None)
                elif result["status"] == "skip":
                    test_case.mark_as_skipped(
                        result.get("reason", None))
                else:
                    # wtf is it?! we should add validation of results...
                    pass

        raw_report = report.to_string()
        if self.output_destination:
            return {"files": {self.output_destination: raw_report},
                    "open": self.output_destination}
        else:
            return {"print": raw_report}
