# Copyright 2019 Vitaliy Zakaznikov (TestFlows Test Framework http://testflows.com)
#
# 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 json
import threading

from datetime import datetime

import testflows.settings as settings
import testflows._core.cli.arg.type as argtype

from testflows._core.flags import Flags, SKIP
from testflows._core.testtype import TestType
from testflows._core.transform.log.message import message_map
from testflows._core.cli.arg.common import epilog
from testflows._core.cli.arg.common import HelpFormatter
from testflows._core.cli.arg.handlers.handler import Handler as HandlerBase
from testflows._core.transform.log.pipeline import ResultsLogPipeline
from testflows._core.transform.log.message import FailResults, XoutResults
from testflows._core.utils.timefuncs import localfromtimestamp, strftimedelta

testflows = '<span class="testflows-logo"></span> [<span class="logo-test">Test</span><span class="logo-flows">Flows</span>]'
testflows_em = testflows.replace("[", "").replace("]", "")

template = f"""
# Comparison Report
%(body)s
  
---
Generated by {testflows} Open-Source Test Framework

[<span class="logo-test">Test</span><span class="logo-flows">Flows</span>]: https://testflows.com
"""

class Formatter:
    def format_reference(self, data):
        table = data["table"]
        s = "\n\n## Reference\n\n"
        # reference table
        s += " | ".join(table["reference"]["header"]) + "\n"
        s += " | ".join(["---"] * len(table["reference"]["header"])) + "\n"
        for row in table["reference"]["rows"]:
            s += " | ".join(row) + "\n"
        return s

    def format_table(self, data):
        table = data["table"]
        s = "\n\n## Comparison\n\n"
        # comparison table
        s += " | ".join(table["header"]) + "\n"
        s += " | ".join(["---"] * len(table["header"])) + "\n"
        span = '<span class="result result-%(cls)s">%(name)s</span>'
        for row in table["rows"]:
            name, *results = row
            s += " | ".join([name] + [
                span % {'cls': result.lower(), 'name': result} for result in results
            ]) + "\n"
        return s

    def format_chart(self, data):
        script = """
        window.onload = function() {
            window.chart = c3.generate({
                bindto: '#data-chart',
                legend: {
                    position: 'inset',
                    inset: {
                        anchor: 'top-right',
                        x: 50,
                        y: -30,
                        step: 1
                    }
                },
                padding: {
                    top: 30
                },
                data: {
                    x: 'x',
                    columns: [
                        ['x', %(values)s],
                        ['OK', %(ok)s],
                        ['Fail', %(fail)s],
                        ['Known', %(known)s]
                    ],
                    types: {
                        OK: 'area',
                        Fail: 'area',
                        Known: 'area',
                        // 'line', 'spline', 'step', 'area', 'area-step' are also available to stack
                    },
                    groups: [['OK', 'Fail', 'Known']],
                    colors: {
                        'OK': 'rgba(31, 239, 184, 0.7)',
                        'Fail': 'rgba(241, 88, 88, 0.7)',
                        'Known': 'rgba(137, 182, 239, 0.7)'
                    }
                },
                axis: {
                    x: {
                        type: 'category'
                    },
                    y: {
                        tick: {
                            format: d3.format('.0f'),
                            count: 5
                        }
                    }
                }
            });
        };
        """
        script = script % {
            "ok": ",".join([str(c) for c in data["chart"]["ok"]]),
            "fail": ",".join([str(c) for c in data["chart"]["fail"]]),
            "known": ",".join([str(c) for c in data["chart"]["known"]]),
            "values": ",".join([f"'{str(c)}'" for c in data["chart"]["x"]])
        }

        s = (
            '\n\n## Chart\n\n'
            '<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.0/d3.min.js"></script>\n'
            '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.12/c3.min.css">\n'
            '<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.12/c3.min.js"></script>\n'
            '<div><div id="data-chart"></div></div>\n'
            '<script>\n'
            f'{script}\n'
            '</script>'
        )
        return s

    def format(self, data):
        body = self.format_reference(data)
        body += self.format_chart(data)
        body += self.format_table(data)
        return template.strip() % {"body": body}

class Handler(HandlerBase):
    @classmethod
    def add_command(cls, commands):
        parser = commands.add_parser("compare", help="comparison report", epilog=epilog(),
            description="Generate comparison report between test runs.",
            formatter_class=HelpFormatter)

        parser.add_argument("--log", metavar="pattern", type=argtype.file("r", bufsize=1, encoding="utf-8"),
            nargs="+", help="log file pattern", required=True)
        parser.add_argument("--only", metavar="pattern", nargs="+",
            help="compare only selected tests", type=str, required=False)
        parser.add_argument("--order-by", metavar="name", type=str,
            help="attribute that is used to order the logs")
        parser.add_argument("--sort", metavar="direction", type=str,
            help="sort direction. Either 'asc' or 'desc', default: asc", choices=["asc", "desc"], default="asc")
        parser.add_argument("--format", metavar="type", type=str,
            help="output format, default: md (Markdown)", choices=["md"], default="md")
        parser.add_argument("output", metavar="output", type=argtype.file("w", bufsize=1, encoding="utf-8"),
            nargs="?", help='output file, default: stdout', default="-")

        parser.set_defaults(func=cls())

    def chart(self, results):
        chart = {
            "ok": [],
            "fail": [],
            "known": [],
            "x": []
        }
        for r in reversed(list(results.values())):
            counts = r["counts"]

            passed = (counts["module"].ok + counts["suite"].ok + counts["test"].ok
                + counts["feature"].ok + counts["scenario"].ok)

            def xout_counts(testtype):
                return counts[testtype].xok + counts[testtype].xfail + counts[testtype].xerror + counts[testtype].xnull
            xout = (xout_counts("module") + xout_counts("suite") + xout_counts("test")
                    + xout_counts("feature") + xout_counts("scenario"))

            failed = (counts["module"].fail + counts["suite"].fail + counts["test"].fail
                      + counts["feature"].fail + counts["scenario"].fail)
            nulled = (counts["module"].null + counts["suite"].null + counts["test"].null
                      + counts["feature"].null + counts["scenario"].null)
            errored = (counts["module"].error + counts["suite"].error + counts["test"].error
                       + counts["feature"].error + counts["scenario"].error)

            chart["ok"].append(passed)
            chart["fail"].append(failed + errored + nulled)
            chart["known"].append(xout)
            chart["x"].append(r["reference"])
        return chart

    def sort(self, results, order_by=None):
        _results = {}

        def order_key(v):
            started = results[v].get("started", 0)
            if order_by:
                value = "-"
                if results[v].get("tests"):
                    test = list(results[v]["tests"].values())[0]["test"]
                    for attr in test.attributes:
                        if attr.name == order_by:
                            value = attr.value
                return [value, started]
            return [started]

        key_order = sorted(results, key=order_key, reverse=True)

        for i, key in enumerate(key_order):
            _results[key] = results[key]
            ref = order_key(key)
            ref[-1] = f'{localfromtimestamp(ref[-1]):%b %d, %-H:%M}'
            if order_by:
                ref = f'{ref[0]}, {ref[-1]}'
            else:
                ref = ref[-1]
            _results[key]["reference"] = ref
        return _results

    def tests(self, results):
        tests = []
        for r in results.values():
            for test in r["tests"].values():
                if test["test"].p_type < TestType.Test:
                    continue
                tests.append(test["test"].name)
        return sorted(list(set(tests)))

    def table(self, tests, results):
        table = {
            "header": ["Test Name"] + [f'<a href="#ref-{results[r]["reference"]}">{results[r]["reference"]}</a>' for r in results],
            "rows": [],
            "reference": {
                "header": ["Reference", "File"],
                "rows": [[f'<span id="ref-{results[r]["reference"]}"><strong>{results[r]["reference"]}</strong></span>', r] for r in results]
            },
        }
        for test in tests:
            row = [test]
            for result in results.values():
                if result["tests"].get(test):
                    row.append(result["tests"].get(test)["result"].name)
                else:
                    row.append("-")
            table["rows"].append(row)
        return table

    def data(self, results, order_by=None):
        d = dict()
        results = self.sort(results, order_by)
        d["list"] = self.tests(results)
        d["table"] = self.table(d["list"], results)
        d["chart"] = self.chart(results)
        return d

    def generate(self, formatter, results, args):
        output = args.output
        output.write(formatter.format(self.data(results, order_by=args.order_by)))
        output.write("\n")

    def handle(self, args):
        results = {}
        threads = []

        def thread_worker(log, results):
            ResultsLogPipeline(log, results).run()

        for log in args.log:
            log_results = {}
            threads.append(
                threading.Thread(target=thread_worker, args=(log, log_results))
            )
            results[log.name] = log_results
            threads[-1].start()

        for thread in threads:
            thread.join()

        formatter = Formatter()
        self.generate(formatter, results, args)
