from .endpoints import Endpoints
from .instance import Instance
import pytest

class ComponentTest:
    """Execution of common test patterns for a component.

    ComponentTest provides standardized tests for all
    CRUDL operations, specified as endpoints for the
    component.
    In addition, arbitrary endpoints can be tested
    with custom validation methods.

    Parameters
    ----------
    comp: Component object
        Component to execute tests for

    Examples
    --------
    comp = ...
    test = ComponentTest(comp)
    test.test_create(2)
    test.test_update_invalid()
    test.test_update_valid(3)
    """
    def __init__(self, comp):
        self.comp = comp

    def test_create_valid(self, count=1, exp_props=None,
                          max_entries=None, sampling=None, generator=None):
        """Test the success and result of valid input to the create endpoint.

        This test creates instances of the component using
        valid input data, generated by the component.

        For each component instance, it checks if all properties,
        specified in valids, are also present in the endpoints
        result and their values match.
        You can limit the set of tested properties by specifying a list
        of expected properties (exp_props), specified by their name.
        """
        print("# # # test create (valid)")
        for valids in self.comp.get_valids(Endpoints.CREATE, count=count,
                                           max_entries=max_entries,
                                           sampling=sampling,
                                           generator=generator):
            if exp_props is not None:
                exp_props_ = [name for name in exp_props if self.comp.props.get_prop(name).spec_name in valids]
            else:
                exp_props_ = [prop.name for prop in self.comp.props.props if prop.spec_name in valids]
            print("valids:", valids)
            with Instance(self.comp, **valids) as instance:
                for name in exp_props_:
                    p = self.comp.props.get_prop(name)
                    assert self.comp.endpoints.res_has_prop(instance.data, p.res_name), \
                            "Property {} not returned by CREATE endpoint".format(p.name)
                    res = self.comp.endpoints.prop_from_res(instance.data, p.res_name)
                    assert p.equals(valids[p.spec_name], res), \
                            "Property {} not the same for spec ({}) and result ({}) of CREATE endpoint".format(
                                p.name, valids[p.spec_name], res
                            )

    def test_create_invalid(self, count=1,
                            max_entries=None, sampling=None, generator=None):
        """Test the success of invalid input to the create endpoint.

        This test attempts to create instance of the component
        using invalid data, generated by the component.

        For each creation attempt, it checks if the invalid creation
        indeed caused raising an exception.
        """
        print("# # # test create (invalid)")
        for invalids in self.comp.get_invalids(Endpoints.CREATE, count=count,
                                               max_entries=max_entries,
                                               sampling=sampling,
                                               generator=generator):
            print("invalids:", invalids)
            references = {
                k:Instance(reference).__enter__()
                for k,reference in self.comp.references.items()
            }
            with pytest.raises(Exception) as e_info:
                try:
                    self.comp.endpoints.create(invalids, references)
                finally:
                    for ref in references.values():
                        ref.__exit__(None, None, None)

    def test_create(self, count=1, exp_props=None):
        """Test the success and result of valid and invalid input to the create endpoint."""
        self.test_create_valid(count, exp_props)
        self.test_create_invalid(count)

    def test_update_valid(self, count=1, exp_props=None,
                          max_entries=None, sampling=None, generator=None):
        """Test the success and result of valid input to the update endpoint.

        This test updates properties of an instance and validates
        that the updated and rerturned values are the same as the
        ones specified in valid payload data.
        You can limit the set of tested properties by specifying a list
        of expected property (exp_props), specified by their name.
        """
        print("# # # test update (valid)")
        for valids in self.comp.get_valids(Endpoints.UPDATE, count=count,
                                           max_entries=max_entries,
                                           sampling=sampling,
                                           generator=generator):
            if exp_props is not None:
                exp_props_ = [name for name in exp_props if self.comp.props.get_prop(name).spec_name in valids]
            else:
                exp_props_ = [prop.name for prop in self.comp.props.props if prop.spec_name in valids]
            print("valids:", valids)
            with Instance(self.comp) as instance:
                data = self.comp.endpoints.update(valids, instance)
                for name in exp_props_:
                    p = self.comp.props.get_prop(name)
                    assert self.comp.endpoints.res_has_prop(data, p.res_name), \
                            "Property {} not returned by UPDATE endpoint".format(p.name)
                    res = self.comp.endpoints.prop_from_res(data, p.res_name)
                    assert p.equals(valids[p.spec_name], res), \
                            "Property {} not the same for spec ({}) and result ({}) of UPDATE endpoint".format(
                                p.name, valids[p.spec_name], res
                            )

    def test_update_invalid(self, count=1,
                            max_entries=None, sampling=None, generator=None):
        """Test the success and result of invalid input to the update endpoint.

        This test checks if the update endpoint indeed raises an exception
        if the provided data is invalid.
        """
        print("# # # test update (invalid)")
        for invalids in self.comp.get_invalids(Endpoints.UPDATE, count=count,
                                               max_entries=max_entries,
                                               sampling=sampling,
                                               generator=generator):
            print("invalids:", invalids)
            with Instance(self.comp) as instance:
                with pytest.raises(Exception) as e_info:
                    data = self.comp.endpoints.update(valids, instance)

    def test_update(self, count=1, exp_props=None):
        """Test the success and result of valid and invalid input to the update endpoint."""
        self.test_update_valid(count, exp_props)
        self.test_update_invalid(count)

    def test_get(self, count=1, times=2, exp_props=None,
                 max_entries=None, sampling=None, generator=None):
        """Test the success and result of the get endpoint.

        This test checks if the data of a component, obtained using the
        get endpoint, is the same as the one used during its creation.
        """
        print("# # # test get")
        for _ in range(times):
            with Instance(self.comp) as instance:
                for valids in self.comp.get_valids(Endpoints.GET, count=count,
                                                   max_entries=max_entries,
                                                   sampling=sampling,
                                                   generator=generator):
                    if exp_props is not None:
                        exp_props_ = [name for name in exp_props if self.comp.props.get_prop(name).spec_name in valids]
                    else:
                        exp_props_ = [prop.name for prop in self.comp.props.props if prop.spec_name in valids]
                    data = self.comp.endpoints.get(valids, instance)
                    for name in exp_props_:
                        prop = self.comp.props.get_prop(name)
                        k = prop.res_name
                        v = self.comp.endpoints.prop_from_res(instance.data, k)
                        assert self.comp.endpoints.res_has_prop(data, k), \
                                "Property {} not returned by UPDATE endpoint".format(p.name)
                        res = self.comp.endpoints.prop_from_res(data, k)
                        assert prop.equals(v, res), \
                                "Property {} not the same for spec ({}) and result ({}) of UPDATE endpoint".format(
                                    prop.name, v, res
                                )

    def test_list(self, count=1, entries=5, total_from_res=len,
                  max_entries=None, sampling=None, generator=None,
                  limit=None, offset=None,
                  same_references=False, assert_empty_children=True):
        """Test the success of the list endpoint.

        This tests checks the count of entries obtained using the
        list endpoints.
        This is done by creating entries components and checking
        if the number of entries in list is actually increased
        accordingly.

        Parameters
        ----------
        count: int, default 1
            Count passed down to the valid data generator
        entries: int, default 5
            Number of entries to create during the test
        total_from_res: callable, default len
            Function to obtain the current component count from
            the list endpoint's result
        limit: int, default None
            Pagination limit, Argument of list endpoint
        offset: int, default None
            Pagination offset, argument of list endpoint
        same_references: boolean, default False
            If True and the component has referenced component,
            the same ones are used for all entries. Then, is is also
            asserted that the list contains a total of 0 elements
            at the start.
            If False and the component has referenced components,
            separate ones are generated for each entry.
        assert_empty_children: boolean, default True
            If true, the test asserts that the list of children
            is empty after creating an entry.
        """
        print("# # # test list")

        if same_references and len(self.comp.references) > 0:
            dummy_inst = Instance(self.comp)
            dummy_inst.__enter__()

        for valids in self.comp.get_valids(Endpoints.LIST, count=count,
                                           max_entries=max_entries,
                                           sampling=sampling,
                                           generator=generator):
            references = {
                k:Instance(reference).__enter__()
                for k,reference in self.comp.references.items()
            } if same_references else None

            try:
                initial = total_from_res(self.comp.endpoints.list(valids, references, limit, offset))

                if same_references and assert_empty_children:
                    assert initial == 0, \
                            "Initial list of references' children not empty"

                elements = []
                for i in range(entries):
                    elements.append(Instance(self.comp, references))
                    elements[-1].__enter__()
                    current = total_from_res(self.comp.endpoints.list(valids, references, limit, offset))
                    assert current == initial + i + 1, \
                            "List size not increased correctly after adding elements"
                for i in range(entries):
                    elements[i].__exit__(None, None, None)
                    current = total_from_res(self.comp.endpoints.list(valids, references, limit, offset))
                    assert current == initial + entries - i - 1, \
                            "List size not decreased correctly after removing elements"

                final_size = total_from_res(self.comp.endpoints.list(valids, references, limit, offset))
                assert final_size == initial, \
                        "Final list size not the same as the initial one"

            finally:
                if references is not None:
                    for reference in references.values():
                        reference.__exit__(None, None, None)

        if same_references and len(self.comp.references) > 0:
            dummy_inst.__exit__(None, None, None)

    def test_pagination(self,
                        count=1,
                        entries=10,
                        total_from_res=len,
                        size_from_res=len,
                        max_entries=None, sampling=None, generator=None,
                        same_references=False):
        """Test the success of the list endpoint.

        This tests checks the count of entries obtained using the
        list endpoints.
        This is done by creating entries components and checking
        if the number of entries in list is actually increased
        accordingly.

        Parameters
        ----------
        count: int, default 1
            Count passed down to the valid data generator
        entries: int, default 10
            Number of entries to create during the test (min: 10)
        total_from_res: callable, default len
            Function to obtain the current component count from
            the list endpoint's result
        count_from_res: callable, default len
            Function to obtain the current component count from
            the list endpoint's result
        same_references: boolean, default False
            If True and the component has referenced component,
            the same ones are used for all entries. Then, is is also
            asserted that the list contains a total of 0 elements
            at the start.
            If False and the component has referenced components,
            separate ones are generated for each entry.
        """
        print("# # # test list pagination")

        def list(values, references, limit=None, offset=None):
            return self.comp.endpoints.list(valids, references, limit, offset)

        if entries < 10:
            raise ValueError("Entries must be at least 10")

        for valids in self.comp.get_valids(Endpoints.LIST,
                                           count=count,
                                           max_entries=max_entries,
                                           sampling=sampling,
                                           generator=generator):
            
            references = {
                k:Instance(reference).__enter__()
                for k,reference in self.comp.references.items()
            } if same_references else None

            try:
                initial = total_from_res(list(valids, references))
                total = initial + entries
                elements = [Instance(self.comp) for _ in range(total - initial)]
                for element in elements:
                    element.__enter__()

                assert total_from_res(list(valids, references)) == total, \
                        "Total list size not as expected"

                assert total_from_res(list(valids, references, 4)) == total, \
                        "Total list size not as expected"
                assert size_from_res(list(valids, references, 3)) == 3, \
                        "Entry count not as specified by limit"
                assert size_from_res(list(valids, references, 2, total - 1)) == 1, \
                        "Only single elements is expected for an offset of total-1"

                for element in elements:
                    element.__exit__(None, None, None)

                assert total_from_res(list(valids, references)) == initial, \
                        "Total number of entries not as expected after cleanup"
            finally:
                if references is not None:
                        for reference in references.values():
                            reference.__exit__(None, None, None)

    def test_valid(self, key, count=1, val_res=None,
                   max_entries=None, sampling=None, generator=None):
        """Test valid input to a non-standard endpoint.

        The custom endpoint is executed for all valid data
        in comp.get_valids(count).
        The result of this request is then validated using
        the custom function.

        Parameters
        ----------
        key: str
            Key to identify the endpoint (as specified
            during initialization of the Endpoints object)
        count: int, default 1
            Count passed down to the valid data generator
        val_res: callable, default None
            Function to validate the result from the
            custom endpoint
            It should return True is the result data is
            valid and False otherwise.
        """
        print("# # # test {} (valid)".format(key))
        for valids in self.comp.get_valids(key, count=count,
                                           max_entries=max_entries,
                                           sampling=sampling,
                                           generator=generator):
            print("valids:", valids)
            res = self.comp.endpoints.others[key](valids)
            if val_res is not None:
                assert val_res(valids, res), \
                        "Custom test for endpoint {} failed".format(key)

    def test_invalid(self, key, count=1,
                            max_entries=None, sampling=None, generator=None):
        """Test invalid input to a non-standard endpoint.

        The custom endpoint is executed for all invalid data
        in comp.get_invalids(count).
        Then, the test checks if the endpoint's execution
        indeed raises an exception.

        Parameters
        ----------
        key: str
            Key to identify the endpoint (as specified
            during initialization of the Endpoints object)
        count: int, default 1
            Count passed down to the valid data generator
        """
        print("# # # test {} (invalid)".format(key))
        for invalids in self.comp.get_invalids(key, count=count,
                                               max_entries=max_entries,
                                               sampling=sampling,
                                               generator=generator):
            print("invalids:", invalids)
            with pytest.raises(Exception) as e_info:
                res = self.comp.endpoints.others[key](invalids)

    def test_child_delete(self):
        """Test invalid deletion of referenced components.

        This test assumes that this component references others and checks
        that their deletion is not allowed if they are referenced.
        """
        references = {
            k:Instance(reference).__enter__()
            for k,reference in self.comp.references.items()
        }
        try:
            with Instance(self.comp, references) as obj:
                for reference in references.values():
                    with pytest.raises(Exception) as e:
                        reference.__exit__(None, None, None)
        finally:
            for reference in references.values():
                reference.__exit__(None, None, None)

    def test(self, key, count=1, val_res=None):
        """Test valid and invalid input to a non-standard endpoint."""
        self.test_valid(key, count, val_res)
        self.test_invalid(key, count)



import pytest
from .util import rand_int, rand_str, rand_uuid
from .property import Property
from .properties import Properties
from .endpoints import Endpoints
from .component import Component

def test_constr():
    p1 = Property("p1", rand_str, rand_int)
    p2 = Property("p2", rand_int, rand_str)
    def create(values, references):
        return values
    def delete(values, instance):
        return instance.data
    endpoints = Endpoints(create=create, delete=delete)
    comp = Component("c1", Properties(p1, p2), endpoints)
    comp_test = ComponentTest(comp)
