import os.path
from inspect import isabstract
from urllib.parse import urljoin, urlparse, parse_qs
from abc import ABCMeta, abstractproperty

from .utils import to_os_path

MYPY = False
if MYPY:
    # MYPY is set to True when run under Mypy.
    from typing import Any, Dict, Hashable, List, Optional, Sequence, Text, Tuple, Type, Union, cast
    from .manifest import Manifest
    Fuzzy = Dict[Optional[Tuple[str, str, str]], List[int]]
    PageRanges = Dict[str, List[int]]

item_types = {}  # type: Dict[str, Type[ManifestItem]]


class ManifestItemMeta(ABCMeta):
    """Custom metaclass that registers all the subclasses in the
    item_types dictionary according to the value of their item_type
    attribute, and otherwise behaves like an ABCMeta."""

    def __new__(cls, name, bases, attrs):
        # type: (Type[ManifestItemMeta], str, Tuple[type], Dict[str, Any]) -> ManifestItemMeta
        inst = super().__new__(cls, name, bases, attrs)
        if isabstract(inst):
            return inst

        assert issubclass(inst, ManifestItem)
        if MYPY:
            inst_ = cast(Type[ManifestItem], inst)
            item_type = cast(str, inst_.item_type)
        else:
            inst_ = inst
            assert isinstance(inst_.item_type, str)
            item_type = inst_.item_type

        item_types[item_type] = inst_

        return inst_


class ManifestItem(metaclass=ManifestItemMeta):
    __slots__ = ("_tests_root", "path")

    def __init__(self, tests_root, path):
        # type: (Text, Text) -> None
        self._tests_root = tests_root
        self.path = path

    @abstractproperty
    def id(self):
        # type: () -> Text
        """The test's id (usually its url)"""
        pass

    @abstractproperty
    def item_type(self):
        # type: () -> str
        """The item's type"""
        pass

    @property
    def path_parts(self):
        # type: () -> Tuple[Text, ...]
        return tuple(self.path.split(os.path.sep))

    def key(self):
        # type: () -> Hashable
        """A unique identifier for the test"""
        return (self.item_type, self.id)

    def __eq__(self, other):
        # type: (Any) -> bool
        if not hasattr(other, "key"):
            return False
        return bool(self.key() == other.key())

    def __hash__(self):
        # type: () -> int
        return hash(self.key())

    def __repr__(self):
        # type: () -> str
        return f"<{self.__module__}.{self.__class__.__name__} id={self.id!r}, path={self.path!r}>"

    def to_json(self):
        # type: () -> Tuple[Any, ...]
        return ()

    @classmethod
    def from_json(cls,
                  manifest,  # type: Manifest
                  path,  # type: Text
                  obj  # type: Any
                  ):
        # type: (...) -> ManifestItem
        path = to_os_path(path)
        tests_root = manifest.tests_root
        assert tests_root is not None
        return cls(tests_root, path)


class URLManifestItem(ManifestItem):
    __slots__ = ("url_base", "_url", "_extras", "_flags")

    def __init__(self,
                 tests_root,  # type: Text
                 path,  # type: Text
                 url_base,  # type: Text
                 url,  # type: Optional[Text]
                 **extras  # type: Any
                 ):
        # type: (...) -> None
        super().__init__(tests_root, path)
        assert url_base[0] == "/"
        self.url_base = url_base
        assert url is None or url[0] != "/"
        self._url = url
        self._extras = extras
        parsed_url = urlparse(self.url)
        self._flags = (set(parsed_url.path.rsplit("/", 1)[1].split(".")[1:-1]) |
                       set(parse_qs(parsed_url.query).get("wpt_flags", [])))

    @property
    def id(self):
        # type: () -> Text
        return self.url

    @property
    def url(self):
        # type: () -> Text
        rel_url = self._url or self.path.replace(os.path.sep, "/")
        # we can outperform urljoin, because we know we just have path relative URLs
        if self.url_base == "/":
            return "/" + rel_url
        return urljoin(self.url_base, rel_url)

    @property
    def https(self):
        # type: () -> bool
        return "https" in self._flags or "serviceworker" in self._flags or "serviceworker-module" in self._flags

    @property
    def h2(self):
        # type: () -> bool
        return "h2" in self._flags

    @property
    def subdomain(self):
        # type: () -> bool
        # Note: this is currently hard-coded to check for `www`, rather than
        # all possible valid subdomains. It can be extended if needed.
        return "www" in self._flags

    def to_json(self):
        # type: () -> Tuple[Optional[Text], Dict[Any, Any]]
        rel_url = None if self._url == self.path.replace(os.path.sep, "/") else self._url
        rv = (rel_url, {})  # type: Tuple[Optional[Text], Dict[Any, Any]]
        return rv

    @classmethod
    def from_json(cls,
                  manifest,  # type: Manifest
                  path,  # type: Text
                  obj  # type: Tuple[Text, Dict[Any, Any]]
                  ):
        # type: (...) -> URLManifestItem
        path = to_os_path(path)
        url, extras = obj
        tests_root = manifest.tests_root
        assert tests_root is not None
        return cls(tests_root,
                   path,
                   manifest.url_base,
                   url,
                   **extras)


class TestharnessTest(URLManifestItem):
    __slots__ = ()

    item_type = "testharness"

    @property
    def timeout(self):
        # type: () -> Optional[Text]
        return self._extras.get("timeout")

    @property
    def pac(self):
        # type: () -> Optional[Text]
        return self._extras.get("pac")

    @property
    def testdriver(self):
        # type: () -> Optional[Text]
        return self._extras.get("testdriver")

    @property
    def jsshell(self):
        # type: () -> Optional[Text]
        return self._extras.get("jsshell")

    @property
    def script_metadata(self):
        # type: () -> Optional[List[Tuple[Text, Text]]]
        return self._extras.get("script_metadata")

    def to_json(self):
        # type: () -> Tuple[Optional[Text], Dict[Text, Any]]
        rv = super().to_json()
        if self.timeout is not None:
            rv[-1]["timeout"] = self.timeout
        if self.pac is not None:
            rv[-1]["pac"] = self.pac
        if self.testdriver:
            rv[-1]["testdriver"] = self.testdriver
        if self.jsshell:
            rv[-1]["jsshell"] = True
        if self.script_metadata:
            rv[-1]["script_metadata"] = [(k, v) for (k,v) in self.script_metadata]
        return rv


class RefTest(URLManifestItem):
    __slots__ = ("references",)

    item_type = "reftest"

    def __init__(self,
                 tests_root,  # type: Text
                 path,  # type: Text
                 url_base,  # type: Text
                 url,  # type: Optional[Text]
                 references=None,  # type: Optional[List[Tuple[Text, Text]]]
                 **extras  # type: Any
                 ):
        super().__init__(tests_root, path, url_base, url, **extras)
        if references is None:
            self.references = []  # type: List[Tuple[Text, Text]]
        else:
            self.references = references

    @property
    def timeout(self):
        # type: () -> Optional[Text]
        return self._extras.get("timeout")

    @property
    def viewport_size(self):
        # type: () -> Optional[Text]
        return self._extras.get("viewport_size")

    @property
    def dpi(self):
        # type: () -> Optional[Text]
        return self._extras.get("dpi")

    @property
    def fuzzy(self):
        # type: () -> Fuzzy
        fuzzy = self._extras.get("fuzzy", {})  # type: Union[Fuzzy, List[Tuple[Optional[Sequence[Text]], List[int]]]]
        if not isinstance(fuzzy, list):
            return fuzzy

        rv = {}  # type: Fuzzy
        for k, v in fuzzy:  # type: Tuple[Optional[Sequence[Text]], List[int]]
            if k is None:
                key = None  # type: Optional[Tuple[Text, Text, Text]]
            else:
                # mypy types this as Tuple[Text, ...]
                assert len(k) == 3
                key = tuple(k)  # type: ignore
            rv[key] = v
        return rv

    def to_json(self):  # type: ignore
        # type: () -> Tuple[Optional[Text], List[Tuple[Text, Text]], Dict[Text, Any]]
        rel_url = None if self._url == self.path else self._url
        rv = (rel_url, self.references, {})  # type: Tuple[Optional[Text], List[Tuple[Text, Text]], Dict[Text, Any]]
        extras = rv[-1]
        if self.timeout is not None:
            extras["timeout"] = self.timeout
        if self.viewport_size is not None:
            extras["viewport_size"] = self.viewport_size
        if self.dpi is not None:
            extras["dpi"] = self.dpi
        if self.fuzzy:
            extras["fuzzy"] = list(self.fuzzy.items())
        return rv

    @classmethod
    def from_json(cls,  # type: ignore
                  manifest,  # type: Manifest
                  path,  # type: Text
                  obj  # type: Tuple[Text, List[Tuple[Text, Text]], Dict[Any, Any]]
                  ):
        # type: (...) -> RefTest
        tests_root = manifest.tests_root
        assert tests_root is not None
        path = to_os_path(path)
        url, references, extras = obj
        return cls(tests_root,
                   path,
                   manifest.url_base,
                   url,
                   references,
                   **extras)


class PrintRefTest(RefTest):
    __slots__ = ("references",)

    item_type = "print-reftest"

    @property
    def page_ranges(self):
        # type: () -> PageRanges
        return self._extras.get("page_ranges", {})

    def to_json(self):  # type: ignore
        rv = super().to_json()
        if self.page_ranges:
            rv[-1]["page_ranges"] = self.page_ranges
        return rv


class ManualTest(URLManifestItem):
    __slots__ = ()

    item_type = "manual"


class ConformanceCheckerTest(URLManifestItem):
    __slots__ = ()

    item_type = "conformancechecker"


class VisualTest(URLManifestItem):
    __slots__ = ()

    item_type = "visual"


class CrashTest(URLManifestItem):
    __slots__ = ()

    item_type = "crashtest"

    @property
    def timeout(self):
        # type: () -> Optional[Text]
        return None


class WebDriverSpecTest(URLManifestItem):
    __slots__ = ()

    item_type = "wdspec"

    @property
    def timeout(self):
        # type: () -> Optional[Text]
        return self._extras.get("timeout")

    def to_json(self):
        # type: () -> Tuple[Optional[Text], Dict[Text, Any]]
        rv = super().to_json()
        if self.timeout is not None:
            rv[-1]["timeout"] = self.timeout
        return rv


class SupportFile(ManifestItem):
    __slots__ = ()

    item_type = "support"

    @property
    def id(self):
        # type: () -> Text
        return self.path
