"""
# 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
from typing import Any

from _pytest.fixtures import getfixturemarker

from pytest_docker_tools.utils import _FixtureRef

__all__ = [
    "find_fixtures_in_params",
    "resolve_fixtures_in_params",
]


class FixtureFormatter(Formatter):
    def __init__(self, request) -> None:
        self.request = request

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


class Renderer:
    def __init__(self, request) -> None:
        self.request = request

    def visit_value(self, val) -> Any:
        if isinstance(val, str):
            return FixtureFormatter(self.request).format(val)
        elif isinstance(val, _FixtureRef):
            return self.request.getfixturevalue(val.name)
        elif getfixturemarker(val):
            return self.request.getfixturevalue(val.__name__)
        elif callable(val):
            return val(
                *[
                    self.request.getfixturevalue(f)
                    for f in inspect.getfullargspec(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:
    def visit_value(self, val) -> Any:
        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 isinstance(val, _FixtureRef):
            return val.name
        elif getfixturemarker(val):
            return val.__name__
        elif callable(val):
            yield from inspect.getfullargspec(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)
