import abc
import sys
from datetime import datetime, timezone
from functools import partial
from typing import Iterable
from unittest import TestCase

from packaging.version import parse

from apiserver.tests.api_client import APIClient
from apiserver.config_repo import config

log = config.logger(__file__)


class TestServiceInterface(metaclass=abc.ABCMeta):
    api = abc.abstractproperty()

    @abc.abstractmethod
    def defer(self, func, *args, can_fail=False, **kwargs):
        pass


class TestService(TestCase, TestServiceInterface):
    @property
    def api(self):
        return self._api

    @api.setter
    def api(self, value):
        self._api = value

    def defer(self, func, *args, can_fail=False, **kwargs):
        self._deferred.append((can_fail, partial(func, *args, **kwargs)))

    def _create_temp_helper(
        self,
        service,
        object_name,
        create_endpoint,
        delete_endpoint,
        create_params,
        *,
        client=None,
        delete_params=None,
    ):
        client = client or self.api
        res, data = client.send(f"{service}.{create_endpoint}", create_params)

        object_id = data["id"]
        self.defer(
            client.send,
            f"{service}.{delete_endpoint}",
            can_fail=True,
            data={object_name: object_id, "force": True, **(delete_params or {})},
        )
        return object_id

    @staticmethod
    def update_missing(target: dict, **update):
        target.update({k: v for k, v in update.items() if k not in target})

    def create_temp(self, service, *, client=None, delete_params=None, **kwargs) -> str:
        return self._create_temp_helper(
            service=service,
            create_endpoint="create",
            delete_endpoint="delete",
            object_name=service.rstrip("s"),
            create_params=kwargs,
            client=client,
            delete_params=delete_params,
        )

    def setUp(self, version="999.0"):
        self._api = APIClient(base_url=f"http://localhost:8008/v{version}")
        self._deferred = []
        self._version = parse(version)
        header(self.id())

    def tearDown(self):
        log.info("Cleanup...")
        for can_fail, func in reversed(self._deferred):
            try:
                func()
            except Exception as ex:
                if not can_fail:
                    log.exception(ex)
        self._deferred = []

    def assertEqualNoOrder(self, first: Iterable, second: Iterable):
        """Compares 2 sequences regardless of their items order"""
        self.assertEqual(set(first), set(second))


def header(info, title="=" * 20):
    print(title, info, title, file=sys.stderr)


def utc_now_tz_aware() -> datetime:
    """
    Returns utc now with the utc time zone.
    Suitable for subsequent usage with functions that
    make use of tz info like 'timestamp'
    """
    return datetime.now(timezone.utc)