import asyncio
import io
import time
from collections import defaultdict
from datetime import datetime
from typing import Dict, List, Mapping, Optional, Sequence, Tuple

import aiohttp
import pytest
from _pytest.config import Config
from _pytest.terminal import TerminalReporter
from discord import AsyncWebhookAdapter, Colour, Embed, File, Webhook
from discord.errors import Forbidden, HTTPException, InvalidArgument, NotFound
from pytest_md_report.plugin import extract_pytest_stats

from ._const import HelpMsg, Option, TestResultType
from ._opt_retriever import DiscordOptRetriever


MAX_EMBED_LEN = 2048
MAX_EMBEDS_LEN = 6000
MAX_EMBED_CT = 10


def pytest_addoption(parser):
    group = parser.getgroup("discord", "notify test results to a discord channel")

    group.addoption(
        Option.DISCORD_WEBHOOK.cmdoption_str,
        metavar="WEBHOOK_URL",
        help=Option.DISCORD_WEBHOOK.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_WEBHOOK.envvar_str),
    )
    group.addoption(
        Option.DISCORD_VERBOSE.cmdoption_str,
        metavar="VERBOSITY_LEVEL",
        type=int,
        default=None,
        help=Option.DISCORD_VERBOSE.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_VERBOSE.envvar_str),
    )
    group.addoption(
        Option.DISCORD_USERNAME.cmdoption_str,
        help=Option.DISCORD_USERNAME.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_USERNAME.envvar_str),
    )
    group.addoption(
        Option.DISCORD_SUCCESS_ICON.cmdoption_str,
        metavar="ICON_URL",
        help=Option.DISCORD_SUCCESS_ICON.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_SUCCESS_ICON.envvar_str),
    )
    group.addoption(
        Option.DISCORD_SKIP_ICON.cmdoption_str,
        metavar="ICON_URL",
        help=Option.DISCORD_SKIP_ICON.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_SKIP_ICON.envvar_str),
    )
    group.addoption(
        Option.DISCORD_FAIL_ICON.cmdoption_str,
        metavar="ICON_URL",
        help=Option.DISCORD_FAIL_ICON.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_FAIL_ICON.envvar_str),
    )
    group.addoption(
        Option.DISCORD_ATTACH_FILE.cmdoption_str,
        action="store_true",
        default=None,
        help=Option.DISCORD_ATTACH_FILE.help_msg
        + HelpMsg.EXTRA_MSG_TEMPLATE.format(Option.DISCORD_ATTACH_FILE.envvar_str),
    )

    parser.addini(
        Option.DISCORD_WEBHOOK.inioption_str, default=None, help=Option.DISCORD_WEBHOOK.help_msg,
    )
    parser.addini(
        Option.DISCORD_VERBOSE.inioption_str, default=None, help=Option.DISCORD_VERBOSE.help_msg,
    )
    parser.addini(
        Option.DISCORD_USERNAME.inioption_str, default=None, help=Option.DISCORD_USERNAME.help_msg,
    )
    parser.addini(
        Option.DISCORD_SUCCESS_ICON.inioption_str,
        default=None,
        help=Option.DISCORD_SUCCESS_ICON.help_msg,
    )
    parser.addini(
        Option.DISCORD_SKIP_ICON.inioption_str,
        default=None,
        help=Option.DISCORD_SKIP_ICON.help_msg,
    )
    parser.addini(
        Option.DISCORD_FAIL_ICON.inioption_str,
        default=None,
        help=Option.DISCORD_FAIL_ICON.help_msg,
    )
    parser.addini(
        Option.DISCORD_ATTACH_FILE.inioption_str,
        type="bool",
        default=None,
        help=Option.DISCORD_ATTACH_FILE.help_msg,
    )


def _normalize_stat_name(name: str) -> str:
    if name == "error":
        return "errors"

    return name


def _make_results_message(reporter: TerminalReporter) -> Tuple[str, Dict[str, int]]:
    messages = []
    stat_count_map = {}

    for name in ["failed", "passed", "skipped", "error", "xfailed", "xpassed"]:
        count = len(reporter.getreports(name))
        stat_count_map[name] = count

        if count:
            messages.append("{} {}".format(count, _normalize_stat_name(name)))

    return (", ".join(messages), stat_count_map)


def _decorate_code_block(lang: str, text: str) -> str:
    return "```{lang}\n{body}\n```\n".format(lang=lang, body=text)


def _extract_longrepr(reporter: TerminalReporter) -> List[str]:
    messages = []

    for stat_key, values in reporter.stats.items():
        if not stat_key or stat_key not in ["failed", "error"]:
            continue

        for i, value in enumerate(values):
            try:
                if value.longrepr:
                    messages.append(
                        "# {}: #{}\n{}".format(
                            stat_key, i + 1, _decorate_code_block(lang="py", text=value.longrepr),
                        )
                    )
            except AttributeError:
                pass

    return messages


def _make_md_report(config: Config) -> str:
    from pytest_md_report import ColorPolicy, ZerosRender, make_md_report, retrieve_stat_count_map

    opt_retriever = DiscordOptRetriever(config)
    verbosity_level = opt_retriever.retrieve_verbosity_level()
    reporter = config.pluginmanager.get_plugin("terminalreporter")
    stat_count_map = retrieve_stat_count_map(reporter)

    stash_md_report_color = (
        config.option.md_report_color if hasattr(config.option, "md_report_color") else None
    )
    stash_md_report_zeros = (
        config.option.md_report_zeros if hasattr(config.option, "md_report_zeros") else None
    )

    if not hasattr(config.option, "md_report_verbose"):
        config.option.md_report_verbose = max(0, verbosity_level - 1)
    if not hasattr(config.option, "md_report_margin"):
        config.option.md_report_margin = 1

    try:
        config.option.md_report_color = ColorPolicy.NEVER
        config.option.md_report_zeros = ZerosRender.EMPTY

        return make_md_report(config, reporter, stat_count_map)
    finally:
        config.option.md_report_color = stash_md_report_color
        config.option.md_report_zeros = stash_md_report_zeros


_logs = []


def extract_result_type(pytest_stats: Mapping[str, int]) -> TestResultType:
    if sum([pytest_stats[name] for name in ("failed", "error")]):
        return TestResultType.FAIL

    if (
        sum([pytest_stats[name] for name in ("skipped", "xfailed", "xpassed")])
        and pytest_stats["passed"] == 0
    ):
        return TestResultType.SKIP

    return TestResultType.SUCCESS


_result_type_to_colour = {
    TestResultType.SUCCESS: Colour.green(),
    TestResultType.SKIP: Colour.gold(),
    TestResultType.FAIL: Colour.red(),
}


def pytest_unconfigure(config):
    if config.option.help:
        return

    opt_retriever = DiscordOptRetriever(config)
    url = opt_retriever.retrieve_webhook_url()
    if not url:
        return

    verbosity_level = opt_retriever.retrieve_verbosity_level()
    reporter = config.pluginmanager.get_plugin("terminalreporter")
    md_report = _make_md_report(config)

    try:
        duration = time.time() - reporter._sessionstarttime
    except AttributeError:
        return

    message, stat_count_map = _make_results_message(reporter)

    if sum([stat_count_map[name] for name in ["failed", "error"]]):
        avatar_url = opt_retriever.retrieve_fail_icon()
        colour = Colour.red()
    elif (
        sum([stat_count_map[name] for name in ("skipped", "xfailed", "xpassed")])
        and stat_count_map["passed"] == 0
    ):
        avatar_url = opt_retriever.retrieve_skip_icon()
        colour = Colour.gold()
    else:
        avatar_url = opt_retriever.retrieve_success_icon()
        colour = Colour.green()

    embeds = []  # type: List[Embed]
    embeds_len_ct = 100
    exceeds_embeds_limit = False

    embed_summary = Embed(
        description="{} in {:.1f} seconds".format(message, duration), colour=colour
    )
    embed_summary.set_footer(
        text="start at {}".format(
            datetime.fromtimestamp(reporter._sessionstarttime).strftime("%d. %b %H:%M:%S%z")
        )
    )
    embeds.append(embed_summary)

    if verbosity_level >= 1:
        pytest_stats = extract_pytest_stats(
            reporter=reporter,
            outcomes=["passed", "failed", "error", "skipped", "xfailed", "xpassed"],
            verbosity_level=max(0, verbosity_level - 1),
        )
        result_lines_map = defaultdict(list)

        for key, stats in pytest_stats.items():
            result_lines_map[extract_result_type(stats)].append(
                "`{}`: {}".format(
                    ":".join(key),
                    ", ".join(
                        ["`{}` {}".format(ct, outcome) for outcome, ct in stats.items() if ct > 0]
                    ),
                )
            )

        for result_type, result_lines in result_lines_map.items():
            embeds.append(
                Embed(
                    description="\n".join(result_lines)[:MAX_EMBED_LEN],
                    colour=_result_type_to_colour[result_type],
                )
            )

        description = _decorate_code_block(lang="md", text="# test results\n{}".format(md_report))
        embed_summary = Embed(description=description, colour=colour,)
        embeds_len_ct += len(description)

        longreprs = _extract_longrepr(reporter)
        for i, description in enumerate(longreprs):
            description = description[-MAX_EMBED_LEN:]

            if len(embeds) >= MAX_EMBED_CT or (len(description) + embeds_len_ct) > MAX_EMBEDS_LEN:
                embeds.append(
                    Embed(
                        description="and other {} failed".format(len(longreprs) - i), colour=colour
                    )
                )
                exceeds_embeds_limit = True
                break

            embeds.append(Embed(description=description, colour=colour))
            embeds_len_ct += len(description)

    header = "test summary info: {} tests".format(sum(stat_count_map.values()))
    attach_file = None

    if opt_retriever.retrieve_attach_file() or exceeds_embeds_limit:
        attach_file = File(
            io.BytesIO(
                "# {}\n{}\n\n{}".format(
                    header, md_report, "\n\n".join(_extract_longrepr(reporter))
                ).encode("utf8")
            ),
            datetime.fromtimestamp(reporter._sessionstarttime).strftime(
                "pytest_%Y-%m-%dT%H:%M:%S.md"
            ),
        )

    loop = asyncio.get_event_loop()
    loop.run_until_complete(
        _send_message(
            reporter=reporter,
            url=url,
            header=header,
            username=opt_retriever.retrieve_username(),
            avatar_url=avatar_url,
            embeds=embeds,
            attach_file=attach_file,
        )
    )


async def _send_message(
    reporter: TerminalReporter,
    url: str,
    header: str,
    username: Optional[str],
    avatar_url: Optional[str],
    embeds: Sequence[Embed],
    attach_file: Optional[File] = None,
) -> None:
    async with aiohttp.ClientSession() as session:
        try:
            webhook = Webhook.from_url(url, adapter=AsyncWebhookAdapter(session))
        except (InvalidArgument, HTTPException, NotFound, Forbidden) as e:
            reporter.write_line("pytest-discord error: {}".format(str(e)))
            return

        await webhook.send(
            header, username=username, avatar_url=avatar_url, embeds=embeds, file=attach_file,
        )


@pytest.hookimpl()
def pytest_report_teststatus(report):
    if report.longreprtext:
        _logs.append(report.longreprtext)
    if report.capstdout:
        _logs.append(report.capstdout)
    if report.capstderr:
        _logs.append(report.capstderr)
