'''
# Template handling

Fixture factories can take static strings:

```python
redis = container(
    image='redis:latest',
)
```

But that is not useful when building multiple containers that need to reference
one another or you need to parameterize the fixture. This module provides two
facilities: The ability to reference fixtures using python string template
notation and the ability to know what fixtures this will fetch in at test
collection (and generation) time.

For example:

```
def test_simple_resolve(request):
    # These are parameters declared at import time - they can be reevaluated in the context of multiple tests
    kwargs = {
        'somekey': ['{pytestconfig.getoption("verbose")}'],
    }

    # This can be used in a fixture factory to fill in the templates
    resolved = resolve_fixtures_in_params(request, kwargs)

    # And then the test can access them
    pytestconfig = request.getfixturevalue('pytestconfig')
    assert resolved['somekey'][0] == str(pytestconfig.getoption("verbose"))
}
```

In order to make fixtures generated by a fixture factory more seamless we need
to know a fixtures dependencies at collection time. We have a helper to find
them:

def test_simple_find():
    # These are parameters declared at import time - they can be reevaluated in the context of multiple tests
    kwargs = {
        'somekey': ['{pytestconfig.getoption("verbose")}'],
    }

    dependencies = find_fixtures_in_params(kwargs)
    assert dependencies = set('pytestconfig')
'''

import inspect
from string import Formatter

__all__ = [
    'find_fixtures_in_params',
    'resolve_fixtures_in_params',
]


class FixtureFormatter(Formatter):

    def __init__(self, request):
        self.request = request

    def get_value(self, key, args, kwargs):
        return self.request.getfixturevalue(key)


class Renderer(object):

    def __init__(self, request):
        self.request = request

    def visit_value(self, val):
        if isinstance(val, str):
            return FixtureFormatter(self.request).format(val)
        elif callable(val):
            return val(*[self.request.getfixturevalue(f) for f in inspect.getargspec(val)[0]])
        return val

    def visit_list(self, val):
        return [self.visit(v) for v in val]

    def visit_dict(self, mapping):
        return {self.visit(k): self.visit(v) for (k, v) in mapping.items()}

    def visit(self, value):
        if isinstance(value, dict):
            return self.visit_dict(value)
        elif isinstance(value, list):
            return self.visit_list(value)
        elif value:
            return self.visit_value(value)


class FixtureFinder(object):

    def visit_value(self, val):
        if isinstance(val, str):
            for literal_text, format_spec, conversion, _ in Formatter().parse(val):
                if format_spec:
                    yield format_spec.split('.')[0].split('[')[0]
        elif callable(val):
            yield from inspect.getargspec(val)[0]

    def visit_list(self, val):
        for v in val:
            yield from self.visit(v)

    def visit_dict(self, mapping):
        for k, v in mapping.items():
            yield from self.visit(k)
            yield from self.visit(v)

    def visit(self, value):
        if isinstance(value, dict):
            yield from self.visit_dict(value)
        elif isinstance(value, list):
            yield from self.visit_list(value)
        elif value:
            yield from self.visit_value(value)


def find_fixtures_in_params(value):
    '''
    Walk an object and identify fixtures references in templates in strings.
    '''
    finder = FixtureFinder()
    return set(finder.visit(value))


def resolve_fixtures_in_params(request, value):
    '''
    Walk an object and resolve fixture values referenced in template strings.
    '''
    renderer = Renderer(request)
    return renderer.visit(value)
