import textwrap

import pytest

import falcon
from falcon import testing
from falcon.routing import DefaultRouter


@pytest.fixture
def client():
    return testing.TestClient(falcon.API())


@pytest.fixture
def router():
    router = DefaultRouter()

    router.add_route(
        '/repos', {}, ResourceWithId(1))
    router.add_route(
        '/repos/{org}', {}, ResourceWithId(2))
    router.add_route(
        '/repos/{org}/{repo}', {}, ResourceWithId(3))
    router.add_route(
        '/repos/{org}/{repo}/commits', {}, ResourceWithId(4))
    router.add_route(
        u'/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}',
        {}, ResourceWithId(5))

    router.add_route(
        '/teams/{id}', {}, ResourceWithId(6))
    router.add_route(
        '/teams/{id}/members', {}, ResourceWithId(7))

    router.add_route(
        '/teams/default', {}, ResourceWithId(19))
    router.add_route(
        '/teams/default/members/thing', {}, ResourceWithId(19))

    router.add_route(
        '/user/memberships', {}, ResourceWithId(8))
    router.add_route(
        '/emojis', {}, ResourceWithId(9))
    router.add_route(
        '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}/full',
        {}, ResourceWithId(10))
    router.add_route(
        '/repos/{org}/{repo}/compare/all', {}, ResourceWithId(11))

    # NOTE(kgriffs): The ordering of these calls is significant; we
    # need to test that the {id} field does not match the other routes,
    # regardless of the order they are added.
    router.add_route(
        '/emojis/signs/0', {}, ResourceWithId(12))
    router.add_route(
        '/emojis/signs/{id}', {}, ResourceWithId(13))
    router.add_route(
        '/emojis/signs/42', {}, ResourceWithId(14))
    router.add_route(
        '/emojis/signs/42/small.jpg', {}, ResourceWithId(23))
    router.add_route(
        '/emojis/signs/78/small.png', {}, ResourceWithId(24))

    # Test some more special chars
    router.add_route(
        '/emojis/signs/78/small(png)', {}, ResourceWithId(25))
    router.add_route(
        '/emojis/signs/78/small_png', {}, ResourceWithId(26))
    router.add_route('/images/{id}.gif', {}, ResourceWithId(27))

    router.add_route(
        '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}/part',
        {}, ResourceWithId(15))
    router.add_route(
        '/repos/{org}/{repo}/compare/{usr0}:{branch0}',
        {}, ResourceWithId(16))
    router.add_route(
        '/repos/{org}/{repo}/compare/{usr0}:{branch0}/full',
        {}, ResourceWithId(17))

    router.add_route(
        '/gists/{id}/{representation}', {}, ResourceWithId(21))
    router.add_route(
        '/gists/{id}/raw', {}, ResourceWithId(18))
    router.add_route(
        '/gists/first', {}, ResourceWithId(20))

    router.add_route('/item/{q}', {}, ResourceWithId(28))

    # ----------------------------------------------------------------
    # Routes with field converters
    # ----------------------------------------------------------------

    router.add_route(
        '/cvt/teams/{id:int(min=7)}', {}, ResourceWithId(29))
    router.add_route(
        '/cvt/teams/{id:int(min=7)}/members', {}, ResourceWithId(30))
    router.add_route(
        '/cvt/teams/default', {}, ResourceWithId(31))
    router.add_route(
        '/cvt/teams/default/members/{id:int}-{tenure:int}', {}, ResourceWithId(32))

    router.add_route(
        '/cvt/repos/{org}/{repo}/compare/{usr0}:{branch0:int}...{usr1}:{branch1:int}/part',
        {}, ResourceWithId(33))
    router.add_route(
        '/cvt/repos/{org}/{repo}/compare/{usr0}:{branch0:int}',
        {}, ResourceWithId(34))
    router.add_route(
        '/cvt/repos/{org}/{repo}/compare/{usr0}:{branch0:int}/full',
        {}, ResourceWithId(35))

    return router


class ResourceWithId(object):
    def __init__(self, resource_id):
        self.resource_id = resource_id

    def __repr__(self):
        return 'ResourceWithId({0})'.format(self.resource_id)

    def on_get(self, req, resp):
        resp.body = self.resource_id


class SpamConverter(object):
    def __init__(self, times, eggs=False):
        self._times = times
        self._eggs = eggs

    def convert(self, fragment):
        item = fragment
        if self._eggs:
            item += '&eggs'

        return ', '.join(item for i in range(self._times))


# =====================================================================
# Regression tests for use cases reported by users
# =====================================================================


def test_user_regression_versioned_url():
    router = DefaultRouter()
    router.add_route('/{version}/messages', {}, ResourceWithId(2))

    resource, __, __, __ = router.find('/v2/messages')
    assert resource.resource_id == 2

    router.add_route('/v2', {}, ResourceWithId(1))

    resource, __, __, __ = router.find('/v2')
    assert resource.resource_id == 1

    resource, __, __, __ = router.find('/v2/messages')
    assert resource.resource_id == 2

    resource, __, __, __ = router.find('/v1/messages')
    assert resource.resource_id == 2

    route = router.find('/v1')
    assert route is None


def test_user_regression_recipes():
    router = DefaultRouter()
    router.add_route(
        '/recipes/{activity}/{type_id}',
        {},
        ResourceWithId(1)
    )
    router.add_route(
        '/recipes/baking',
        {},
        ResourceWithId(2)
    )

    resource, __, __, __ = router.find('/recipes/baking/4242')
    assert resource.resource_id == 1

    resource, __, __, __ = router.find('/recipes/baking')
    assert resource.resource_id == 2

    route = router.find('/recipes/grilling')
    assert route is None


@pytest.mark.parametrize('uri_template,path,expected_params', [
    ('/serviceRoot/People|{field}', '/serviceRoot/People|susie', {'field': 'susie'}),
    ('/serviceRoot/People[{field}]', "/serviceRoot/People['calvin']", {'field': "'calvin'"}),
    ('/serviceRoot/People({field})', "/serviceRoot/People('hobbes')", {'field': "'hobbes'"}),
    ('/serviceRoot/People({field})', "/serviceRoot/People('hob)bes')", {'field': "'hob)bes'"}),
    ('/serviceRoot/People({field})(z)', '/serviceRoot/People(hobbes)(z)', {'field': 'hobbes'}),
    ("/serviceRoot/People('{field}')", "/serviceRoot/People('rosalyn')", {'field': 'rosalyn'}),
    ('/^{field}', '/^42', {'field': '42'}),
    ('/+{field}', '/+42', {'field': '42'}),
    (
        '/foo/{first}_{second}/bar',
        '/foo/abc_def_ghijk/bar',

        # NOTE(kgriffs): The regex pattern is greedy, so this is
        # expected. We can not change this behavior in a minor
        # release, since it would be a breaking change. If there
        # is enough demand for it, we could introduce an option
        # to toggle this behavior.
        {'first': 'abc_def', 'second': 'ghijk'},
    ),

    # NOTE(kgriffs): Why someone would use a question mark like this
    # I have no idea (esp. since it would have to be encoded to avoid
    # being mistaken for the query string separator). Including it only
    # for completeness.
    ('/items/{x}?{y}', '/items/1080?768', {'x': '1080', 'y': '768'}),

    ('/items/{x}|{y}', '/items/1080|768', {'x': '1080', 'y': '768'}),
    ('/items/{x},{y}', '/items/1080,768', {'x': '1080', 'y': '768'}),
    ('/items/{x}^^{y}', '/items/1080^^768', {'x': '1080', 'y': '768'}),
    ('/items/{x}*{y}*', '/items/1080*768*', {'x': '1080', 'y': '768'}),
    ('/thing-2/something+{field}+', '/thing-2/something+42+', {'field': '42'}),
    ('/thing-2/something*{field}/notes', '/thing-2/something*42/notes', {'field': '42'}),
    (
        '/thing-2/something+{field}|{q}/notes',
        '/thing-2/something+else|z/notes',
        {'field': 'else', 'q': 'z'},
    ),
    (
        "serviceRoot/$metadata#Airports('{field}')/Name",
        "serviceRoot/$metadata#Airports('KSFO')/Name",
        {'field': 'KSFO'},
    ),
])
def test_user_regression_special_chars(uri_template, path, expected_params):
    router = DefaultRouter()

    router.add_route(uri_template, {}, ResourceWithId(1))

    route = router.find(path)
    assert route is not None

    resource, __, params, __ = route
    assert resource.resource_id == 1
    assert params == expected_params


# =====================================================================
# Other tests
# =====================================================================


@pytest.mark.parametrize('uri_template', [
    {},
    set(),
    object()
])
def test_not_str(uri_template):
    app = falcon.API()
    with pytest.raises(TypeError):
        app.add_route(uri_template, ResourceWithId(-1))


def test_root_path():
    router = DefaultRouter()
    router.add_route('/', {}, ResourceWithId(42))

    resource, __, __, __ = router.find('/')
    assert resource.resource_id == 42

    expected_src = textwrap.dedent("""
        def find(path, return_values, patterns, converters, params):
            path_len = len(path)
            if path_len > 0:
                if path[0] == '':
                    if path_len == 1:
                        return return_values[0]
                    return None
                return None
            return None
    """).strip()

    assert router.finder_src == expected_src


@pytest.mark.parametrize('uri_template', [
    '/{field}{field}',
    '/{field}...{field}',
    '/{field}/{another}/{field}',
    '/{field}/something/something/{field}/something',
])
def test_duplicate_field_names(uri_template):
    router = DefaultRouter()
    with pytest.raises(ValueError):
        router.add_route(uri_template, {}, ResourceWithId(1))


@pytest.mark.parametrize('uri_template,path', [
    ('/items/thing', '/items/t'),
    ('/items/{x}|{y}|', '/items/1080|768'),
    ('/items/{x}*{y}foo', '/items/1080*768foobar'),
    ('/items/{x}*768*', '/items/1080*768***'),
])
def test_match_entire_path(uri_template, path):
    router = DefaultRouter()

    router.add_route(uri_template, {}, ResourceWithId(1))

    route = router.find(path)
    assert route is None


@pytest.mark.parametrize('uri_template', [
    '/teams/{conflict}',  # simple vs simple
    '/emojis/signs/{id_too}',  # another simple vs simple
    '/repos/{org}/{repo}/compare/{complex}:{vs}...{complex2}:{conflict}',
    '/teams/{id:int}/settings',  # converted vs. non-converted
])
def test_conflict(router, uri_template):
    with pytest.raises(ValueError):
        router.add_route(uri_template, {}, ResourceWithId(-1))


@pytest.mark.parametrize('uri_template', [
    '/repos/{org}/{repo}/compare/{simple_vs_complex}',
    '/repos/{complex}.{vs}.{simple}',
    '/repos/{org}/{repo}/compare/{complex}:{vs}...{complex2}/full',
])
def test_non_conflict(router, uri_template):
    router.add_route(uri_template, {}, ResourceWithId(-1))


@pytest.mark.parametrize('uri_template', [
    # Missing field name
    '/{}',
    '/repos/{org}/{repo}/compare/{}',
    '/repos/{complex}.{}.{thing}',

    # Field names must be valid Python identifiers
    '/{9v}',
    '/{524hello}/world',
    '/hello/{1world}',
    '/repos/{complex}.{9v}.{thing}/etc',
    '/{*kgriffs}',
    '/{@kgriffs}',
    '/repos/{complex}.{v}.{@thing}/etc',
    '/{-kgriffs}',
    '/repos/{complex}.{-v}.{thing}/etc',
    '/repos/{simple-thing}/etc',

    # Neither fields nor literal segments may not contain whitespace
    '/this and that',
    '/this\tand\tthat'
    '/this\nand\nthat'
    '/{thing }/world',
    '/{thing\t}/world',
    '/{\nthing}/world',
    '/{th\ving}/world',
    '/{ thing}/world',
    '/{ thing }/world',
    '/{thing}/wo rld',
    '/{thing} /world',
    '/repos/{or g}/{repo}/compare/{thing}',
    '/repos/{org}/{repo}/compare/{th\ting}',
])
def test_invalid_field_name(router, uri_template):
    with pytest.raises(ValueError):
        router.add_route(uri_template, {}, ResourceWithId(-1))


def test_print_src(router):
    """Diagnostic test that simply prints the router's find() source code.

    Example:

        $ tox -e py27_debug -- -k test_print_src -s
    """
    print('\n\n' + router.finder_src + '\n')


def test_override(router):
    router.add_route('/emojis/signs/0', {}, ResourceWithId(-1))

    resource, __, __, __ = router.find('/emojis/signs/0')
    assert resource.resource_id == -1


def test_literal_segment(router):
    resource, __, __, __ = router.find('/emojis/signs/0')
    assert resource.resource_id == 12

    resource, __, __, __ = router.find('/emojis/signs/1')
    assert resource.resource_id == 13

    resource, __, __, __ = router.find('/emojis/signs/42')
    assert resource.resource_id == 14

    resource, __, __, __ = router.find('/emojis/signs/42/small.jpg')
    assert resource.resource_id == 23

    route = router.find('/emojis/signs/1/small')
    assert route is None


@pytest.mark.parametrize('path', [
    '/teams',
    '/emojis/signs',
    '/gists',
    '/gists/42',
])
def test_dead_segment(router, path):
    route = router.find(path)
    assert route is None


@pytest.mark.parametrize('path', [
    '/repos/racker/falcon/compare/foo',
    '/repos/racker/falcon/compare/foo/full',
])
def test_malformed_pattern(router, path):
    route = router.find(path)
    assert route is None


def test_literal(router):
    resource, __, __, __ = router.find('/user/memberships')
    assert resource.resource_id == 8


@pytest.mark.parametrize('path,expected_params', [
    ('/cvt/teams/007', {'id': 7}),
    ('/cvt/teams/1234/members', {'id': 1234}),
    ('/cvt/teams/default/members/700-5', {'id': 700, 'tenure': 5}),
    (
        '/cvt/repos/org/repo/compare/xkcd:353',
        {'org': 'org', 'repo': 'repo', 'usr0': 'xkcd', 'branch0': 353},
    ),
    (
        '/cvt/repos/org/repo/compare/gunmachan:1234...kumamon:5678/part',
        {
            'org': 'org',
            'repo': 'repo',
            'usr0': 'gunmachan',
            'branch0': 1234,
            'usr1': 'kumamon',
            'branch1': 5678,
        }
    ),
    (
        '/cvt/repos/xkcd/353/compare/susan:0001/full',
        {'org': 'xkcd', 'repo': '353', 'usr0': 'susan', 'branch0': 1},
    )
])
def test_converters(router, path, expected_params):
    __, __, params, __ = router.find(path)
    assert params == expected_params


@pytest.mark.parametrize('uri_template', [
    '/foo/{bar:int(0)}',
    '/foo/{bar:int(num_digits=0)}',
    '/foo/{bar:int(-1)}/baz',
    '/foo/{bar:int(num_digits=-1)}/baz',
])
def test_converters_with_invalid_options(router, uri_template):
    # NOTE(kgriffs): Sanity-check that errors are properly bubbled up
    # when calling add_route(). Additional checks can be found
    # in test_uri_converters.py
    with pytest.raises(ValueError):
        router.add_route(uri_template, {}, ResourceWithId(1))


@pytest.mark.parametrize('uri_template', [
    '/foo/{bar:}',
    '/foo/{bar:unknown}/baz',
])
def test_converters_malformed_specification(router, uri_template):
    with pytest.raises(ValueError):
        router.add_route(uri_template, {}, ResourceWithId(1))


def test_variable(router):
    resource, __, params, __ = router.find('/teams/42')
    assert resource.resource_id == 6
    assert params == {'id': '42'}

    __, __, params, __ = router.find('/emojis/signs/stop')
    assert params == {'id': 'stop'}

    __, __, params, __ = router.find('/gists/42/raw')
    assert params == {'id': '42'}

    __, __, params, __ = router.find('/images/42.gif')
    assert params == {'id': '42'}


def test_single_character_field_name(router):
    __, __, params, __ = router.find('/item/1234')
    assert params == {'q': '1234'}


@pytest.mark.parametrize('path,expected_id', [
    ('/teams/default', 19),
    ('/teams/default/members', 7),
    ('/cvt/teams/default', 31),
    ('/cvt/teams/default/members/1234-10', 32),
    ('/teams/1234', 6),
    ('/teams/1234/members', 7),
    ('/gists/first', 20),
    ('/gists/first/raw', 18),
    ('/gists/first/pdf', 21),
    ('/gists/1776/pdf', 21),
    ('/emojis/signs/78', 13),
    ('/emojis/signs/78/small.png', 24),
    ('/emojis/signs/78/small(png)', 25),
    ('/emojis/signs/78/small_png', 26),
])
def test_literal_vs_variable(router, path, expected_id):
    resource, __, __, __ = router.find(path)
    assert resource.resource_id == expected_id


@pytest.mark.parametrize('path', [
    # Misc.
    '/this/does/not/exist',
    '/user/bogus',
    '/repos/racker/falcon/compare/johndoe:master...janedoe:dev/bogus',

    # Literal vs variable (teams)
    '/teams',
    '/teams/42/members/undefined',
    '/teams/42/undefined',
    '/teams/42/undefined/segments',
    '/teams/default/members/undefined',
    '/teams/default/members/thing/undefined',
    '/teams/default/members/thing/undefined/segments',
    '/teams/default/undefined',
    '/teams/default/undefined/segments',

    # Literal vs. variable (converters)
    '/cvt/teams/default/members',  # 'default' can't be converted to an int
    '/cvt/teams/NaN',
    '/cvt/teams/default/members/NaN',

    # Literal vs variable (emojis)
    '/emojis/signs',
    '/emojis/signs/0/small',
    '/emojis/signs/0/undefined',
    '/emojis/signs/0/undefined/segments',
    '/emojis/signs/20/small',
    '/emojis/signs/20/undefined',
    '/emojis/signs/42/undefined',
    '/emojis/signs/78/undefined',
])
def test_not_found(router, path):
    route = router.find(path)
    assert route is None


def test_subsegment_not_found(router):
    route = router.find('/emojis/signs/0/x')
    assert route is None


def test_multivar(router):
    resource, __, params, __ = router.find('/repos/racker/falcon/commits')
    assert resource.resource_id == 4
    assert params == {'org': 'racker', 'repo': 'falcon'}

    resource, __, params, __ = router.find('/repos/racker/falcon/compare/all')
    assert resource.resource_id == 11
    assert params == {'org': 'racker', 'repo': 'falcon'}


@pytest.mark.parametrize('url_postfix,resource_id', [
    ('', 5),
    ('/full', 10),
    ('/part', 15),
])
def test_complex(router, url_postfix, resource_id):
    uri = '/repos/racker/falcon/compare/johndoe:master...janedoe:dev'
    resource, __, params, __ = router.find(uri + url_postfix)

    assert resource.resource_id == resource_id
    assert (params == {
        'org': 'racker',
        'repo': 'falcon',
        'usr0': 'johndoe',
        'branch0': 'master',
        'usr1': 'janedoe',
        'branch1': 'dev',
    })


@pytest.mark.parametrize('url_postfix,resource_id,expected_template', [
    ('', 16, '/repos/{org}/{repo}/compare/{usr0}:{branch0}'),
    ('/full', 17, '/repos/{org}/{repo}/compare/{usr0}:{branch0}/full')
])
def test_complex_alt(router, url_postfix, resource_id, expected_template):
    uri = '/repos/falconry/falcon/compare/johndoe:master' + url_postfix
    resource, __, params, uri_template = router.find(uri)

    assert resource.resource_id == resource_id
    assert (params == {
        'org': 'falconry',
        'repo': 'falcon',
        'usr0': 'johndoe',
        'branch0': 'master',
    })
    assert uri_template == expected_template


def test_options_converters_set(router):
    router.options.converters['spam'] = SpamConverter

    router.add_route('/{food:spam(3, eggs=True)}', {}, ResourceWithId(1))
    resource, __, params, __ = router.find('/spam')

    assert params == {'food': 'spam&eggs, spam&eggs, spam&eggs'}


@pytest.mark.parametrize('converter_name', [
    'spam',
    'spam_2'
])
def test_options_converters_update(router, converter_name):
    router.options.converters.update({
        'spam': SpamConverter,
        'spam_2': SpamConverter,
    })

    template = '/{food:' + converter_name + '(3, eggs=True)}'
    router.add_route(template, {}, ResourceWithId(1))
    resource, __, params, __ = router.find('/spam')

    assert params == {'food': 'spam&eggs, spam&eggs, spam&eggs'}


@pytest.mark.parametrize('name', [
    'has whitespace',
    'whitespace ',
    ' whitespace ',
    ' whitespace',
    'funky$character',
    '42istheanswer',
    'with-hyphen',
])
def test_options_converters_invalid_name(router, name):
    with pytest.raises(ValueError):
        router.options.converters[name] = object


def test_options_converters_invalid_name_on_update(router):
    with pytest.raises(ValueError):
        router.options.converters.update({
            'valid_name': SpamConverter,
            '7eleven': SpamConverter,
        })
