from jsonschema import validate, ValidationError

from pykechain.enums import PropertyVTypes, PropertyType
from pykechain.exceptions import IllegalArgumentError
from pykechain.models import Property2, AttachmentProperty2
from pykechain.models.validators import PropertyValidator, ValidatorEffect, VisualEffect, ValidVisualEffect, \
    InvalidVisualEffect, NumericRangeValidator, BooleanFieldValidator
from pykechain.models.validators.validator_schemas import options_json_schema, validator_jsonschema_stub, \
    effects_jsonschema_stub
from pykechain.models.validators.validators import RegexStringValidator, RequiredFieldValidator, EvenNumberValidator, \
    OddNumberValidator, SingleReferenceValidator, EmailValidator, AlwaysAllowValidator, FileSizeValidator, \
    FileExtensionValidator
from tests.classes import SixTestCase, TestBetamax


class TestValidatorJSON(SixTestCase):
    def test_valid_numeric_range_validator_json(self):
        options = dict(
            validators=[dict(
                vtype='numericRangeValidator',
                config=dict(
                    minvalue=2,
                    maxvalue=10,
                    stepsize=2,
                    on_valid=[dict(effect="CssEffect", config=dict(applyCss="valid"))],
                    on_invalid=[dict(effect="CssEffect", config=dict(applyCss="invalid")),
                                dict(effect="ErrorText",
                                     config=dict(text="Range should be between 2 and 10 with step 2."))]
                )
            )]
        )

        validate(options_json_schema, options)

    def test_valid_requiredfield_validator_json(self):
        options = {'validators': [
            {
                'vtype': 'requiredFieldValidator',
                'config': {
                    'effects_when_valid': [],
                    'effects_when_invalid': [{'effect': "ErrorText", 'config': {'Text': "This field is required."}}]
                }
            }, {
                'vtype': 'numericRangeValidator',
                'config': {'minValue': 0}
            }
        ]}

        validate(options_json_schema, options)

    def test_valid_booleanfield_validator_json(self):
        options = {'validators': [{
            'vtype': 'booleanFieldValidator',
            'config': {
                'is_valid': False,
                'is_invalid': [True, None],
                'effects_when_valid': [],
                'effects_when_invalid': [{'effect': 'ErrorText', 'text': 'Value of the field should be False'}]}
        }]}

        validate(options_json_schema, options)

    def test_validator_invalid_vtype(self):
        validator_json = {
            'vtype': 'invalid',
            'config': {}
        }
        with self.assertRaisesRegex(ValidationError, "'invalid' is not one of"):
            validate(validator_json, validator_jsonschema_stub)

    def test_validator_missing_vtype(self):
        validator_json = {
            'config': {}
        }
        with self.assertRaisesRegex(ValidationError, "'vtype' is a required property"):
            validate(validator_json, validator_jsonschema_stub)

    def test_validator_config_not_a_dict(self):
        v = {
            'vtype': PropertyVTypes.NUMERICRANGE,
            'config': []
        }
        with self.assertRaisesRegex(ValidationError, "is not of type 'object'"):
            validate(v, validator_jsonschema_stub)

    def test_validator_on_valid_not_a_list(self):
        v = {
            'vtype': PropertyVTypes.NUMERICRANGE,
            'config': {
                'on_valid': {}
            }
        }
        with self.assertRaisesRegex(ValidationError, "is not of type 'array'"):
            validate(v, validator_jsonschema_stub)

    def test_validator_on_invalid_not_a_list(self):
        v = {
            'vtype': PropertyVTypes.NUMERICRANGE,
            'config': {
                'on_invalid': {}
            }
        }
        with self.assertRaisesRegex(ValidationError, "is not of type 'array'"):
            validate(v, validator_jsonschema_stub)

    def test_validator_on_valid_list_with_obj(self):
        v = {
            'vtype': PropertyVTypes.NUMERICRANGE,
            'config': {
                'on_valid': [{'effect': '', 'config': {}}]
            }
        }
        validate(v, validator_jsonschema_stub)

    def test_validator_on_invalid_list_with_obj(self):
        v = {
            'vtype': PropertyVTypes.NUMERICRANGE,
            'config': {
                'on_invalid': [{'effect': '', 'config': {}}]
            }
        }
        validate(v, validator_jsonschema_stub)

    def test_validatoreffect_requires_effect_property(self):
        v = {}
        with self.assertRaisesRegex(ValidationError, "'effect' is a required property"):
            validate(v, effects_jsonschema_stub)

    def test_validatoreffect_requires_config_property(self):
        v = {'effect': ''}
        with self.assertRaisesRegex(ValidationError, "'config' is a required property"):
            validate(v, effects_jsonschema_stub)

    def test_validatoreffect_not_allows_additional_properties(self):
        v = {
            'effect': '',
            'config': {},
            'additional_option': None

        }
        with self.assertRaisesRegex(ValidationError,
                                    r"Additional properties are not allowed \('additional_option' was unexpected\)"):
            validate(v, effects_jsonschema_stub)


class TestPropertyValidatorClass(SixTestCase):
    def test_propertyvalidator_produces_valid_json(self):
        pv = PropertyValidator()
        pv.validate_json()


class TestValidatorEffects(SixTestCase):
    def test_validator_effect_produces_valid_json(self):
        ve = ValidatorEffect()
        ve.validate_json()

    def test_visual_effect_produces_valid_json(self):
        ve = VisualEffect()
        self.assertTrue('applyCss' in ve.as_json().get('config'))
        ve.validate_json()

    def test_valid_visualeffect_produces_valid_json(self):
        ve = ValidVisualEffect()
        self.assertTrue('applyCss' in ve.as_json().get('config'))
        ve.validate_json()

    def test_invalid_visualeffect_productes_valid_json(self):
        ve = InvalidVisualEffect()
        self.assertTrue('applyCss' in ve.as_json().get('config'))
        ve.validate_json()


class TestValidatorParsing(SixTestCase):

    def test_valid_numeric_range_validator_json(self):
        validator_json = dict(
            vtype='numericRangeValidator',
            config=dict(
                minvalue=2,
                maxvalue=10,
                stepsize=2,
                enforce_stepsize=False,
                on_valid=[dict(effect="visualEffect", config=dict(applyCss="valid"))],
                on_invalid=[dict(effect="visualEffect", config=dict(applyCss="invalid")),
                            dict(effect="errorTextEffect",
                                 config=dict(text="Range should be between 2 and 10 with step 2."))]
            )
        )

        validator = PropertyValidator.parse(validator_json)

        self.assertIsInstance(validator, NumericRangeValidator)
        self.assertTrue(validator.validate_json)
        pass


class TestValidatorDumping(SixTestCase):

    def test_valid_numeric_range_validator_dumped(self):
        validator_json = dict(
            vtype='numericRangeValidator',
            config=dict(
                minvalue=2,
                maxvalue=10,
                stepsize=2,
                enforce_stepsize=False,
                on_valid=[dict(effect="visualEffect", config=dict(applyCss="valid"))],
                on_invalid=[dict(effect="visualEffect", config=dict(applyCss="invalid")),
                            dict(effect="errorTextEffect",
                                 config=dict(text="Range should be between 2 and 10 with step 2."))]
            )
        )

        validator = PropertyValidator.parse(validator_json)
        self.assertIsInstance(validator, NumericRangeValidator)

        dumped_json = validator.as_json()
        self.assertIsNone(validate(dumped_json, validator_jsonschema_stub))
        self.assertIsNone(validator.validate_json())


class TestNumericRangeValidator(SixTestCase):
    def test_numeric_range_without_settings_validated_json(self):
        validator = NumericRangeValidator()

        as_json = validator.as_json()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(as_json, dict)
        self.assertDictEqual(as_json, {'config': {}, 'vtype': 'numericRangeValidator'})

    def test_numeric_range_validates_with_lower_bound(self):
        validator = NumericRangeValidator(minvalue=0)
        self.assertTrue(validator.is_valid(1))
        validator = NumericRangeValidator(minvalue=5)
        self.assertTrue(validator.is_valid(6))

    def test_numeric_range_validates_with_max_bound(self):
        validator = NumericRangeValidator(maxvalue=0)
        self.assertTrue(validator.is_valid(-3))
        validator = NumericRangeValidator(maxvalue=10)
        self.assertTrue(validator.is_valid(1))

    def test_numeric_range_validates_with_min_max_bound(self):
        validator = NumericRangeValidator(maxvalue=10, minvalue=0)
        self.assertTrue(validator.is_valid(1))

    def test_numeric_range_invalidates_with_lower_bound(self):
        validator = NumericRangeValidator(minvalue=0)
        self.assertFalse(validator.is_valid(-1))
        validator = NumericRangeValidator(minvalue=5)
        self.assertFalse(validator.is_valid(2))

    def test_numeric_range_invalidates_with_max_bound(self):
        validator = NumericRangeValidator(maxvalue=0)
        self.assertFalse(validator.is_valid(1))
        validator = NumericRangeValidator(maxvalue=5)
        self.assertFalse(validator.is_valid(6))

    def test_numeric_range_invalidates_with_min_max_bound(self):
        validator = NumericRangeValidator(maxvalue=10, minvalue=0)
        self.assertFalse(validator.is_valid(-1))

    def test_numeric_range_validates_with_min_max_bound_float(self):
        validator = NumericRangeValidator(maxvalue=10., minvalue=0.)
        self.assertTrue(validator.is_valid(1.5))

    def test_numeric_range_validates_with_stepsize_float(self):
        validator = NumericRangeValidator(stepsize=0.2, enforce_stepsize=True)
        self.assertTrue(validator.is_valid(1.2))
        self.assertTrue(validator.is_invalid(1.3))

    def test_numeric_range_validates_with_stepsize_int_with_minvalue(self):
        validator = NumericRangeValidator(minvalue=11, stepsize=2, enforce_stepsize=True)
        self.assertTrue(validator.is_valid(13))
        self.assertTrue(validator.is_invalid(16))

    def test_numeric_range_raises_exception_when_min_is_greater_than_max(self):
        with self.assertRaisesRegex(Exception, 'should be smaller than the maxvalue'):
            NumericRangeValidator(minvalue=11, maxvalue=-11)

    def test_numeric_range_raises_exception_when_enforce_stepsize_without_stepsize(self):
        with self.assertRaisesRegex(Exception, 'The stepsize should be provided when enforcing stepsize'):
            NumericRangeValidator(stepsize=None, enforce_stepsize=True)

    def test_numeric_range_does_not_respect_max_issue_435(self):
        """With reference to issue #435

        In KE-chain I've added a property with numeric range validators min = unbound and max = 1000.
        A value of 3333 simply returns as valid in pykechain:
        _validation_reasons =>'Value \'3333\' is between -inf and inf'
        """
        validator = NumericRangeValidator(maxvalue=1000)
        self.assertFalse(validator.is_valid(3333))
        self.assertRegex(validator.get_reason(), "Value '3333' should be between -inf and 1000")


class TestBooleanFieldValidator(SixTestCase):
    def test_boolean_validator_without_settings(self):
        validator = BooleanFieldValidator()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(validator.as_json(), dict)
        self.assertDictEqual(validator.as_json(), {'config': {}, 'vtype': 'booleanFieldValidator'})


class TestRequiredFieldValidator(SixTestCase):
    def test_requiredfield_validator_without_settings(self):
        validator = RequiredFieldValidator()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(validator.as_json(), dict)
        self.assertDictEqual(validator.as_json(), {'config': {}, 'vtype': 'requiredFieldValidator'})

    def test_requiredfield_validator_is_false_on_nonevalue(self):
        validator = RequiredFieldValidator()
        self.assertFalse(validator.is_valid(None))
        self.assertFalse(validator.is_valid(''))
        self.assertFalse(validator.is_valid([]))
        self.assertFalse(validator.is_valid(list()))
        self.assertFalse(validator.is_valid(tuple()))
        self.assertFalse(validator.is_valid(set()))

        # the value 'False' is not a None value so that is True (placed in other test)
        # self.assertFalse(validator.is_valid(False))

    def test_requiredfield_validator_is_true_on_value(self):
        validator = RequiredFieldValidator()
        self.assertTrue(validator.is_valid(1))
        self.assertTrue(validator.is_valid(0))
        self.assertTrue(validator.is_valid(0.0))
        self.assertTrue(validator.is_valid(float('inf')))
        self.assertTrue(validator.is_valid('string'))
        self.assertTrue(validator.is_valid({'key': 'val'}))
        self.assertTrue(validator.is_valid([1, 2, 3]))
        self.assertTrue(validator.is_valid((1,)))
        self.assertTrue(validator.is_valid({1, 2, 3}))
        self.assertTrue(validator.is_valid(False))


class TestRegexValidator(SixTestCase):
    def test_regex_validator_without_settings(self):
        validator = RegexStringValidator()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(validator.as_json(), dict)
        self.assertDictEqual(validator.as_json(), {'config': {}, 'vtype': 'regexStringValidator'})

    def test_regex_validator_with_pattern_match(self):
        validator = RegexStringValidator(pattern=r'.*')

        self.assertTrue(validator('mr cactus is tevree'))

    def test_regex_validator_without_pattern_match(self):
        validator = RegexStringValidator()

        # per default the regex string matches everything
        self.assertEqual(validator.pattern, '.+')
        self.assertTrue(validator('mr cactus is tevree'))
        self.assertFalse(validator(''))

    def test_regex_validator_fails_on_none_value(self):
        validator = RegexStringValidator(pattern=r'.*')

        self.assertFalse(validator.is_valid(None))

    def test_regex_validator_complex_email_regex(self):
        validator = EmailValidator()

        self.assertTrue(validator.is_valid('support@ke-works.com'))
        self.assertFalse(validator.is_valid('___'))
        self.assertIsNone(validator.is_valid(None))
        self.assertFalse(validator.is_valid('user@domain'))


class TestOddEvenNumberValidator(SixTestCase):
    def test_even_number_validator_without_settings(self):
        validator = EvenNumberValidator()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(validator.as_json(), dict)
        self.assertDictEqual(validator.as_json(), {'config': {}, 'vtype': 'evenNumberValidator'})

    def test_odd_number_validator_without_settings(self):
        validator = OddNumberValidator()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(validator.as_json(), dict)
        self.assertDictEqual(validator.as_json(), {'config': {}, 'vtype': 'oddNumberValidator'})

    def test_even_number_validator_is_valid(self):
        validator = EvenNumberValidator()
        self.assertTrue(validator.is_valid(2))

    def test_odd_number_validator_is_valid(self):
        validator = OddNumberValidator()
        self.assertTrue(validator.is_valid(3))

    def test_even_number_validator_is_invalid(self):
        validator = EvenNumberValidator()
        self.assertFalse(validator.is_valid(3))

    def test_odd_number_validator_is_invalid(self):
        validator = OddNumberValidator()
        self.assertFalse(validator.is_valid(4))

    def test_even_number_validator_is_none(self):
        validator = EvenNumberValidator()
        self.assertIsNone(validator.is_valid(None))

    def test_odd_number_validator_is_none(self):
        validator = OddNumberValidator()
        self.assertIsNone(validator.is_valid(None))

    def test_even_number_validator_float_valid(self):
        validator = EvenNumberValidator()
        self.assertTrue(validator.is_valid(4.5))
        self.assertTrue(validator.is_valid(4.0))
        self.assertTrue(validator.is_valid(3.9999999999999999))  # rounding accuracy

    def test_odd_number_validator_float_valid(self):
        validator = OddNumberValidator()
        self.assertTrue(validator.is_valid(3.0))
        self.assertTrue(validator.is_valid(4.9999999999999999))  # rounding accuracy

    def test_even_number_validator_float_invalid(self):
        validator = EvenNumberValidator()
        self.assertFalse(validator.is_valid(3.99))
        self.assertFalse(validator.is_valid(5.5))

    def test_odd_number_validator_float_invalid(self):
        validator = OddNumberValidator()
        self.assertFalse(validator.is_valid(4.99))
        self.assertFalse(validator.is_valid(6.0))

    def test_even_number_validator_invalid_input(self):
        validator = EvenNumberValidator()
        self.assertFalse(validator.is_valid(dict()))
        self.assertFalse(validator.is_valid(list))
        self.assertFalse(validator.is_valid(set()))
        self.assertFalse(validator.is_valid(tuple()))
        self.assertFalse(validator.is_valid("3"))

    def test_odd_number_validator_invalid_input(self):
        validator = OddNumberValidator()
        self.assertFalse(validator.is_valid(dict()))
        self.assertFalse(validator.is_valid(list))
        self.assertFalse(validator.is_valid(set()))
        self.assertFalse(validator.is_valid(tuple()))
        self.assertFalse(validator.is_valid("3"))


class TestSingleReferenceValidator(SixTestCase):
    def test_singlereference_validator_without_settings(self):
        validator = SingleReferenceValidator()
        self.assertIsNone(validator.validate_json())
        self.assertIsInstance(validator.as_json(), dict)
        self.assertDictEqual(validator.as_json(), {'config': {}, 'vtype': 'singleReferenceValidator'})

    def test_singlereference_validator_is_valid(self):
        validator = SingleReferenceValidator()
        self.assertIsNone(validator.is_valid(None))
        self.assertTrue(validator.is_valid(list()))
        self.assertTrue(validator.is_valid(set()))
        self.assertTrue(validator.is_valid(tuple()))
        self.assertTrue(validator.is_valid(('first selection',)))

    def test_singlereference_validator_is_invalid(self):
        validator = SingleReferenceValidator()
        self.assertFalse(validator.is_valid(["first", "second"]))
        self.assertFalse(validator.is_valid((1, 2)))

    def test_singlerefence_validator_is_invalid_with_invalid_values(self):
        validator = SingleReferenceValidator()
        self.assertFalse(validator.is_valid("a string"))
        self.assertFalse(validator.is_valid(1.0))
        self.assertFalse(validator.is_valid(dict()))


class TestAlwaysAllowValidator(SixTestCase):
    def test_always_allow_validator_without_settings(self):
        validator = AlwaysAllowValidator()
        self.assertTrue(validator.is_valid('some text'))
        self.assertTrue(validator.is_valid(42))
        self.assertTrue(validator.is_valid(42.0))
        self.assertTrue(validator.is_valid(['a', 'b']))
        self.assertTrue(validator.is_valid(True))
        self.assertTrue(validator.is_valid(False))
        self.assertTrue(validator.is_valid(None))
        self.assertTrue(validator.is_valid({"a": "b"}))
        self.assertFalse(validator.is_invalid(None))
        self.assertTrue(validator.is_valid(list()))
        self.assertTrue(validator.is_valid(set()))
        self.assertTrue(validator.is_valid(tuple()))
        self.assertIsNone(validator.validate_json())


class TestFileSizeValidator(SixTestCase):
    def test_validator_valid_json_with_settings(self):
        validator = FileSizeValidator(max_size=100)
        self.assertIsNone(validator.validate_json())

    def test_validator_valid_json_without_settings(self):
        validator = FileSizeValidator()
        self.assertIsNone(validator.validate_json())

    def test_validator_valid_json_with_additional_arguments(self):
        validator = FileSizeValidator(max_size=100, another_argument=False)
        # another_argument is not found in the json
        validator.validate_json()
        validator.as_json()

    # noinspection PyTypeChecker
    def test_validator_invalid_arguments(self):
        with self.assertRaisesRegex(ValueError, 'should be a number'):
            FileSizeValidator(max_size=set())
        with self.assertRaisesRegex(ValueError, 'should be a number'):
            FileSizeValidator(max_size=list())
        with self.assertRaisesRegex(ValueError, 'should be a number'):
            FileSizeValidator(max_size=dict())
        with self.assertRaisesRegex(ValueError, 'should be a number'):
            FileSizeValidator(max_size=tuple())
        with self.assertRaisesRegex(ValueError, 'should be a number'):
            FileSizeValidator(max_size=tuple())
        with self.assertRaisesRegex(ValueError, 'should be a number'):
            FileSizeValidator(max_size='100')

    def test_filesizevalidator_being_valid(self):
        validator = FileSizeValidator(max_size=100)

        self.assertTrue(validator.is_valid(100))
        self.assertTrue(validator.is_valid(100.))
        self.assertTrue(validator.is_valid(99))
        self.assertTrue(validator.is_valid(0))

        self.assertTrue(validator.is_valid('text'))
        self.assertTrue(validator.is_valid(list()))
        self.assertTrue(validator.is_valid(tuple()))
        self.assertTrue(validator.is_valid(dict()))
        self.assertTrue(validator.is_valid(set()))

    def test_filesizevalidator_being_invalid(self):
        validator = FileSizeValidator(max_size=100)

        self.assertFalse(validator.is_valid(101))
        self.assertFalse(validator.is_valid(101.))
        self.assertFalse(validator.is_valid(-1))

    def test_filesizevalidator_being_none(self):
        validator = FileSizeValidator(max_size=100)

        self.assertIsNone(validator.is_valid(None))


class TestFileExtensionValidator(SixTestCase):
    def test_validator_valid_json_with_settings(self):
        validator = FileExtensionValidator(accept=[".csv"])
        self.assertIsNone(validator.validate_json())

    def test_validator_valid_json_without_settings(self):
        validator = FileExtensionValidator()
        self.assertIsNone(validator.validate_json())

    def test_fileextensionvalidator_on_extension(self):
        validator = FileExtensionValidator(accept=[".csv"])
        self.assertTrue(validator.is_valid('file.csv'))
        self.assertFalse(validator.is_valid('file.tsv'))
        self.assertFalse(validator.is_valid('file.txt'))

    def test_fileextensionvalidator_on_extensions(self):
        validator = FileExtensionValidator(accept=[".csv", ".pdf"])
        self.assertTrue(validator.is_valid('file.csv'))
        self.assertTrue(validator.is_valid('file.pdf'))
        self.assertFalse(validator.is_valid('file.docx'))

    def test_fileextensionvalidator_on_mimetype(self):
        validator = FileExtensionValidator(accept=["application/pdf"])
        self.assertTrue(validator.is_valid('file.pdf'))
        self.assertFalse(validator.is_valid('file.docx'))

    def test_fileextensionvalidator_on_expanded_mimetypes(self):
        validator = FileExtensionValidator(accept=["video/*"])
        self.assertTrue(validator.is_valid('file.mp4'))
        self.assertTrue(validator.is_valid('file.mov'))
        self.assertTrue(validator.is_valid('file.mpeg'))
        self.assertTrue(validator.is_valid('file.flv'))
        self.assertTrue(validator.is_valid('file.wmv'))
        self.assertTrue(validator.is_valid('file.webm'))

    def test_fileextensionvalidator_on_mixed_mode(self):
        validator = FileExtensionValidator(accept=[".csv", "application/pdf"])
        self.assertTrue(validator.is_valid('file.csv'))
        self.assertTrue(validator.is_valid('file.pdf'))
        self.assertFalse(validator.is_valid('file.docx'))

    def test_fileextensionvalidator_on_excel(self):
        validator = FileExtensionValidator(accept=[".xls", ".xlsx"])
        self.assertTrue(validator.is_valid('file.xls'))
        self.assertTrue(validator.is_valid('file.xlsx'))
        self.assertFalse(validator.is_valid('file.csv'))

    def test_fileextensionvalidator_being_none(self):
        validator = FileExtensionValidator(accept=["video/*"])

        self.assertIsNone(validator.is_valid(None))


class TestPropertyWithValidator(SixTestCase):

    def test_property_without_validator(self):
        prop = Property2(json={}, client=None)
        self.assertIsNone(prop.is_valid)
        self.assertIsNone(prop.is_invalid)
        self.assertEqual(prop._validators, list())
        self.assertEqual(prop.validate(), list())

    def test_property_with_numeric_range_validator(self):
        prop_json = dict(
            value=1,
            value_options=dict(
                validators=[NumericRangeValidator(minvalue=0, maxvalue=10).as_json()]
            ))
        prop = Property2(json=prop_json, client=None)
        self.assertTrue(prop.is_valid)
        self.assertFalse(prop.is_invalid)
        self.assertTrue(prop.validate())

    def test_property_with_numeric_range_validator_value_is_none(self):
        prop_json = dict(
            value=None,
            value_options=dict(
                validators=[NumericRangeValidator(minvalue=0, maxvalue=10).as_json()]
            ))
        prop = Property2(json=prop_json, client=None)
        self.assertIsNone(prop.is_valid)
        self.assertIsNone(prop.is_invalid)
        self.assertListEqual([(None, "No reason")], prop.validate())

    def test_property_with_filesize_validator(self):
        prop_json = dict(
            value="attachments/12345678-1234-5678-1234-567812345678/some_file.txt",
            value_options=dict(
                validators=[FileSizeValidator(max_size=100).as_json()]
            ))
        prop = AttachmentProperty2(json=prop_json, client=None)
        self.assertTrue(prop.is_valid)
        self.assertFalse(prop.is_invalid)
        self.assertTrue(prop.validate())

    def test_property_without_value_with_filesize_validator(self):
        prop_json = dict(
            value=None,
            value_options=dict(
                validators=[FileSizeValidator(max_size=100).as_json()]
            ))
        prop = AttachmentProperty2(json=prop_json, client=None)
        self.assertIsNone(prop.is_valid)
        self.assertIsNone(prop.is_invalid)
        self.assertListEqual([(None, "No reason")], prop.validate())

    def test_property_with_fileextension_validator(self):
        prop_json = dict(
            value="attachments/12345678-1234-5678-1234-567812345678/some_file.txt",
            value_options=dict(
                validators=[FileExtensionValidator(accept=['.txt', 'application/pdf']).as_json()]
            ))
        prop = AttachmentProperty2(json=prop_json, client=None)
        self.assertTrue(prop.is_valid)
        self.assertFalse(prop.is_invalid)
        self.assertTrue(prop.validate())

    def test_property_without_value_with_fileextension_validator(self):
        prop_json = dict(
            value=None,
            value_options=dict(
                validators=[FileExtensionValidator(accept=['.txt', 'application/pdf']).as_json()]
            ))
        prop = AttachmentProperty2(json=prop_json, client=None)
        self.assertIsNone(prop.is_valid)
        self.assertIsNone(prop.is_invalid)
        self.assertListEqual([(None, "No reason")], prop.validate())


class TestPropertyWithValidatorFromLiveServer(TestBetamax):

    def setUp(self):
        super(TestPropertyWithValidatorFromLiveServer, self).setUp()
        self.part_model = self.project.model(name='Model')
        self.numeric_range_prop_model = self.part_model.add_property(name='numericrange_validatortest',
                                                                     property_type=PropertyType.FLOAT_VALUE)
        self.numeric_range_prop_model.validators = [NumericRangeValidator(min=0., max=42.)]
        self.part = self.project.part(name__startswith='Catalog').add(self.part_model, name='Model Instance for tests')
        self.numeric_range_prop_instance = self.part.property(name='numericrange_validatortest')

    def tearDown(self):
        self.numeric_range_prop_model.delete()
        self.part.delete()
        super(TestPropertyWithValidatorFromLiveServer, self).tearDown()

    def test_numeric_property_with_validator_parses(self):
        self.assertIsInstance(self.numeric_range_prop_instance._validators, list)
        self.assertIsInstance(self.numeric_range_prop_instance._validators[0], PropertyValidator)

    def test_numeric_property_add_requiredvalidator_on_model(self):
        # test
        validators = self.numeric_range_prop_model.validators
        validators.append(RequiredFieldValidator())
        self.numeric_range_prop_model.validators = validators

        for validator in self.numeric_range_prop_model.validators:
            self.assertIsInstance(validator, (NumericRangeValidator, RequiredFieldValidator))

    def test_numeric_property_add_requiredvalidator_on_instance(self):
        # test
        validators = self.numeric_range_prop_instance.validators
        validators.append(RequiredFieldValidator())

        with self.assertRaisesRegex(IllegalArgumentError, "To update the list of validators, it can only work "
                                                          "on `Property` of category 'MODEL'"):
            self.numeric_range_prop_instance.validators = validators
