# Tests of custom properties for Python programming.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: March 2, 2020
# URL: https://property-manager.readthedocs.io

"""Automated tests for the :mod:`property_manager` module."""

# Standard library modules.
import logging
import os
import random
import sys
import unittest

# External dependencies.
import coloredlogs
from humanfriendly.text import compact, format
from verboselogs import VerboseLogger

# Modules included in our package.
import property_manager
from property_manager import (
    CACHED_PROPERTY_NOTE,
    CUSTOM_PROPERTY_NOTE,
    DYNAMIC_PROPERTY_NOTE,
    ENVIRONMENT_PROPERTY_NOTE,
    REQUIRED_PROPERTY_NOTE,
    RESETTABLE_CACHED_PROPERTY_NOTE,
    RESETTABLE_WRITABLE_PROPERTY_NOTE,
    WRITABLE_PROPERTY_NOTE,
    PropertyManager,
    cached_property,
    custom_property,
    key_property,
    lazy_property,
    mutable_property,
    required_property,
    writable_property,
)
from property_manager.sphinx import TypeInspector, setup, append_property_docs

# Initialize a logger for this module.
logger = VerboseLogger(__name__)


class PropertyManagerTestCase(unittest.TestCase):

    """Container for the :mod:`property_manager` test suite."""

    def setUp(self):
        """Enable verbose logging and usage notes."""
        property_manager.USAGE_NOTES_ENABLED = True
        coloredlogs.install(level=logging.NOTSET)
        # Separate the name of the test method (printed by the superclass
        # and/or py.test without a newline at the end) from the first line of
        # logging output that the test method is likely going to generate.
        sys.stderr.write("\n")

    def test_builtin_property(self):
        """
        Test that our assumptions about the behavior of :class:`property` are correct.

        This test helps to confirm that :class:`PropertyInspector` (on which
        other tests are based) shows sane behavior regardless of whether a
        custom property is inspected.
        """
        class NormalPropertyTest(object):
            @property
            def normal_test_property(self):
                return random.random()
        with PropertyInspector(NormalPropertyTest, 'normal_test_property') as p:
            assert p.is_recognizable
            assert p.is_recomputed
            assert p.is_read_only
            assert not p.is_injectable

    def test_custom_property(self):
        """Test that :class:`.custom_property` works just like :class:`property`."""
        class CustomPropertyTest(object):
            @custom_property
            def custom_test_property(self):
                return random.random()
        with PropertyInspector(CustomPropertyTest, 'custom_test_property') as p:
            assert p.is_recognizable
            assert p.is_recomputed
            assert p.is_read_only
            assert not p.is_injectable
            p.check_usage_notes()
        # Test that custom properties expect a function argument and validate their assumption.
        self.assertRaises(ValueError, custom_property, None)

    def test_writable_property(self):
        """Test that :class:`.writable_property` supports assignment."""
        class WritablePropertyTest(object):
            @writable_property
            def writable_test_property(self):
                return random.random()
        with PropertyInspector(WritablePropertyTest, 'writable_test_property') as p:
            assert p.is_recognizable
            assert p.is_recomputed
            assert p.is_writable
            assert not p.is_resettable
            assert p.is_injectable
            p.check_usage_notes()

    def test_required_property(self):
        """Test that :class:`.required_property` performs validation."""
        class RequiredPropertyTest(PropertyManager):
            @required_property
            def required_test_property(self):
                pass
        with PropertyInspector(RequiredPropertyTest, 'required_test_property', required_test_property=42) as p:
            assert p.is_recognizable
            assert p.is_writable
            assert not p.is_resettable
            assert p.is_injectable
            p.check_usage_notes()
        # Test that required properties must be set using the constructor.
        self.assertRaises(TypeError, RequiredPropertyTest)

    def test_mutable_property(self):
        """Test that :class:`mutable_property` supports assignment and deletion."""
        class MutablePropertyTest(object):
            @mutable_property
            def mutable_test_property(self):
                return random.random()
        with PropertyInspector(MutablePropertyTest, 'mutable_test_property') as p:
            assert p.is_recognizable
            assert p.is_recomputed
            assert p.is_writable
            assert p.is_resettable
            assert p.is_injectable
            p.check_usage_notes()

    def test_lazy_property(self):
        """Test that :class:`lazy_property` caches computed values."""
        class LazyPropertyTest(object):
            @lazy_property
            def lazy_test_property(self):
                return random.random()
        with PropertyInspector(LazyPropertyTest, 'lazy_test_property') as p:
            assert p.is_recognizable
            assert p.is_cached
            assert p.is_read_only
            p.check_usage_notes()

    def test_cached_property(self):
        """Test that :class:`.cached_property` caches its result."""
        class CachedPropertyTest(object):
            @cached_property
            def cached_test_property(self):
                return random.random()
        with PropertyInspector(CachedPropertyTest, 'cached_test_property') as p:
            assert p.is_recognizable
            assert p.is_cached
            assert not p.is_writable
            assert p.is_resettable
            p.check_usage_notes()

    def test_environment_property(self):
        """Test that custom properties can be based on environment variables."""
        variable_name = 'PROPERTY_MANAGER_TEST_VALUE'

        class EnvironmentPropertyTest(object):
            @mutable_property(environment_variable=variable_name)
            def environment_test_property(self):
                return str(random.random())

        with PropertyInspector(EnvironmentPropertyTest, 'environment_test_property') as p:
            # Make sure the property's value can be overridden using the
            # expected environment variable.
            value_from_environment = str(random.random())
            os.environ[variable_name] = value_from_environment
            assert p.value == value_from_environment
            # Make sure assignment overrides the value from the environment.
            value_from_assignment = str(random.random())
            assert value_from_assignment != value_from_environment
            p.value = value_from_assignment
            assert p.value == value_from_assignment
            # Make sure the assigned value can be cleared so that the
            # property's value falls back to the environment variable.
            p.delete()
            assert p.value == value_from_environment
            # Make sure the property's value falls back to the computed value
            # if the environment variable isn't set.
            os.environ.pop(variable_name)
            assert p.value != value_from_assignment
            assert p.value != value_from_environment
            p.check_usage_notes()

    def test_property_manager_repr(self):
        """Test :func:`repr()` rendering of :class:`PropertyManager` objects."""
        class RepresentationTest(PropertyManager):
            @required_property
            def important(self):
                pass

            @mutable_property
            def optional(self):
                return 42
        instance = RepresentationTest(important=1)
        assert "important=1" in repr(instance)
        assert "optional=42" in repr(instance)

    def test_property_injection(self):
        """Test that :class:`.PropertyManager` raises an error for unknown properties."""
        class PropertyInjectionTest(PropertyManager):
            @mutable_property
            def injected_test_property(self):
                return 'default'
        assert PropertyInjectionTest().injected_test_property == 'default'
        assert PropertyInjectionTest(injected_test_property='injected').injected_test_property == 'injected'
        self.assertRaises(TypeError, PropertyInjectionTest, random_keyword_argument=True)

    def test_property_customization(self):
        """Test that :func:`.custom_property.__new__()` dynamically constructs subclasses."""
        class CustomizedPropertyTest(object):
            @custom_property(cached=True, writable=True)
            def customized_test_property(self):
                pass
        with PropertyInspector(CustomizedPropertyTest, 'customized_test_property') as p:
            assert p.is_recognizable
            assert p.is_cached
            assert p.is_writable

    def test_setters(self):
        """Test that custom properties support setters."""
        class SetterTest(object):

            @custom_property
            def setter_test_property(self):
                return getattr(self, 'whatever_you_want_goes_here', 42)

            @setter_test_property.setter
            def setter_test_property(self, value):
                if value < 0:
                    raise ValueError
                self.whatever_you_want_goes_here = value

        with PropertyInspector(SetterTest, 'setter_test_property') as p:
            # This is basically just testing the lazy property.
            assert p.is_recognizable
            assert p.value == 42
            # Test that the setter is being called by verifying
            # that it raises a value error on invalid arguments.
            self.assertRaises(ValueError, setattr, p, 'value', -5)
            # Test that valid values are actually set.
            p.value = 13
            assert p.value == 13

    def test_deleters(self):
        """Test that custom properties support deleters."""
        class DeleterTest(object):

            @custom_property
            def deleter_test_property(self):
                return getattr(self, 'whatever_you_want_goes_here', 42)

            @deleter_test_property.setter
            def deleter_test_property(self, value):
                self.whatever_you_want_goes_here = value

            @deleter_test_property.deleter
            def deleter_test_property(self):
                delattr(self, 'whatever_you_want_goes_here')

        with PropertyInspector(DeleterTest, 'deleter_test_property') as p: