# Standard Library
import difflib
import time
from json import loads

# 3rd Party
import pytest
import requests
from colorama import Fore, Style
from preggy import assertion
from pymongo import MongoClient


@pytest.fixture(autouse=True)
def cleanup():
    mongo_client = MongoClient("localhost", 27355)
    database = mongo_client.fastlane_test
    database.Task.drop()
    database.Job.drop()

    yield


class RequestClient:
    def __init__(self):
        self._client = requests.Session()

    def get(self, url, headers=None, absolute=False):
        abs_url = url

        if not absolute:
            abs_url = f"http://localhost:10000/{url.lstrip('/')}"
        response = self._client.get(abs_url, headers=headers)

        return response.status_code, response.text, response.headers

    def put(self, url, data, headers=None, absolute=False):
        return self.__submit("PUT", url, data, headers, absolute)

    def post(self, url, data, headers=None, absolute=False):
        return self.__submit("POST", url, data, headers, absolute)

    def __submit(self, method, url, data, headers, absolute):
        abs_url = url

        if not absolute:
            abs_url = f"http://localhost:10000/{url.lstrip('/')}"

        func = getattr(self._client, method.lower())
        response = func(abs_url, json=data, headers=headers)

        return response.status_code, response.text, response.headers


@pytest.fixture()
def client():
    yield RequestClient()


def __show_diff(expected, actual):
    seqm = difflib.SequenceMatcher(None, expected, actual)
    output = [Style.RESET_ALL]

    for opcode, a0, a1, b0, b1 in seqm.get_opcodes():
        if opcode == "equal":
            output.append(seqm.a[a0:a1])
        elif opcode == "insert":
            output.append(Fore.GREEN + seqm.b[b0:b1] + Style.RESET_ALL)
        elif opcode == "delete":
            output.append(Fore.RED + seqm.a[a0:a1] + Style.RESET_ALL)
        elif opcode == "replace":
            output.append(Fore.BLUE + seqm.b[b0:b1] + Style.RESET_ALL)
        else:
            raise RuntimeError("unexpected opcode")

    return "".join(output)


def __validate(topic, execution, **arguments):
    errors = []

    for key, value in arguments.items():
        val = execution[key]

        if isinstance(val, (bytes, str)):
            val = val.strip()

        if val != value:
            if isinstance(val, (bytes, str)):
                diff = __show_diff(value, val)
                errors.append(
                    f"{key} field:\n\tExpected: {value}{Style.RESET_ALL}\n\t"
                    f"Actual:   {val}\n\tDiff:     {diff}\n\t"
                    f"(diff: {Fore.RED}remove{Style.RESET_ALL} "
                    f"{Fore.GREEN}add{Style.RESET_ALL} "
                    f"{Fore.BLUE}replace{Style.RESET_ALL})"
                )
            else:
                errors.append(
                    f"{key} field:\n\tExpected: {value}{Style.RESET_ALL}\n\tActual:   {val}"
                )

    if errors:
        error_msg = "\n".join(errors)
        raise AssertionError(
            f"Execution did not match expectations!\n{Style.RESET_ALL}"
            f"URL: {topic}\n\n{error_msg}"
        )


@assertion
def to_have_finished_with(topic, cli, timeout=30, **kw):
    start = time.time()

    last_obj = None

    while time.time() - start < timeout:
        status_code, body, _ = cli.get(topic, absolute=True)

        if status_code != 200:
            raise AssertionError(f"{topic} could not be found (status: {status_code}).")

        last_obj = loads(body)

        try:
            if __validate(topic, last_obj["execution"], **kw):
                return
        except AssertionError:
            pass

        time.sleep(0.1)

    __validate(topic, last_obj["execution"], **kw)


@assertion
def to_have_execution(topic, cli, execution, execution_count=1, timeout=10):
    start = time.time()

    last_obj = None

    while time.time() - start < timeout:
        status_code, body, _ = cli.get(topic, absolute=True)

        if status_code != 200:
            raise AssertionError(f"{topic} could not be found (status: {status_code}).")

        last_obj = loads(body)

        if last_obj["job"]["executions"]:
            current_execution_count = len(last_obj["job"]["executions"])

            if current_execution_count != execution_count:
                continue
            ex = last_obj["job"]["executions"][0]
            execution["url"] = ex["url"]
            execution["executionId"] = ex["executionId"]

            return

        time.sleep(0.5)

    raise AssertionError(f"Execution for job {topic} has not started.")