import os
import pathlib
import re
from collections.abc import Container, Iterable, Mapping, MutableMapping, Sized
from urllib.parse import unquote

import pytest
from yarl import URL

import aiohttp
from aiohttp import hdrs, web
from aiohttp.test_utils import make_mocked_request
from aiohttp.web import HTTPMethodNotAllowed, HTTPNotFound, Response
from aiohttp.web_urldispatcher import (
    PATH_SEP,
    AbstractResource,
    Domain,
    DynamicResource,
    MaskDomain,
    PlainResource,
    ResourceRoute,
    StaticResource,
    SystemRoute,
    View,
    _default_expect_handler,
)


def make_handler():

    async def handler(request):
        return Response(request)  # pragma: no cover

    return handler


@pytest.fixture
def app():
    return web.Application()


@pytest.fixture
def router(app):
    return app.router


@pytest.fixture
def fill_routes(router):
    def go():
        route1 = router.add_route('GET', '/plain', make_handler())
        route2 = router.add_route('GET', '/variable/{name}',
                                  make_handler())
        resource = router.add_static('/static',
                                     os.path.dirname(aiohttp.__file__))
        return [route1, route2] + list(resource)
    return go


def test_register_uncommon_http_methods(router) -> None:
    uncommon_http_methods = {
        'PROPFIND',
        'PROPPATCH',
        'COPY',
        'LOCK',
        'UNLOCK',
        'MOVE',
        'SUBSCRIBE',
        'UNSUBSCRIBE',
        'NOTIFY'
    }

    for method in uncommon_http_methods:
        router.add_route(method, '/handler/to/path', make_handler())


async def test_add_route_root(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/', handler)
    req = make_mocked_request('GET', '/')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_simple(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/handler/to/path', handler)
    req = make_mocked_request('GET', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_with_matchdict(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/handler/{to}', handler)
    req = make_mocked_request('GET', '/handler/tail')
    info = await router.resolve(req)
    assert info is not None
    assert {'to': 'tail'} == info
    assert handler is info.handler
    assert info.route.name is None


async def test_add_with_matchdict_with_colon(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/handler/{to}', handler)
    req = make_mocked_request('GET', '/handler/1:2:3')
    info = await router.resolve(req)
    assert info is not None
    assert {'to': '1:2:3'} == info
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_with_add_get_shortcut(router) -> None:
    handler = make_handler()
    router.add_get('/handler/to/path', handler)
    req = make_mocked_request('GET', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_with_add_post_shortcut(router) -> None:
    handler = make_handler()
    router.add_post('/handler/to/path', handler)
    req = make_mocked_request('POST', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_with_add_put_shortcut(router) -> None:
    handler = make_handler()
    router.add_put('/handler/to/path', handler)
    req = make_mocked_request('PUT', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_with_add_patch_shortcut(router) -> None:
    handler = make_handler()
    router.add_patch('/handler/to/path', handler)
    req = make_mocked_request('PATCH', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_with_add_delete_shortcut(router) -> None:
    handler = make_handler()
    router.add_delete('/handler/to/path', handler)
    req = make_mocked_request('DELETE', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_route_with_add_head_shortcut(router) -> None:
    handler = make_handler()
    router.add_head('/handler/to/path', handler)
    req = make_mocked_request('HEAD', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 0 == len(info)
    assert handler is info.handler
    assert info.route.name is None


async def test_add_with_name(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/handler/to/path', handler,
                     name='name')
    req = make_mocked_request('GET', '/handler/to/path')
    info = await router.resolve(req)
    assert info is not None
    assert 'name' == info.route.name


async def test_add_with_tailing_slash(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/handler/to/path/', handler)
    req = make_mocked_request('GET', '/handler/to/path/')
    info = await router.resolve(req)
    assert info is not None
    assert {} == info
    assert handler is info.handler


def test_add_invalid_path(router) -> None:
    handler = make_handler()
    with pytest.raises(ValueError):
        router.add_route('GET', '/{/', handler)


def test_add_url_invalid1(router) -> None:
    handler = make_handler()
    with pytest.raises(ValueError):
        router.add_route('post', '/post/{id', handler)


def test_add_url_invalid2(router) -> None:
    handler = make_handler()
    with pytest.raises(ValueError):
        router.add_route('post', '/post/{id{}}', handler)


def test_add_url_invalid3(router) -> None:
    handler = make_handler()
    with pytest.raises(ValueError):
        router.add_route('post', '/post/{id{}', handler)


def test_add_url_invalid4(router) -> None:
    handler = make_handler()
    with pytest.raises(ValueError):
        router.add_route('post', '/post/{id"}', handler)


async def test_add_url_escaping(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/+$', handler)

    req = make_mocked_request('GET', '/+$')
    info = await router.resolve(req)
    assert info is not None
    assert handler is info.handler


async def test_any_method(router) -> None:
    handler = make_handler()
    route = router.add_route(hdrs.METH_ANY, '/', handler)

    req = make_mocked_request('GET', '/')
    info1 = await router.resolve(req)
    assert info1 is not None
    assert route is info1.route

    req = make_mocked_request('POST', '/')
    info2 = await router.resolve(req)
    assert info2 is not None

    assert info1.route is info2.route


async def test_match_second_result_in_table(router) -> None:
    handler1 = make_handler()
    handler2 = make_handler()
    router.add_route('GET', '/h1', handler1)
    router.add_route('POST', '/h2', handler2)
    req = make_mocked_request('POST', '/h2')
    info = await router.resolve(req)
    assert info is not None
    assert {} == info
    assert handler2 is info.handler


async def test_raise_method_not_allowed(router) -> None:
    handler1 = make_handler()
    handler2 = make_handler()
    router.add_route('GET', '/', handler1)
    router.add_route('POST', '/', handler2)
    req = make_mocked_request('PUT', '/')

    match_info = await router.resolve(req)
    assert isinstance(match_info.route, SystemRoute)
    assert {} == match_info

    with pytest.raises(HTTPMethodNotAllowed) as ctx:
        await match_info.handler(req)

    exc = ctx.value
    assert 'PUT' == exc.method
    assert 405 == exc.status
    assert {'POST', 'GET'} == exc.allowed_methods


async def test_raise_method_not_found(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/a', handler)
    req = make_mocked_request('GET', '/b')

    match_info = await router.resolve(req)
    assert isinstance(match_info.route, SystemRoute)
    assert {} == match_info

    with pytest.raises(HTTPNotFound) as ctx:
        await match_info.handler(req)

    exc = ctx.value
    assert 404 == exc.status


def test_double_add_url_with_the_same_name(router) -> None:
    handler1 = make_handler()
    handler2 = make_handler()
    router.add_route('GET', '/get', handler1, name='name')

    regexp = ("Duplicate 'name', already handled by")
    with pytest.raises(ValueError) as ctx:
        router.add_route('GET', '/get_other', handler2, name='name')
    assert re.match(regexp, str(ctx.value))


def test_route_plain(router) -> None:
    handler = make_handler()
    route = router.add_route('GET', '/get', handler, name='name')
    route2 = next(iter(router['name']))
    url = route2.url_for()
    assert '/get' == str(url)
    assert route is route2


def test_route_unknown_route_name(router) -> None:
    with pytest.raises(KeyError):
        router['unknown']


def test_route_dynamic(router) -> None:
    handler = make_handler()
    route = router.add_route('GET', '/get/{name}', handler,
                             name='name')

    route2 = next(iter(router['name']))
    url = route2.url_for(name='John')
    assert '/get/John' == str(url)
    assert route is route2


def test_add_static(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(aiohttp.__file__),
                                 name='static')
    assert router['static'] is resource
    url = resource.url_for(filename='/dir/a.txt')
    assert '/st/dir/a.txt' == str(url)
    assert len(resource) == 2


def test_add_static_append_version(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(__file__),
                                 name='static')
    url = resource.url_for(filename='/data.unknown_mime_type',
                           append_version=True)
    expect_url = '/st/data.unknown_mime_type?' \
                 'v=aUsn8CHEhhszc81d28QmlcBW0KQpfS2F4trgQKhOYd8%3D'
    assert expect_url == str(url)


def test_add_static_append_version_set_from_constructor(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(__file__),
                                 append_version=True,
                                 name='static')
    url = resource.url_for(filename='/data.unknown_mime_type')
    expect_url = '/st/data.unknown_mime_type?' \
                 'v=aUsn8CHEhhszc81d28QmlcBW0KQpfS2F4trgQKhOYd8%3D'
    assert expect_url == str(url)


def test_add_static_append_version_override_constructor(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(__file__),
                                 append_version=True,
                                 name='static')
    url = resource.url_for(filename='/data.unknown_mime_type',
                           append_version=False)
    expect_url = '/st/data.unknown_mime_type'
    assert expect_url == str(url)


def test_add_static_append_version_filename_without_slash(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(__file__),
                                 name='static')
    url = resource.url_for(filename='data.unknown_mime_type',
                           append_version=True)
    expect_url = '/st/data.unknown_mime_type?' \
                 'v=aUsn8CHEhhszc81d28QmlcBW0KQpfS2F4trgQKhOYd8%3D'
    assert expect_url == str(url)


def test_add_static_append_version_non_exists_file(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(__file__),
                                 name='static')
    url = resource.url_for(filename='/non_exists_file', append_version=True)
    assert '/st/non_exists_file' == str(url)


def test_add_static_append_version_non_exists_file_without_slash(
        router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(__file__),
                                 name='static')
    url = resource.url_for(filename='non_exists_file', append_version=True)
    assert '/st/non_exists_file' == str(url)


def test_add_static_append_version_follow_symlink(router, tmpdir) -> None:
    """
    Tests the access to a symlink, in static folder with apeend_version
    """
    tmp_dir_path = str(tmpdir)
    symlink_path = os.path.join(tmp_dir_path, 'append_version_symlink')
    symlink_target_path = os.path.dirname(__file__)
    os.symlink(symlink_target_path, symlink_path, True)

    # Register global static route:
    resource = router.add_static('/st', tmp_dir_path, follow_symlinks=True,
                                 append_version=True)

    url = resource.url_for(
        filename='/append_version_symlink/data.unknown_mime_type')

    expect_url = '/st/append_version_symlink/data.unknown_mime_type?' \
                 'v=aUsn8CHEhhszc81d28QmlcBW0KQpfS2F4trgQKhOYd8%3D'
    assert expect_url == str(url)


def test_add_static_append_version_not_follow_symlink(router, tmpdir) -> None:
    """
    Tests the access to a symlink, in static folder with apeend_version
    """
    tmp_dir_path = str(tmpdir)
    symlink_path = os.path.join(tmp_dir_path, 'append_version_symlink')
    symlink_target_path = os.path.dirname(__file__)
    os.symlink(symlink_target_path, symlink_path, True)

    # Register global static route:
    resource = router.add_static('/st', tmp_dir_path, follow_symlinks=False,
                                 append_version=True)

    filename = '/append_version_symlink/data.unknown_mime_type'
    url = resource.url_for(filename=filename)
    assert '/st/append_version_symlink/data.unknown_mime_type' == str(url)


def test_plain_not_match(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get/path', handler, name='name')
    route = router['name']
    assert route._match('/another/path') is None


def test_dynamic_not_match(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get/{name}', handler, name='name')
    route = router['name']
    assert route._match('/another/path') is None


async def test_static_not_match(router) -> None:
    router.add_static('/pre', os.path.dirname(aiohttp.__file__),
                      name='name')
    resource = router['name']
    ret = await resource.resolve(
        make_mocked_request('GET', '/another/path'))
    assert (None, set()) == ret


def test_dynamic_with_trailing_slash(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get/{name}/', handler, name='name')
    route = router['name']
    assert {'name': 'John'} == route._match('/get/John/')


def test_len(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get1', handler, name='name1')
    router.add_route('GET', '/get2', handler, name='name2')
    assert 2 == len(router)


def test_iter(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get1', handler, name='name1')
    router.add_route('GET', '/get2', handler, name='name2')
    assert {'name1', 'name2'} == set(iter(router))


def test_contains(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get1', handler, name='name1')
    router.add_route('GET', '/get2', handler, name='name2')
    assert 'name1' in router
    assert 'name3' not in router


def test_static_repr(router) -> None:
    router.add_static('/get', os.path.dirname(aiohttp.__file__),
                      name='name')
    assert re.match(r"<StaticResource 'name' /get", repr(router['name']))


def test_static_adds_slash(router) -> None:
    route = router.add_static('/prefix',
                              os.path.dirname(aiohttp.__file__))
    assert '/prefix' == route._prefix


def test_static_remove_trailing_slash(router) -> None:
    route = router.add_static('/prefix/',
                              os.path.dirname(aiohttp.__file__))
    assert '/prefix' == route._prefix


async def test_add_route_with_re(router) -> None:
    handler = make_handler()
    router.add_route('GET', r'/handler/{to:\d+}', handler)

    req = make_mocked_request('GET', '/handler/1234')
    info = await router.resolve(req)
    assert info is not None
    assert {'to': '1234'} == info

    router.add_route('GET', r'/handler/{name}.html', handler)
    req = make_mocked_request('GET', '/handler/test.html')
    info = await router.resolve(req)
    assert {'name': 'test'} == info


async def test_add_route_with_re_and_slashes(router) -> None:
    handler = make_handler()
    router.add_route('GET', r'/handler/{to:[^/]+/?}', handler)
    req = make_mocked_request('GET', '/handler/1234/')
    info = await router.resolve(req)
    assert info is not None
    assert {'to': '1234/'} == info

    router.add_route('GET', r'/handler/{to:.+}', handler)
    req = make_mocked_request('GET', '/handler/1234/5/6/7')
    info = await router.resolve(req)
    assert info is not None
    assert {'to': '1234/5/6/7'} == info


async def test_add_route_with_re_not_match(router) -> None:
    handler = make_handler()
    router.add_route('GET', r'/handler/{to:\d+}', handler)

    req = make_mocked_request('GET', '/handler/tail')
    match_info = await router.resolve(req)
    assert isinstance(match_info.route, SystemRoute)
    assert {} == match_info
    with pytest.raises(HTTPNotFound):
        await match_info.handler(req)


async def test_add_route_with_re_including_slashes(router) -> None:
    handler = make_handler()
    router.add_route('GET', r'/handler/{to:.+}/tail', handler)
    req = make_mocked_request('GET', '/handler/re/with/slashes/tail')
    info = await router.resolve(req)
    assert info is not None
    assert {'to': 're/with/slashes'} == info


def test_add_route_with_invalid_re(router) -> None:
    handler = make_handler()
    with pytest.raises(ValueError) as ctx:
        router.add_route('GET', r'/handler/{to:+++}', handler)
    s = str(ctx.value)
    assert s.startswith("Bad pattern '" +
                        PATH_SEP +
                        "handler" +
                        PATH_SEP +
                        "(?P<to>+++)': nothing to repeat")
    assert ctx.value.__cause__ is None


def test_route_dynamic_with_regex_spec(router) -> None:
    handler = make_handler()
    route = router.add_route('GET', r'/get/{num:^\d+}', handler,
                             name='name')

    url = route.url_for(num='123')
    assert '/get/123' == str(url)


def test_route_dynamic_with_regex_spec_and_trailing_slash(router) -> None:
    handler = make_handler()
    route = router.add_route('GET', r'/get/{num:^\d+}/', handler,
                             name='name')

    url = route.url_for(num='123')
    assert '/get/123/' == str(url)


def test_route_dynamic_with_regex(router) -> None:
    handler = make_handler()
    route = router.add_route('GET', r'/{one}/{two:.+}', handler)

    url = route.url_for(one='1', two='2')
    assert '/1/2' == str(url)


def test_route_dynamic_quoting(router) -> None:
    handler = make_handler()
    route = router.add_route('GET', r'/{arg}', handler)

    url = route.url_for(arg='1 2/текст')
    assert '/1%202/%D1%82%D0%B5%D0%BA%D1%81%D1%82' == str(url)


async def test_regular_match_info(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get/{name}', handler)

    req = make_mocked_request('GET', '/get/john')
    match_info = await router.resolve(req)
    assert {'name': 'john'} == match_info
    assert re.match("<MatchInfo {'name': 'john'}: .+<Dynamic.+>>",
                    repr(match_info))


async def test_match_info_with_plus(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/get/{version}', handler)

    req = make_mocked_request('GET', '/get/1.0+test')
    match_info = await router.resolve(req)
    assert {'version': '1.0+test'} == match_info


async def test_not_found_repr(router) -> None:
    req = make_mocked_request('POST', '/path/to')
    match_info = await router.resolve(req)
    assert "<MatchInfoError 404: Not Found>" == repr(match_info)


async def test_not_allowed_repr(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/path/to', handler)

    handler2 = make_handler()
    router.add_route('POST', '/path/to', handler2)

    req = make_mocked_request('PUT', '/path/to')
    match_info = await router.resolve(req)
    assert "<MatchInfoError 405: Method Not Allowed>" == repr(match_info)


def test_default_expect_handler(router) -> None:
    route = router.add_route('GET', '/', make_handler())
    assert route._expect_handler is _default_expect_handler


def test_custom_expect_handler_plain(router) -> None:

    async def handler(request):
        pass

    route = router.add_route(
        'GET', '/', make_handler(), expect_handler=handler)
    assert route._expect_handler is handler
    assert isinstance(route, ResourceRoute)


def test_custom_expect_handler_dynamic(router) -> None:

    async def handler(request):
        pass

    route = router.add_route(
        'GET', '/get/{name}', make_handler(), expect_handler=handler)
    assert route._expect_handler is handler
    assert isinstance(route, ResourceRoute)


def test_expect_handler_non_coroutine(router) -> None:

    def handler(request):
        pass

    with pytest.raises(AssertionError):
        router.add_route('GET', '/', make_handler(),
                         expect_handler=handler)


async def test_dynamic_match_non_ascii(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/{var}', handler)
    req = make_mocked_request(
        'GET',
        '/%D1%80%D1%83%D1%81%20%D1%82%D0%B5%D0%BA%D1%81%D1%82')
    match_info = await router.resolve(req)
    assert {'var': 'рус текст'} == match_info


async def test_dynamic_match_with_static_part(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/{name}.html', handler)
    req = make_mocked_request('GET', '/file.html')
    match_info = await router.resolve(req)
    assert {'name': 'file'} == match_info


async def test_dynamic_match_two_part2(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/{name}.{ext}', handler)
    req = make_mocked_request('GET', '/file.html')
    match_info = await router.resolve(req)
    assert {'name': 'file', 'ext': 'html'} == match_info


async def test_dynamic_match_unquoted_path(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/{path}/{subpath}', handler)
    resource_id = 'my%2Fpath%7Cwith%21some%25strange%24characters'
    req = make_mocked_request('GET', '/path/{0}'.format(resource_id))
    match_info = await router.resolve(req)
    assert match_info == {
        'path': 'path',
        'subpath': unquote(resource_id)
    }


def test_add_route_not_started_with_slash(router) -> None:
    with pytest.raises(ValueError):
        handler = make_handler()
        router.add_route('GET', 'invalid_path', handler)


def test_add_route_invalid_method(router) -> None:

    sample_bad_methods = {
        'BAD METHOD',
        'B@D_METHOD',
        '[BAD_METHOD]',
        '{BAD_METHOD}',
        '(BAD_METHOD)',
        'B?D_METHOD',
    }

    for bad_method in sample_bad_methods:
        with pytest.raises(ValueError):
            handler = make_handler()
            router.add_route(bad_method, '/path', handler)


def test_routes_view_len(router, fill_routes) -> None:
    fill_routes()
    assert 4 == len(router.routes())


def test_routes_view_iter(router, fill_routes) -> None:
    routes = fill_routes()
    assert list(routes) == list(router.routes())


def test_routes_view_contains(router, fill_routes) -> None:
    routes = fill_routes()
    for route in routes:
        assert route in router.routes()


def test_routes_abc(router) -> None:
    assert isinstance(router.routes(), Sized)
    assert isinstance(router.routes(), Iterable)
    assert isinstance(router.routes(), Container)


def test_named_resources_abc(router) -> None:
    assert isinstance(router.named_resources(), Mapping)
    assert not isinstance(router.named_resources(), MutableMapping)


def test_named_resources(router) -> None:
    route1 = router.add_route('GET', '/plain', make_handler(),
                              name='route1')
    route2 = router.add_route('GET', '/variable/{name}',
                              make_handler(), name='route2')
    route3 = router.add_static('/static',
                               os.path.dirname(aiohttp.__file__),
                               name='route3')
    names = {route1.name, route2.name, route3.name}

    assert 3 == len(router.named_resources())

    for name in names:
        assert name in router.named_resources()
        assert isinstance(router.named_resources()[name],
                          AbstractResource)


def test_resource_iter(router) -> None:
    async def handler(request):
        pass
    resource = router.add_resource('/path')
    r1 = resource.add_route('GET', handler)
    r2 = resource.add_route('POST', handler)
    assert 2 == len(resource)
    assert [r1, r2] == list(resource)


def test_deprecate_bare_generators(router) -> None:
    resource = router.add_resource('/path')

    def gen(request):
        yield

    with pytest.warns(DeprecationWarning):
        resource.add_route('GET', gen)


def test_view_route(router) -> None:
    resource = router.add_resource('/path')

    route = resource.add_route('GET', View)
    assert View is route.handler


def test_resource_route_match(router) -> None:
    async def handler(request):
        pass
    resource = router.add_resource('/path')
    route = resource.add_route('GET', handler)
    assert {} == route.resource._match('/path')


def test_error_on_double_route_adding(router) -> None:
    async def handler(request):
        pass
    resource = router.add_resource('/path')

    resource.add_route('GET', handler)
    with pytest.raises(RuntimeError):
        resource.add_route('GET', handler)


def test_error_on_adding_route_after_wildcard(router) -> None:
    async def handler(request):
        pass
    resource = router.add_resource('/path')

    resource.add_route('*', handler)
    with pytest.raises(RuntimeError):
        resource.add_route('GET', handler)


async def test_http_exception_is_none_when_resolved(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/', handler)
    req = make_mocked_request('GET', '/')
    info = await router.resolve(req)
    assert info.http_exception is None


async def test_http_exception_is_not_none_when_not_resolved(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/', handler)
    req = make_mocked_request('GET', '/abc')
    info = await router.resolve(req)
    assert info.http_exception.status == 404


async def test_match_info_get_info_plain(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/', handler)
    req = make_mocked_request('GET', '/')
    info = await router.resolve(req)
    assert info.get_info() == {'path': '/'}


async def test_match_info_get_info_dynamic(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/{a}', handler)
    req = make_mocked_request('GET', '/value')
    info = await router.resolve(req)
    assert info.get_info() == {
        'pattern': re.compile(PATH_SEP+'(?P<a>[^{}/]+)'),
        'formatter': '/{a}'}


async def test_match_info_get_info_dynamic2(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/{a}/{b}', handler)
    req = make_mocked_request('GET', '/path/to')
    info = await router.resolve(req)
    assert info.get_info() == {
        'pattern': re.compile(PATH_SEP +
                              '(?P<a>[^{}/]+)' +
                              PATH_SEP +
                              '(?P<b>[^{}/]+)'),
        'formatter': '/{a}/{b}'}


def test_static_resource_get_info(router) -> None:
    directory = pathlib.Path(aiohttp.__file__).parent.resolve()
    resource = router.add_static('/st', directory)
    assert resource.get_info() == {'directory': directory,
                                   'prefix': '/st'}


async def test_system_route_get_info(router) -> None:
    handler = make_handler()
    router.add_route('GET', '/', handler)
    req = make_mocked_request('GET', '/abc')
    info = await router.resolve(req)
    assert info.get_info()['http_exception'].status == 404


def test_resources_view_len(router) -> None:
    router.add_resource('/plain')
    router.add_resource('/variable/{name}')
    assert 2 == len(router.resources())


def test_resources_view_iter(router) -> None:
    resource1 = router.add_resource('/plain')
    resource2 = router.add_resource('/variable/{name}')
    resources = [resource1, resource2]
    assert list(resources) == list(router.resources())


def test_resources_view_contains(router) -> None:
    resource1 = router.add_resource('/plain')
    resource2 = router.add_resource('/variable/{name}')
    resources = [resource1, resource2]
    for resource in resources:
        assert resource in router.resources()


def test_resources_abc(router) -> None:
    assert isinstance(router.resources(), Sized)
    assert isinstance(router.resources(), Iterable)
    assert isinstance(router.resources(), Container)


def test_static_route_user_home(router) -> None:
    here = pathlib.Path(aiohttp.__file__).parent
    home = pathlib.Path(os.path.expanduser('~'))
    if not str(here).startswith(str(home)):  # pragma: no cover
        pytest.skip("aiohttp folder is not placed in user's HOME")
    static_dir = '~/' + str(here.relative_to(home))
    route = router.add_static('/st', static_dir)
    assert here == route.get_info()['directory']


def test_static_route_points_to_file(router) -> None:
    here = pathlib.Path(aiohttp.__file__).parent / '__init__.py'
    with pytest.raises(ValueError):
        router.add_static('/st', here)


async def test_404_for_static_resource(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(aiohttp.__file__))
    ret = await resource.resolve(
        make_mocked_request('GET', '/unknown/path'))
    assert (None, set()) == ret


async def test_405_for_resource_adapter(router) -> None:
    resource = router.add_static('/st',
                                 os.path.dirname(aiohttp.__file__))
    ret = await resource.resolve(
        make_mocked_request('POST', '/st/abc.py'))
    assert (None, {'HEAD', 'GET'}) == ret


async def test_check_allowed_method_for_found_resource(router) -> None:
    handler = make_handler()
    resource = router.add_resource('/')
    resource.add_route('GET', handler)
    ret = await resource.resolve(make_mocked_request('GET', '/'))
    assert ret[0] is not None
    assert {'GET'} == ret[1]


def test_url_for_in_static_resource(router) -> None:
    resource = router.add_static('/static',
                                 os.path.dirname(aiohttp.__file__))
    assert URL('/static/file.txt') == resource.url_for(filename='file.txt')


def test_url_for_in_static_resource_pathlib(router) -> None:
    resource = router.add_static('/static',
                                 os.path.dirname(aiohttp.__file__))
    assert URL('/static/file.txt') == resource.url_for(
        filename=pathlib.Path('file.txt'))


def test_url_for_in_resource_route(router) -> None:
    route = router.add_route('GET', '/get/{name}', make_handler(),
                             name='name')
    assert URL('/get/John') == route.url_for(name='John')


def test_subapp_get_info(app) -> None:
    subapp = web.Application()
    resource = subapp.add_subapp('/pre', subapp)
    assert resource.get_info() == {'prefix': '/pre', 'app': subapp}


@pytest.mark.parametrize('domain,error', [
    (None, TypeError),
    ('', ValueError),
    ('http://dom', ValueError),
    ('*.example.com', ValueError),
    ('example$com', ValueError),
])
def test_domain_validation_error(domain, error):
    with pytest.raises(error):
        Domain(domain)


def test_domain_valid():
    assert Domain('example.com:81').canonical == 'example.com:81'
    assert MaskDomain('*.example.com').canonical == r'.*\.example\.com'
    assert Domain('пуни.код').canonical == 'xn--h1ajfq.xn--d1alm'


@pytest.mark.parametrize('a,b,result', [
    ('example.com', 'example.com', True),
    ('example.com:81', 'example.com:81', True),
    ('example.com:81', 'example.com', False),
    ('пуникод', 'xn--d1ahgkhc2a', True),
    ('*.example.com', 'jpg.example.com', True),
    ('*.example.com', 'a.example.com', True),
    ('*.example.com', 'example.com', False),
])
def test_match_domain(a, b, result):
    if '*' in a:
        rule = MaskDomain(a)
    else:
        rule = Domain(a)
    assert rule.match_domain(b) is result


def test_add_subapp_errors(app):
    with pytest.raises(TypeError):
        app.add_subapp(1, web.Application())


def test_subapp_rule_resource(app):
    subapp = web.Application()
    subapp.router.add_get('/', make_handler())
    rule = Domain('example.com')
    assert rule.get_info() == {'domain': 'example.com'}
    resource = app.add_domain('example.com', subapp)
    assert resource.canonical == 'example.com'
    assert resource.get_info() == {'rule': resource._rule, 'app': subapp}
    resource.add_prefix('/a')
    resource.raw_match('/b')
    assert len(resource)
    assert list(resource)
    assert repr(resource).startswith('<MatchedSubAppResource')
    with pytest.raises(RuntimeError):
        resource.url_for()


async def test_add_domain_not_str(app, loop):
    app = web.Application()
    with pytest.raises(TypeError):
        app.add_domain(1, app)


async def test_add_domain(app, loop):
    subapp1 = web.Application()
    h1 = make_handler()
    subapp1.router.add_get('/', h1)
    app.add_domain('example.com', subapp1)

    subapp2 = web.Application()
    h2 = make_handler()
    subapp2.router.add_get('/', h2)
    app.add_domain('*.example.com', subapp2)

    subapp3 = web.Application()
    h3 = make_handler()
    subapp3.router.add_get('/', h3)
    app.add_domain('*', subapp3)

    request = make_mocked_request('GET', '/', {'host': 'example.com'})
    match_info = await app.router.resolve(request)
    assert match_info.route.handler is h1

    request = make_mocked_request('GET', '/', {'host': 'a.example.com'})
    match_info = await app.router.resolve(request)
    assert match_info.route.handler is h2

    request = make_mocked_request('GET', '/', {'host': 'example2.com'})
    match_info = await app.router.resolve(request)
    assert match_info.route.handler is h3

    request = make_mocked_request('POST', '/', {'host': 'example.com'})
    match_info = await app.router.resolve(request)
    assert isinstance(match_info.http_exception, HTTPMethodNotAllowed)


def test_subapp_url_for(app) -> None:
    subapp = web.Application()
    resource = app.add_subapp('/pre', subapp)
    with pytest.raises(RuntimeError):
        resource.url_for()


def test_subapp_repr(app) -> None:
    subapp = web.Application()
    resource = app.add_subapp('/pre', subapp)
    assert repr(resource).startswith(
        '<PrefixedSubAppResource /pre -> <Application')


def test_subapp_len(app) -> None:
    subapp = web.Application()
    subapp.router.add_get('/', make_handler(), allow_head=False)
    subapp.router.add_post('/', make_handler())
    resource = app.add_subapp('/pre', subapp)
    assert len(resource) == 2


def test_subapp_iter(app) -> None:
    subapp = web.Application()
    r1 = subapp.router.add_get('/', make_handler(), allow_head=False)
    r2 = subapp.router.add_post('/', make_handler())
    resource = app.add_subapp('/pre', subapp)
    assert list(resource) == [r1, r2]


def test_invalid_route_name(router) -> None:
    with pytest.raises(ValueError):
        router.add_get('/', make_handler(), name='invalid name')


def test_frozen_router(router) -> None:
    router.freeze()
    with pytest.raises(RuntimeError):
        router.add_get('/', make_handler())


def test_frozen_router_subapp(app) -> None:
    subapp = web.Application()
    subapp.freeze()
    with pytest.raises(RuntimeError):
        app.add_subapp('/pre', subapp)


def test_frozen_app_on_subapp(app) -> None:
    app.freeze()
    subapp = web.Application()
    with pytest.raises(RuntimeError):
        app.add_subapp('/pre', subapp)


def test_set_options_route(router) -> None:
    resource = router.add_static('/static',
                                 os.path.dirname(aiohttp.__file__))
    options = None
    for route in resource:
        if route.method == 'OPTIONS':
            options = route
    assert options is None
    resource.set_options_route(make_handler())
    for route in resource:
        if route.method == 'OPTIONS':
            options = route
    assert options is not None

    with pytest.raises(RuntimeError):
        resource.set_options_route(make_handler())


def test_dynamic_url_with_name_started_from_underscore(router) -> None:
    route = router.add_route('GET', '/get/{_name}', make_handler())
    assert URL('/get/John') == route.url_for(_name='John')


def test_cannot_add_subapp_with_empty_prefix(app) -> None:
    subapp = web.Application()
    with pytest.raises(ValueError):
        app.add_subapp('', subapp)


def test_cannot_add_subapp_with_slash_prefix(app) -> None:
    subapp = web.Application()
    with pytest.raises(ValueError):
        app.add_subapp('/', subapp)


async def test_convert_empty_path_to_slash_on_freezing(router) -> None:
    handler = make_handler()
    route = router.add_get('', handler)
    resource = route.resource
    assert resource.get_info() == {'path': ''}
    router.freeze()
    assert resource.get_info() == {'path': '/'}


def test_deprecate_non_coroutine(router) -> None:
    def handler(request):
        pass

    with pytest.warns(DeprecationWarning):
        router.add_route('GET', '/handler', handler)


def test_plain_resource_canonical() -> None:
    canonical = '/plain/path'
    res = PlainResource(path=canonical)
    assert res.canonical == canonical


def test_dynamic_resource_canonical() -> None:
    canonicals = {
        '/get/{name}': '/get/{name}',
        r'/get/{num:^\d+}': '/get/{num}',
        r'/handler/{to:\d+}': r'/handler/{to}',
        r'/{one}/{two:.+}': r'/{one}/{two}',
    }
    for pattern, canonical in canonicals.items():
        res = DynamicResource(path=pattern)
        assert res.canonical == canonical


def test_static_resource_canonical() -> None:
    prefix = '/prefix'
    directory = str(os.path.dirname(aiohttp.__file__))
    canonical = prefix
    res = StaticResource(prefix=prefix, directory=directory)
    assert res.canonical == canonical


def test_prefixed_subapp_resource_canonical(app) -> None:
    canonical = '/prefix'
    subapp = web.Application()
    res = subapp.add_subapp(canonical, subapp)
    assert res.canonical == canonical


async def test_prefixed_subapp_overlap(app) -> None:
    """
    Subapp should not overshadow other subapps with overlapping prefixes
    """
    subapp1 = web.Application()
    handler1 = make_handler()
    subapp1.router.add_get('/a', handler1)
    app.add_subapp('/s', subapp1)

    subapp2 = web.Application()
    handler2 = make_handler()
    subapp2.router.add_get('/b', handler2)
    app.add_subapp('/ss', subapp2)

    match_info = await app.router.resolve(make_mocked_request('GET', '/s/a'))
    assert match_info.route.handler is handler1
    match_info = await app.router.resolve(make_mocked_request('GET', '/ss/b'))
    assert match_info.route.handler is handler2


async def test_prefixed_subapp_empty_route(app) -> None:
    subapp = web.Application()
    handler = make_handler()
    subapp.router.add_get('', handler)
    app.add_subapp('/s', subapp)

    match_info = await app.router.resolve(make_mocked_request('GET', '/s'))
    assert match_info.route.handler is handler
    match_info = await app.router.resolve(make_mocked_request('GET', '/s/'))
    assert "<MatchInfoError 404: Not Found>" == repr(match_info)


async def test_prefixed_subapp_root_route(app) -> None:
    subapp = web.Application()
    handler = make_handler()
    subapp.router.add_get('/', handler)
    app.add_subapp('/s', subapp)

    match_info = await app.router.resolve(make_mocked_request('GET', '/s/'))
    assert match_info.route.handler is handler
    match_info = await app.router.resolve(make_mocked_request('GET', '/s'))
    assert "<MatchInfoError 404: Not Found>" == repr(match_info)
