import datetime
from unittest import skipIf

import pytest

from pykechain.enums import Multiplicity, Category, Classification, PropertyType
from pykechain.exceptions import NotFoundError, MultipleFoundError, APIError, IllegalArgumentError
from pykechain.models import PartSet, Part
from tests.classes import TestBetamax
from tests.utils import TEST_FLAG_IS_PIM3


class TestParts(TestBetamax):

    def setUp(self):
        super().setUp()
        self._part = None

    def tearDown(self):
        if self._part:
            self._part.delete()
        super().tearDown()

    def test_retrieve_parts(self):
        parts = self.project.parts()

        # Check if there are parts
        self.assertTrue(len(parts))
        self.assertIsInstance(parts[0], Part)

    def test_retrieve_single_part(self):
        part_to_retrieve = self.project.parts()[0]
        part = self.project.part(part_to_retrieve.name)

        self.assertTrue(part)

    def test_base_comparison(self):
        all_parts = self.project.parts()

        tip = ' See Base.__eq__() method.'
        self.assertFalse(all_parts[0] == all_parts[1], msg='2 different parts must not be equal.' + tip)
        self.assertTrue(all_parts[0] == all_parts[0], msg='The same part must be equal.' + tip)
        self.assertFalse(all_parts[0] == 5, msg='A part must not be equal to a non-pykechain object.' + tip)

    def test_base_hash(self):
        wheel = self.project.model('Wheel')
        a_dict = dict()

        try:
            a_dict[wheel] = 3
        except TypeError:
            self.assertTrue(False, msg='Parts must be hashable See Base.__hash__() method.')

    def test_part_attributes(self):
        attributes = ['_client', '_json_data', 'id', 'name', 'created_at', 'updated_at', 'ref',
                      'category', 'classification', 'parent_id', 'description', 'multiplicity',
                      '_cached_children', 'properties']

        obj = self.project.parts(limit=1)[0]
        for attribute in attributes:
            with self.subTest(msg=attribute):
                self.assertTrue(hasattr(obj, attribute), "Could not find '{}' in the object: '{}'".format(
                    attribute, obj.__dict__.keys()))

    def test_retrieve_single_unknown(self):
        with self.assertRaises(NotFoundError):
            self.project.part('123lladadwd')

    def test_retrieve_single_multiple(self):
        with self.assertRaises(MultipleFoundError):
            self.project.part()

    def test_retrieve_models(self):
        wheel = self.project.model('Wheel')

        self.assertTrue(self.project.parts(model=wheel))

    def test_retrieve_model_unknown(self):
        with self.assertRaises(NotFoundError):
            self.client.model('123lladadwd')

    def test_retrieve_model_multiple(self):
        with self.assertRaises(MultipleFoundError):
            self.project.model()

    def test_scope(self):
        first_part = self.project.parts()[0]

        self.assertEqual(first_part.scope(), self.project)

    def test_part_set_iterator(self):
        for part in self.project.parts():
            self.assertTrue(part.name)

    def test_part_set_get_item_invalid(self):
        part_set = self.project.parts()

        with self.assertRaises(NotImplementedError):
            # noinspection PyStatementEffect
            part_set['testing']

    def test_wrongly_create_model(self):
        # setUp
        bike_model = self.project.model(name='Bike')

        # testing
        with self.assertRaises(IllegalArgumentError):
            self.client.create_model(name='Multiplicity does not exist', parent=bike_model, multiplicity='TEN')

    def test_part_add_delete_part(self):
        bike = self.project.part('Bike')
        wheel_model = self.project.model('Wheel')

        wheel = bike.add(wheel_model, name='Test Wheel')
        wheel.delete()

        wheel = wheel_model.add_to(bike)
        wheel.delete()

        with self.assertRaises(APIError):
            bike.delete()

    def test_create_part_where_parent_is_model(self):
        # setUp
        bike_model = self.project.model(name='Bike')

        # testing
        with self.assertRaises(IllegalArgumentError):
            self._part = self.client.create_part(name='Parent should be instance',
                                                 parent=bike_model, model=bike_model)

    def test_create_part_where_model_is_instance(self):
        # setUp
        bike_instance = self.project.part(name='Bike')

        # testing
        with self.assertRaises(IllegalArgumentError):
            self._part = self.client.create_part(name='Model should not be instance',
                                                 parent=bike_instance, model=bike_instance)

    def test_create_model_where_parent_is_instance(self):
        # setUp
        bike_instance = self.project.part(name='Bike')

        # testing
        with self.assertRaises(IllegalArgumentError):
            self._part = self.client.create_model(name='Parent should be model',
                                                  parent=bike_instance, multiplicity=Multiplicity.ONE)

    def test_create_proxy_model_where_model_is_instance(self):
        # setUp
        bike_instance = self.project.part(name='Bike')

        # testing
        with self.assertRaises(IllegalArgumentError):
            self._part = self.client.create_proxy_model(name='Model should not be instance',
                                                        model=bike_instance, parent=bike_instance)

    def test_create_proxy_model_where_parent_is_instance(self):
        # setUp
        bike_instance = self.project.part(name='Bike')
        bike_model = self.project.model(name='Bike')

        # testing
        with self.assertRaises(IllegalArgumentError):
            self._part = self.client.create_proxy_model(name='Parent should not be instance',
                                                        model=bike_model, parent=bike_instance)

    def test_add_to_wrong_categories(self):
        # This test has the purpose of testing of whether APIErrors are raised when illegal operations are
        # performed (e.g. operation that should be performed on part instances but instead are being performed
        # on part models and vice versa)
        project = self.project

        bike_model = project.model('Bike')
        bike_instance = project.part('Bike')
        wheel_model = project.model('Wheel')

        with self.assertRaises(APIError):
            self._part = bike_model.add(wheel_model)

        front_fork_instance = project.part('Front Fork')

        with self.assertRaises(APIError):
            self._part = front_fork_instance.add_to(bike_instance)

        with self.assertRaises(APIError):
            self._part = bike_instance.add_model('Engine')

        with self.assertRaises(APIError):
            self._part = bike_instance.add_property('Electrical Power')

        with self.assertRaises(APIError):
            self._part = bike_model.add_with_properties(wheel_model)

    def test_part_html_table(self):
        part = self.project.part('Front Wheel')

        self.assertIn("</table>", part._repr_html_())

    def test_part_set_html_table(self):
        parts = self.project.parts()

        self.assertIn("</table>", parts._repr_html_())

    def test_part_set_html_categories(self):
        parts = self.project.parts(category=None)

        self.assertIn("<th>Category</th>", parts._repr_html_())

    # version 1.1.2 and later
    def test_part_set_with_limit(self):
        limit = 5
        parts = self.project.parts(limit=limit)

        self.assertEqual(len(parts), limit)

    def test_part_set_with_batch(self):
        batch = 5
        parts = self.project.parts(batch=batch)
        self.assertTrue(len(parts) >= batch)

    # version 1.1.3 and later
    def test_retrieve_parent_of_part(self):
        frame = self.project.part('Frame')  # type: Part
        self.assertTrue(hasattr(frame, 'parent_id'))
        parent_of_frame = frame.parent()
        self.assertIsInstance(parent_of_frame, type(frame))

    def test_retrieve_children_of_part(self):
        bike = self.project.part('Bike')  # type: Part
        self.assertIsInstance(bike, Part)
        children_of_bike = bike.children()
        self.assertIsInstance(children_of_bike, (PartSet, list))
        self.assertTrue(len(children_of_bike) >= 1)  # eg. Wheels ...

    def test_retrieve_siblings_of_part(self):
        """Test if the siblings of a part is the same as the children of the parent of the part"""
        frame = self.project.part('Frame')  # type: Part
        siblings_of_frame = frame.siblings()
        self.assertIsInstance(siblings_of_frame, PartSet)
        self.assertTrue(len(siblings_of_frame) >= 1)  # eg. Wheels ...

        # double check that the children of the parent of frame are the same as the siblings of frame
        children_of_parent_of_frame = frame.parent().children()  # type: PartSet
        self.assertEqual(len(children_of_parent_of_frame), len(siblings_of_frame))
        children_of_parent_of_frame_ids = [p.id for p in children_of_parent_of_frame]
        for sibling in siblings_of_frame:
            self.assertIn(sibling.id, children_of_parent_of_frame_ids,
                          'sibling {} is appearing in the siblings method and not in the children of '
                          'parent method'.format(sibling))

    # new in 1.3+
    def test_kwargs_on_part_retrieval(self):
        # test that the additional kwargs are added to the query filter on the api
        bikes = self.project.parts('Bike', order=True)  # type:PartSet
        self.assertTrue(len(bikes) == 1)
        self.assertTrue(self.client.last_url.find('order'))

    # new in 1.5+
    def test_edit_part_instance_name(self):
        front_fork = self.project.part('Front Fork')
        front_fork_old_name = str(front_fork.name)
        front_fork.edit(name='Front Fork - updated')

        front_fork_updated = self.project.part('Front Fork - updated')
        self.assertEqual(front_fork.id, front_fork_updated.id)
        self.assertEqual(front_fork.name, front_fork_updated.name)
        self.assertEqual(front_fork.name, 'Front Fork - updated')

        with self.assertRaises(IllegalArgumentError):
            front_fork.edit(name=True)

        # teardown
        front_fork.edit(name=front_fork_old_name)

    def test_edit_part_instance_description(self):
        front_fork = self.project.part('Front Fork')
        front_fork_old_description = str(front_fork._json_data.get('description'))
        front_fork.edit(description='A normal Front Fork')

        front_fork_updated = self.project.part('Front Fork')
        self.assertEqual(front_fork.id, front_fork_updated.id)

        with self.assertRaises(IllegalArgumentError):
            front_fork.edit(description=42)

        # teardown
        front_fork.edit(description=front_fork_old_description)

    def test_edit_part_model_name(self):
        front_fork = self.project.model('Front Fork')
        front_fork_oldname = str(front_fork.name)
        front_fork.edit(name='Front Fork - updated')

        front_fork_updated = self.project.model('Front Fork - updated')
        self.assertEqual(front_fork.id, front_fork_updated.id)
        self.assertEqual(front_fork.name, front_fork_updated.name)

        # teardown
        front_fork.edit(name=front_fork_oldname)

    # test added due to #847 - providing no inputs overwrites values
    def test_edit_part_clear_values(self):
        # setup
        front_fork = self.project.part('Front Fork')
        front_fork_name = str(front_fork.name)
        front_fork_description = str(front_fork.description)

        front_fork.edit(
            name=None,
            description=None
        )

        # testing
        self.assertEqual(front_fork.name, front_fork_name)
        self.assertEqual(front_fork.description, str())

        # tearDown
        front_fork.edit(description=front_fork_description)

    def test_create_model(self):
        bike = self.project.model('Bike')
        pedal = self.project.create_model(bike, 'Pedal', multiplicity=Multiplicity.ONE)
        self._part = pedal
        pedal_model = self.project.model('Pedal')

        self.assertTrue(pedal.id, pedal_model.id)
        self.assertTrue(pedal._json_data['multiplicity'], 'ONE')

    def test_add_proxy_to(self):
        catalog_container = self.project.model(name__startswith='Catalog')
        bearing_catalog_model = catalog_container.add_model('Bearing', multiplicity=Multiplicity.ZERO_MANY)
        wheel_model = self.project.model('Wheel')

        bearing_proxy_model = bearing_catalog_model.add_proxy_to(wheel_model, 'Bearing', Multiplicity.ZERO_ONE)

        self.assertTrue(bearing_proxy_model.category, Category.MODEL)
        self.assertTrue(bearing_proxy_model.parent(), wheel_model)

        # teardown
        all_bearing_proxies = self.project.parts(name='Bearing', category=Category.MODEL, parent=wheel_model.id)
        self.assertGreaterEqual(len(all_bearing_proxies), 1)
        for bearing_proxy in all_bearing_proxies:
            bearing_proxy.delete()

        all_bearing_catalog_models = self.project.parts(name='Bearing', category=Category.MODEL)
        self.assertGreaterEqual(len(all_bearing_catalog_models), 1)
        for bearing_catalog_model in all_bearing_catalog_models:
            bearing_catalog_model.delete()

        all_bearing_models = self.project.parts(name='Bearing', category=Category.MODEL)
        self.assertEqual(len(all_bearing_models), 0)

    # new in 1.6
    def test_retrieve_model(self):
        front_fork = self.project.part('Front Fork')
        front_fork_model = self.project.model('Front Fork')

        front_fork_retrieved_model = front_fork.model()

        # Added to improve coverage. Assert whether NotFoundError is raised when model() method is applied to
        # a part that has no model
        with self.assertRaises(NotFoundError):
            front_fork_model.model()

        self.assertEqual(front_fork_model.id, front_fork_retrieved_model.id)

    def test_count_instances(self):
        front_fork_model = self.project.model("Wheel")
        nr = front_fork_model.count_instances()

        self.assertIsInstance(nr, int)
        self.assertEqual(2, nr)

        bike_part = self.project.part("Bike")
        with self.assertRaises(IllegalArgumentError):
            bike_part.count_instances()

    def test_count_children(self):
        bike_instance = self.project.part("Bike")
        bike_model = self.project.model("Bike")

        nr = bike_instance.count_children()

        self.assertIsInstance(nr, int)
        self.assertEqual(7, nr)

        nr = bike_model.count_children()

        self.assertEqual(5, nr)

    def test_retrieve_catalog_model_of_proxy(self):
        catalog_container = self.project.model(name__startswith='Catalog')
        bearing_catalog_model = catalog_container.add_model('Bearing', multiplicity=Multiplicity.ZERO_MANY)
        wheel_model = self.project.model('Wheel')

        # add proxy model to product Bike > Wheel based on catalog model 'Bearing'
        bearing_proxy_model = bearing_catalog_model.add_proxy_to(wheel_model, 'Bearing', Multiplicity.ZERO_ONE)

        self.assertTrue(bearing_proxy_model.category, Category.MODEL)
        self.assertTrue(bearing_proxy_model.parent(), wheel_model)

        # the call to test here
        re_retrieved_bearing_catalog_model = self.project.model('Bearing', classification=Classification.CATALOG)
        self.assertEqual(bearing_catalog_model, re_retrieved_bearing_catalog_model)

        # teardown
        all_bearing_proxies = self.project.parts(name='Bearing', category=Category.MODEL, parent=wheel_model.id)
        self.assertGreaterEqual(len(all_bearing_proxies), 1)
        for bearing_proxy in all_bearing_proxies:
            bearing_proxy.delete()

        all_bearing_catalog_models = self.project.parts(name='Bearing', category=Category.MODEL)
        self.assertGreaterEqual(len(all_bearing_catalog_models), 1)
        for bearing_catalog_model in all_bearing_catalog_models:
            bearing_catalog_model.delete()

        all_bearing_models = self.project.parts(name='Bearing', category=Category.MODEL)
        self.assertEqual(len(all_bearing_models), 0)

    def test_retrieve_non_existent_proxies_of_a_catalog_model_raises_error(self):
        # Added to improve coverage. Assert whether NotFoundError is raised when proxy_model() method is applied to
        # a part that is not a proxy
        catalog_model = self.project.model('Model', classification=Classification.CATALOG)
        with self.assertRaises(NotFoundError):
            catalog_model.proxy_model()

    def test_retrieve_proxy_of_instance(self):
        instance = self.project.part('Rear Wheel')

        with self.assertRaises(IllegalArgumentError):
            instance.proxy_model()

    # new in 1.8+
    def test_retrieve_part_multiplicity(self):
        bike_model = self.project.model('Bike')
        self.assertEqual(bike_model.multiplicity, Multiplicity.ONE_MANY, bike_model.multiplicity)

    # new in 1.9
    def test_retrieve_part_properties_in_a_dict(self):
        # Retrieve the bike part
        bike = self.project.part('Bike')

        # Call the function to be tested
        bike_properties = bike.as_dict()

        # Check whether bike_properties contains all the property names in bike
        for prop in bike.properties:
            self.assertTrue(prop.name in bike_properties)

    # new in 1.12
    def test_retrieve_children_of_part_with_additional_arguments(self):
        bike = self.project.part('Bike')  # type: Part
        self.assertIsInstance(bike, Part)
        children_of_bike = bike.children(name__icontains='Wheel')
        self.assertIsInstance(children_of_bike, PartSet)
        self.assertTrue(len(children_of_bike) >= 1)  # eg. Wheels ...

    def test_retrieve_siblings_of_part_with_additional_arguments(self):
        """Test if the siblings of a part is the same as the children of the parent of the part"""
        frame = self.project.part('Frame')  # type: Part
        siblings_of_frame = frame.siblings(name__icontains='Wheel')
        self.assertIsInstance(siblings_of_frame, PartSet)
        self.assertTrue(len(siblings_of_frame) >= 1)  # eg. Wheels ...

    # new in 2.3
    def test_clone_model(self):
        # setUp
        model_name = 'Seat'
        seat = self.project.model(model_name)
        self._part = seat.clone()

        # testing
        clone_seat_model = self.project.model('CLONE - {}'.format(model_name))
        self.assertTrue(clone_seat_model)

    def test_clone_instance(self):
        instance_name = 'Front Wheel'
        wheel = self.project.part(instance_name)
        self._part = wheel.clone()

        # testing
        clone_spoke_instance = self.project.part('CLONE - {}'.format(instance_name))
        self.assertTrue(clone_spoke_instance)

    def test_clone_instance_with_multiplicity_violation(self):
        instance_name = 'Seat'
        seat = self.project.part(instance_name)

        seat_model = seat.model()

        # testing
        self.assertEqual(Multiplicity.ONE, seat_model.multiplicity)
        with self.assertRaises(APIError):
            self._part = seat.clone()


@skipIf(not TEST_FLAG_IS_PIM3, reason="This tests is designed for PIM version 3, expected to fail on older PIM3")
class TestPIM2SpecificPartTests(TestBetamax):
    """Pim3 capable tests."""

    def test_retrieve_part_without_parent_id(self):
        # only the root does not have a parent_id
        product_root_node = self.project.part(name='Product', classification=Classification.PRODUCT)
        self.assertTrue(hasattr(product_root_node, 'parent_id'))
        self.assertIsNotNone(product_root_node.parent_id)
        self.assertIsNone(product_root_node.parent().parent_id)

    def test_retrieve_parent_of_part_without_parent_id(self):
        # only the root does not have a parent_id
        product_root_node = self.project.part(name='Product', classification=Classification.PRODUCT)
        root_node = product_root_node.parent()
        self.assertEqual(root_node.name, "Root")
        self.assertIsNone(root_node.parent())

    def test_retrieve_siblings_of_part_without_parent_id(self):
        product_root_node = self.project.part(name='Product', classification=Classification.PRODUCT)
        siblings_of_product_root_node = product_root_node.siblings()
        self.assertIsInstance(siblings_of_product_root_node, PartSet)
        self.assertEqual(len(siblings_of_product_root_node), 2)

        siblings_of_root_node = product_root_node.parent().siblings()
        self.assertIsInstance(siblings_of_root_node, PartSet)
        self.assertEqual(len(siblings_of_root_node), 0)


@skipIf(not TEST_FLAG_IS_PIM3, reason="This tests is designed for PIM version 3, expected to fail on older PIM3")
@pytest.mark.skipif("os.getenv('TRAVIS', False) or os.getenv('GITHUB_ACTIONS', False)",
                    reason="Skipping tests when using Travis or Github Actions, as not Auth can be provided")
class TestBulkPartsCreation(TestBetamax):
    def setUp(self):
        super().setUp()

        self.product_model = self.project.model(name="Product")
        self.part_model = self.project.create_model(name="Part", parent=self.product_model)
        self.text_prop = self.part_model.add_property(name="Text Property", property_type=PropertyType.TEXT_VALUE, unit="mm")
        self.char_prop = self.part_model.add_property(name="Char Property", property_type=PropertyType.CHAR_VALUE)
        self.int_prop = self.part_model.add_property(name="Int Property", property_type=PropertyType.INT_VALUE)
        self.float_prop = self.part_model.add_property(name="Float Property", property_type=PropertyType.FLOAT_VALUE)
        self.bool_prop = self.part_model.add_property(
            name="Boolean Property", property_type=PropertyType.BOOLEAN_VALUE)
        self.date_prop = self.part_model.add_property(name="Date Property", property_type=PropertyType.DATE_VALUE)
        self.datetime_prop = self.part_model.add_property(
            name="Datetime Property", property_type=PropertyType.DATETIME_VALUE)
        self.attachment_prop = self.part_model.add_property(
            name="Attach Property", property_type=PropertyType.ATTACHMENT_VALUE)
        self.link_prop = self.part_model.add_property(name="Link Property", property_type=PropertyType.LINK_VALUE)
        self.ss_prop = self.part_model.add_property(
            name="SS Property", property_type=PropertyType.SINGLE_SELECT_VALUE)
        self.ss_prop.options = ["1", "2", "3", "4", "5"]
        self.ms_prop = self.part_model.add_property(
            name="MS Property", property_type=PropertyType.MULTI_SELECT_VALUE,
            options={"value_choices": ["1", "2", "3", "4", "5"]})
        self.part_ref_prop = self.part_model.add_property(
            name="Part ref Property", property_type=PropertyType.REFERENCES_VALUE,
            default_value=self.project.model(name="Wheel").id)
        self.geo_info_prop = self.part_model.add_property(
            name="Geo Property", property_type=PropertyType.GEOJSON_VALUE)
        self.weather_prop = self.part_model.add_property(
            name="Weather Property", property_type=PropertyType.WEATHER_VALUE)
        self.act_ref_prop = self.part_model.add_property(
            name="Act Property", property_type=PropertyType.ACTIVITY_REFERENCES_VALUE)

        self.product_part = self.project.part(name="Product")
        self.part_dict = {
            "name": "Dummy name",
            "parent_id": self.product_part.id,
            "description": "",
            "model_id": self.part_model.id,
            "properties": [
                {
                    "name": self.text_prop.name,
                    "value": "Dummy value",
                    "model_id": self.text_prop.id,
                }]
        }

    def tearDown(self):
        self.part_model.delete()

    """Bulk parts creation"""
    def test_bulk_create_parts(self):
        new_parts = list()
        for idx in range(1, 5):
            part_dict = {
                "name": idx,
                "parent_id": self.product_part.id,
                "description": "Description part {}".format(idx),
                "model_id": self.part_model.id,
                "properties": [
                    {
                        "name": self.text_prop.name,
                        "value": "{}".format(idx),
                        "model_id": self.text_prop.id,
                    },
                    {
                        "name": self.char_prop.name,
                        "value": "{}".format(idx),
                        "model_id": self.char_prop.id
                    },
                    {
                        "name": self.int_prop.name,
                        "value": idx,
                        "model_id": self.int_prop.id
                    },
                    {
                        "name": self.float_prop.name,
                        "value": float("{}.{}".format(idx, idx)),
                        "model_id": self.float_prop.id
                    },
                    {
                        "name": self.bool_prop.name,
                        "value": True if idx % 2 == 0 else False,
                        "model_id": self.bool_prop.id
                    },
                    {
                        "name": self.date_prop.name,
                        "value": datetime.date(2020, idx, idx).isoformat(),
                        "model_id": self.date_prop.id
                    },
                    {
                        "name": self.datetime_prop.name,
                        "value": datetime.datetime(2020, idx, idx, 15, 00, 00).isoformat("T"),
                        "model_id": self.datetime_prop.id
                    },
                    {
                        "name": self.link_prop.name,
                        "value": "http://{}.com".format(idx),
                        "model_id": self.link_prop.id
                    },
                    {
                        "name": self.ss_prop.name,
                        "value": "{}".format(idx),
                        "model_id": self.ss_prop.id
                    },
                    {
                        "name": self.ms_prop.name,
                        "value": ["{}".format(idx)],
                        "model_id": self.ms_prop.id
                    },
                    {
                        "name": self.part_ref_prop.name,
                        "value": [self.project.part(name="Front Wheel").id],
                        "model_id": self.part_ref_prop.id
                    },
                    {
                        "name": self.act_ref_prop.name,
                        "value": [self.project.activity(name="Specify wheel diameter").id],
                        "model_id": self.act_ref_prop.id
                    },
                ]
            }
            new_parts.append(part_dict)
        parts_created = self.client._create_parts_bulk(
            parts=new_parts
        )

        # testing
        self.assertEqual(len(parts_created), 4)
        for part_created in parts_created:
            idx = int(part_created.name)
            self.assertEqual(part_created.description, "Description part {}".format(idx))
            self.assertEqual(part_created.category, Category.INSTANCE)
            self.assertEqual(len(part_created.properties), 15)

            self.assertEqual(part_created.property(name=self.text_prop.name).value, str(idx))
            self.assertEqual(part_created.property(name=self.char_prop.name).value, str(idx))
            self.assertEqual(part_created.property(name=self.int_prop.name).value, idx)
            self.assertEqual(part_created.property(name=self.float_prop.name).value, float("{}.{}".format(idx, idx)))
            self.assertEqual(part_created.property(name=self.bool_prop.name).value, True if idx % 2 == 0 else False)
            self.assertEqual(part_created.property(name=self.date_prop.name).value,
                             datetime.date(2020, idx, idx).isoformat())
            self.assertEqual(part_created.property(name=self.datetime_prop.name).value.split("+")[0],
                             datetime.datetime(2020, idx, idx, 15, 00, 00).isoformat("T"))
            self.assertEqual(part_created.property(name=self.link_prop.name).value, "http://{}.com".format(idx))
            self.assertEqual(part_created.property(name=self.ss_prop.name).value, "{}".format(idx))
            self.assertEqual(part_created.property(name=self.ms_prop.name).value, ["{}".format(idx)])
            self.assertEqual(part_created.property(name=self.part_ref_prop.name).value[0].name, "Front Wheel")
            self.assertEqual(part_created.property(name=self.act_ref_prop.name).value[0].name, "Specify wheel diameter")
            self.assertEqual(part_created.property(name=self.weather_prop.name).value, dict())
            self.assertEqual(part_created.property(name=self.geo_info_prop.name).value, dict())

    def test_bulk_create_parts_without_name(self):
        self.part_dict.pop("name")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )

    def test_bulk_create_parts_without_parent_id(self):
        self.part_dict.pop("parent_id")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )

    def test_bulk_create_parts_without_model_id(self):
        self.part_dict.pop("model_id")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )

    def test_bulk_create_parts_without_properties(self):
        self.part_dict.pop("properties")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )

    def test_bulk_create_parts_without_property_name(self):
        self.part_dict["properties"][0].pop("name")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )

    def test_bulk_create_parts_without_property_model_id(self):
        self.part_dict["properties"][0].pop("model_id")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )

    def test_bulk_create_parts_without_property_value(self):
        self.part_dict["properties"][0].pop("value")
        with self.assertRaises(IllegalArgumentError):
            self.client._create_parts_bulk(
                parts=[self.part_dict]
            )


@skipIf(not TEST_FLAG_IS_PIM3, reason="This tests is designed for PIM version 3, expected to fail on older PIM3")
@pytest.mark.skipif("os.getenv('TRAVIS', False) or os.getenv('GITHUB_ACTIONS', False)",
                    reason="Skipping tests when using Travis or Github Actions, as not Auth can be provided")
class TestBulkPartsDeletion(TestBetamax):
    def setUp(self):
        super().setUp()
        self.parts = list()
        self.wheel_model = self.project.model(name="Wheel")
        self.bike_instance = self.project.part(name="Bike")

        self.diameter_prop = self.wheel_model.property(name="Diameter")
        self.spokes_prop = self.wheel_model.property(name="Spokes")
        self.rim_material_prop = self.wheel_model.property(name="Rim Material")
        self.tire_thickness_prop = self.wheel_model.property(name="Tire Thickness")

        for idx in range(1, 5):
            part_dict = {
                "name": "Wheel {}".format(idx),
                "parent_id": self.bike_instance.id,
                "description": "",
                "model_id": self.wheel_model.id,
                "properties": [
                    {
                        "name": self.diameter_prop.name,
                        "value": float("{}.{}".format(idx, idx)),
                        "model_id": self.diameter_prop.id,
                    },
                    {
                        "name": self.spokes_prop.name,
                        "value": "{}".format(idx),
                        "model_id": self.spokes_prop.id
                    },
                    {
                        "name": self.rim_material_prop.name,
                        "value": "Material {}".format(idx),
                        "model_id": self.rim_material_prop.id
                    },
                    {
                        "name": self.tire_thickness_prop.name,
                        "value": float("{}.{}".format(idx, idx)),
                        "model_id": self.tire_thickness_prop.id
                    }
                ]
            }
            self.parts.append(part_dict)
        self.parts_created = list(self.client._create_parts_bulk(
            parts=self.parts
        ))

    def tearDown(self):
        for part in self.parts_created:
            # In case the parts have not been deleted and a clean up is required
            try:
                part.delete()
            except APIError:
                pass

    def test_bulk_delete_parts(self):
        input_parts_and_uuids = [self.parts_created[0],
                                 self.parts_created[1],
                                 self.parts_created[2].id,
                                 self.parts_created[3].id]
        self.client._delete_parts_bulk(parts=input_parts_and_uuids)
        for idx in range(1, 5):
            with self.subTest(idx=idx):
                with self.assertRaises(NotFoundError):
                    self.project.part(name="Wheel {}".format(idx))

    def test_bulk_delete_parts_with_wrong_input(self):
        wrong_input = [self.project.activity(name="Specify wheel diameter")]
        with self.assertRaises(IllegalArgumentError):
            self.client._delete_parts_bulk(parts=wrong_input)
