"""Test the models of the ``versionable`` app"""

from contextlib import contextmanager
from mock import call, patch
from unittest import skipIf

from django.test import TestCase
from django.utils import timezone

from mtp.apps.accounts.factories import AdminFactory, TrainerFactory
from mtp.discrete_apps.lockable.exceptions import LockedField

from ..exceptions import *

from .versionable_testapp.models import *


class BaseVersioningTestCase(TestCase):
    """Base test case to tests versionable.

    It provides some default test data useful in a lot of tests.

    Attributes
    ----------
    admin : MTPUser
        A ``MTPUser` with admin rights.
    versionable1 : versionable_testapp.models.VersionableModel1
        An instance of a simple versionable model, created by ``cls.admin``.
    versionable2 : versionable_testapp.models.VersionableModel1
        An instance of a simple versionable model, created by ``cls.admin``.
    dependency1 : versionable_testapp.models.DependencyModel1
        An instance of a simple dependency model, attached to ``versionable1``.
    dependency2 : versionable_testapp.models.DependencyModel2
        An instance of a simple dependency model, attached to ``versionable1``.
    dependency3 : versionable_testapp.models.DependencyModel2
        An instance of a simple dependency model, attached to ``versionable2``.
    dependency4 : versionable_testapp.models.DependencyModel2
        An instance of a simple dependency model, attached to ``versionable2``.
    subdependency1 : versionable_testapp.models.SubDependency1
        An instance of a sub dependency model, attached to ``dependency1`` and to ``versionable1``.

    """

    @classmethod
    def setUpTestData(cls):
        """Create some data to use in versionable tests."""

        super().setUpTestData()

        # Create one administrator.
        cls.admin = AdminFactory()

        # Create two versionable instances, and two dependencies for each one.
        cls.versionable1 = VersionableModel1.objects.create(
            owner=cls.admin, name='versionable1')
        cls.dependency1 = DependencyModel1.objects.create(
            parent=cls.versionable1, name='dependency1')
        cls.dependency2 = DependencyModel2.objects.create(
            parent=cls.versionable1, name='dependency1')

        # We add a sub-dependency
        cls.subdependency1 = SubDependencyModel1.objects.create(
            parent=cls.dependency1, name='subdependency1')

        # And M2M entries
        cls.item1 = NonDependencyModel.objects.create(name='item1')
        cls.item2 = NonDependencyModel.objects.create(name='item2')
        cls.subdependency1.items.add(cls.item1)
        cls.item2.links.add(cls.subdependency1)

        # For the second one, both dependencies are from the same model.
        cls.versionable2 = VersionableModel1.objects.create(
            owner=cls.admin, name='versionable2')
        cls.dependency3 = DependencyModel2.objects.create(
            parent=cls.versionable2, name='dependency3')
        cls.dependency4 = DependencyModel2.objects.create(
            parent=cls.versionable2, name='dependency4')

    def set_version_state(self, version_state, obj=None):
        """Helper to set the version state of ``self.versionable1`` (or the given object)

        We use it in the tests instead the ``override_version_state`` context manager, to
        simulate a real usage.

        """

        if obj is None:
            obj = self.versionable1

        obj.refresh_from_db()
        obj.version_state = version_state
        obj.save()


class DependencyTestCase(BaseVersioningTestCase):
    """The dependency links between ``Versionable`` and ``Dependency`` should work."""

    def test_versionable_dependency_links(self):
        """A ``Versionable`` instance should access its dependent models via ``dependency_links.."""

        self.assertEqual(self.versionable1.dependency_links, [
            (SubDependencyModel1, 'parent__parent'),
            (DependencyModel2, 'parent'),
            (DependencyModel1, 'parent'),
        ])
        self.assertEqual(self.versionable2.dependency_links, [
            (SubDependencyModel1, 'parent__parent'),
            (DependencyModel2, 'parent'),
            (DependencyModel1, 'parent'),
        ])

    def test_versionable_dependencies(self):
        """A ``Versionable`` instance should access its dependency instances via ``dependencies``"""

        self.assertEqual(self.versionable1.dependencies, [
            # SubDependencyModel
            self.subdependency1,
            # DependencyModel2
            self.dependency2,
            # DependencyModel1
            self.dependency1,
        ])
        self.assertEqual(self.versionable2.dependencies, [
            # DependencyModel2
            self.dependency3,
            self.dependency4,
        ])

    def test_dependency_versionable_instance(self):
        """A ``Dependency`` model should access its versionable instance."""

        self.assertEqual(self.dependency1.versionable_instance, self.versionable1)
        self.assertEqual(self.dependency2.versionable_instance, self.versionable1)
        self.assertEqual(self.subdependency1.versionable_instance, self.versionable1)
        self.assertEqual(self.dependency3.versionable_instance, self.versionable2)
        self.assertEqual(self.dependency4.versionable_instance, self.versionable2)


class FlagsTestCase(BaseVersioningTestCase):
    """The ``is_*`` flags of ``Versionable`` and ``Dependency`` models should work."""

    def test_versionable_is_draftable(self):
        """A ``Versionable`` can only be used to create a draft if it has specific states."""

        # Ok if copied or published.
        self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
        self.assertTrue(self.versionable1.is_draftable)
        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.assertTrue(self.versionable1.is_draftable)

        # Not ok if already a draft or archived.
        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
        self.assertFalse(self.versionable1.is_draftable)
        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
        self.assertFalse(self.versionable1.is_draftable)

    def test_versionable_is_publishable(self):
        """A ``Versionable`` can only be published it has specific states."""

        # Ok if draft or copied
        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
        self.assertTrue(self.versionable1.is_publishable)
        self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
        self.assertTrue(self.versionable1.is_publishable)

        # Not ok in all other states.
        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.assertFalse(self.versionable1.is_publishable)
        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
        self.assertFalse(self.versionable1.is_publishable)

    def test_versionable_is_editable(self):
        """A ``Versionable`` can only be edited it has specific states."""

        # Ok if draft.
        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
        self.assertTrue(self.versionable1.is_editable)

        # Not ok in all other states.
        self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
        self.assertFalse(self.versionable1.is_editable)
        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.assertFalse(self.versionable1.is_editable)
        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
        self.assertFalse(self.versionable1.is_editable)

    def test_dependency_is_editable(self):
        """A ``Dependency`` is editable only if it's related ``Versionable`` is too."""

        # Ok if draft.
        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
        self.assertTrue(self.dependency1.is_editable)
        self.assertTrue(self.subdependency1.is_editable)

        # Not ok in all other states.
        self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
        self.assertFalse(self.dependency1.is_editable)
        self.assertFalse(self.subdependency1.is_editable)
        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.assertFalse(self.dependency1.is_editable)
        self.assertFalse(self.subdependency1.is_editable)
        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
        self.assertFalse(self.dependency1.is_editable)
        self.assertFalse(self.subdependency1.is_editable)

    def test_versionable_create_draft_only_if_draftable(self):
        """Trying to create a draft of a ``Versionable`` should fail if not in the correct state."""

        # We don't want to call ``create_new_versions``, just check the state.
        with patch.object(self.versionable1, 'create_new_version'):

            # Ok if copied or published.
            self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
            self.versionable1.create_draft()
            self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
            self.versionable1.create_draft()

            def check_raise(state):
                """Helper checking that creating a draft from the instance raises correctly."""

                with self.assertRaises(NotDraftable) as context:
                    self.versionable1.create_draft()

                message = 'The "%s" cannot be used to create a draft because it is in the ' \
                          '"%s" state.'
                self.assertEqual(str(context.exception), message % (
                    repr(self.versionable1),
                    state
                ))
                self.assertEqual(context.exception.instance, self.versionable1)

            # Not ok if already a draft or archived.
            self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
            check_raise('Draft')

            self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
            check_raise('Archived')


class LockingTestCase(BaseVersioningTestCase):
    """Test locked fields on an instance when authorized or not."""

    def test_versionable_update(self):
        """A ``Versionable`` can only be edited if it has specific states."""

        # Ok if draft.
        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
        self.versionable1.name = 'versionable1 foo1'

        @contextmanager
        def check_raise(state):
            """Helper checking that saving the instance raises correctly."""

            with self.assertRaises(NotEditable) as context:
                yield

            message = 'The "name" field of "%s" cannot be edited because it is in the "%s" state.'

            self.assertEqual(str(context.exception), message % (
                repr(self.versionable1),
                state
            ))
            self.assertEqual(context.exception.instance, self.versionable1)

        # Not ok in all other states.
        self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
        with check_raise('Copied'):
            self.versionable1.name = 'versionable1 foo2'

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        with check_raise('Published'):
            self.versionable1.name = 'versionable1 foo3'

        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
        with check_raise('Archived'):
            self.versionable1.name = 'versionable1 foo4'

    def test_dependency_update(self):
        """A ``Dependency``` can only be edited if its versionable instance has specific states."""

        # Ok if draft.
        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)
        self.dependency1.name = 'dependency1 foo1'
        self.subdependency1.name = 'subdependency1 foo1'

        @contextmanager
        def check_raise(obj, state):
            """Helper checking that saving the instance raises correctly."""

            with self.assertRaises(NotEditable) as context:
                yield

            message = 'The "name" field of "%s" cannot be edited because "%s" is in the "%s" state.'

            self.assertEqual(str(context.exception), message % (
                repr(obj),
                repr(self.versionable1),
                state
            ))
            self.assertEqual(context.exception.instance, obj)

        # Not ok in all other states.
        self.set_version_state(self.versionable1.VERSION_STATES.COPIED)
        self.dependency1.refresh_from_db()
        with check_raise(self.dependency1, 'Copied'):
            self.dependency1.name = 'dependency1 foo2'

        self.subdependency1.refresh_from_db()
        with check_raise(self.subdependency1, 'Copied'):
            self.subdependency1.name = 'subdependency1 foo2'

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.dependency1.refresh_from_db()
        with check_raise(self.dependency1, 'Published'):
            self.dependency1.name = 'dependency1 foo3'

        self.subdependency1.refresh_from_db()
        with check_raise(self.subdependency1, 'Published'):
            self.subdependency1.name = 'subdependency1 foo3'

        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED)
        self.dependency1.refresh_from_db()
        with check_raise(self.dependency1, 'Archived'):
            self.dependency1.name = 'dependency1 foo4'

        self.subdependency1.refresh_from_db()
        with check_raise(self.subdependency1, 'Archived'):
            self.subdependency1.name = 'subdependency1 foo4'

    def test_dependency_is_editable_if_no_versionable(self):
        """Test that the dependency is editable if the link does not exist yet."""

        # No PK: the object is editable
        dep = DependencyModel1()
        dep.name = 'foo'

        # PK but no versionable instance: the object is editable
        dep = DependencyModel1(pk=123123)
        dep.name = 'foo'

    def test_original_and_parent_version_cannot_be_updated(self):
        """Test that we cannot update an original or parent version already set."""

        # Will not raise if the value is not already set
        obj = VersionableModel1.objects.create(owner=self.admin, name='versionable-tmp')
        obj.original_version = None
        obj.original_version = self.versionable2
        obj.parent_version = None
        obj.parent_version = self.versionable2
        obj = VersionableModel1.objects.create(owner=self.admin, name='versionable-tmp')
        obj.original_version_id = self.versionable1.pk
        obj.parent_version_id = self.versionable1.pk

        # Will raise if the value is already set

        draft = self.versionable1.duplicate()

        with self.assertRaises(LockedField):
            draft.original_version = None

        with self.assertRaises(LockedField):
            draft.original_version = self.versionable2

        with self.assertRaises(LockedField):
            draft.original_version_id = self.versionable2.pk

        with self.assertRaises(LockedField):
            draft.parent_version = None

        with self.assertRaises(LockedField):
            draft.parent_version = self.versionable2

        with self.assertRaises(LockedField):
            draft.parent_version_id = self.versionable2.pk

        # But not if the same value
        draft.original_version_id = draft.original_version.pk
        draft.original_version = VersionableModel1(pk=draft.original_version.pk)
        draft.parent_version_id = draft.parent_version.pk
        draft.parent_version = VersionableModel1(pk=draft.parent_version.pk)


class CreateNewVersionTestCase(BaseVersioningTestCase):
    """Check that instances are correctly duplicated when calling ``create_new_versions``"""

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        cls.trainer1 = TrainerFactory()
        cls.trainer2 = TrainerFactory()

    def test_duplicate_should_set_state_to_copied(self):
        """Check that the default state after a duplicate is ``COPIED``"""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)

        new_object = self.versionable1.duplicate()

        self.assertEqual(new_object.version_state, self.versionable1.VERSION_STATES.COPIED)

    def test_create_draft_should_call_create_new_version(self):
        """Check that ``create_new_version`` is correctly called when calling ``create_draft``"""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)

        # We don't want to call ``create_new_versions``, just check the call
        with patch.object(self.versionable1, 'create_new_version') as patched:
            self.versionable1.create_draft()

        # Check that ``create_new_version`` is called once, with correct arguments.
        patched.assert_called_once_with(
            new_state=self.versionable1.VERSION_STATES.DRAFT,
            params={}
        )

    def test_create_draft_with_fields_should_call_create_new_version(self):
        """Check that ``create_new_version`` is correctly called when calling ``create_draft``"""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)

        # We don't want to call ``create_new_versions``, just check the call
        with patch.object(self.versionable1, 'create_new_version') as patched:
            self.versionable1.create_draft(lang='en')

        # Check that ``create_new_version`` is called once, with correct arguments.
        patched.assert_called_once_with(
            new_state=self.versionable1.VERSION_STATES.DRAFT,
            params={'lang': 'en'}
        )

    def test_create_new_version_should_call_duplicate(self):
        """Check that ``duplicate`` is correctly called when calling ``create_new_version``"""

        now = timezone.now()

        # We don't want to call ``duplicate``, just check the calls
        with patch.object(self.versionable1, 'duplicate') as patched:
            with patch.object(timezone, 'now', return_value=now):
                self.versionable1.create_new_version(
                    new_state=self.versionable1.VERSION_STATES.DRAFT,
                    params={'lang': 'en'},
                )

        patched.assert_called_once_with(
            created_at=now,
            version_state=self.versionable1.VERSION_STATES.DRAFT,
            lang='en',
        )

    def test_create_new_versions_should_call_create_new_version(self):
        """Check that ``duplicate`` is correctly called when calling ``create_new_versions``"""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.versionable1.save()

        # We don't want to call ``create_new_version``, just check the calls
        with patch.object(self.versionable1, 'create_new_version') as patched:
            self.versionable1.create_new_versions(
                new_state=self.versionable1.VERSION_STATES.DRAFT,
                params_list=[
                    {'lang': 'en', 'owner': self.trainer1, },
                    {'lang': 'de', 'owner': self.trainer2, }
                ]
            )

        # ``create_new_version`` should be called twice, once for each user
        patched.assert_has_calls([
            call(
                new_state=self.versionable1.VERSION_STATES.DRAFT,
                params={'lang': 'en', 'owner': self.trainer1, },
            ),
            call(
                new_state=self.versionable1.VERSION_STATES.DRAFT,
                params={'lang': 'de', 'owner': self.trainer2, },
            ),
        ], any_order=True)


class UtilsTestCase(BaseVersioningTestCase):
    """Test case to test some utilities."""

    def test_override_version_state(self):
        """Test the ``override_version_state`` context manager."""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)

        # By default the published object cannot be changed
        with self.assertRaises(NotEditable):
            self.versionable1.name = 'versionable1 foo2'

        # But by locally changing the version state, it's ok
        with self.versionable1.override_version_state(self.versionable1.VERSION_STATES.DRAFT):
            self.versionable1.name = 'versionable1 foo2'

    def test_override_version_state_cache(self):
        """Test that ``override_version_state`` works for other instances of the same db object."""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)

        same_versionable = VersionableModel1.objects.get(pk=self.versionable1.pk)

        # Call the context-manager on an instance and try to update another one
        with self.versionable1.override_version_state(self.versionable1.VERSION_STATES.DRAFT):
            same_versionable.name = 'versionable1 foo1'

    def test_publish_method(self):
        """Test that calling the ``publish`` method of a ``Versionable`` set the correct state"""

        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)

        # Check that we can call it without saving
        self.versionable1.publish(commit=False)
        self.assertEqual(self.versionable1.version_state,
                         self.versionable1.VERSION_STATES.PUBLISHED)
        self.assertEqual(self.versionable1.loaded_version_state,
                         self.versionable1.VERSION_STATES.DRAFT)

        # And now we want to save it

        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)

        self.versionable1.publish()
        self.assertEqual(self.versionable1.version_state,
                         self.versionable1.VERSION_STATES.PUBLISHED)
        self.assertEqual(self.versionable1.loaded_version_state,
                         self.versionable1.VERSION_STATES.PUBLISHED)

    def test_archive_method(self):
        """Test that calling the ``archive`` method of a ``Versionable`` set the correct state"""

        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)

        # Check that we can call it without saving
        self.versionable1.archive(commit=False)
        self.assertEqual(self.versionable1.version_state,
                         self.versionable1.VERSION_STATES.ARCHIVED)
        self.assertEqual(self.versionable1.loaded_version_state,
                         self.versionable1.VERSION_STATES.DRAFT)

        # And now we want to save it

        self.set_version_state(self.versionable1.VERSION_STATES.DRAFT)

        self.versionable1.archive()
        self.assertEqual(self.versionable1.version_state,
                         self.versionable1.VERSION_STATES.ARCHIVED)
        self.assertEqual(self.versionable1.loaded_version_state,
                         self.versionable1.VERSION_STATES.ARCHIVED)

    def test_queryset(self):
        """Test the ``VersionableQuerySet`` util methods."""

        self.set_version_state(self.versionable1.VERSION_STATES.PUBLISHED)
        self.set_version_state(self.versionable1.VERSION_STATES.ARCHIVED, self.versionable2)

        published = self.versionable1
        archived = self.versionable2
        draft = published.create_draft()
        copied = published.duplicate()

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_editable()),
                            {draft})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_editable()),
                            {copied, published, archived})

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_draftable()),
                            {copied, published})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_draftable()),
                            {draft, archived})

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_publishable()),
                            {draft, copied})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_publishable()),
                            {published, archived})

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_draft()),
                            {draft})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_draft()),
                            {copied, published, archived})

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_published()),
                            {published})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_published()),
                            {draft, copied, archived})

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_copied()),
                            {copied})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_copied()),
                            {draft, published, archived})

        self.assertSetEqual(set(VersionableModel1.objects.filter_is_archived()),
                            {archived})

        self.assertSetEqual(set(VersionableModel1.objects.exclude_is_archived()),
                            {draft, copied, published})


@skipIf(not TEST_POLYMORPHIC, 'Do not test polymorphic adaptation as it is not installed.')
class PolymorpicTestCase(TestCase):
    """Test the adaptations for django-polymorphic models"""

    @classmethod
    def setUpTestData(cls):
        """Create some data to use in ``PolymorphicTestCase``."""

        super().setUpTestData()

        # Create some versionable instances
        cls.child1 = PolymorphicChild1.objects.create(name='child1', name1='child1-1')
        cls.child2 = PolymorphicChild1.objects.create(name='child2', name1='child2-1')

        # Create two dependencies
        cls.dep1 = PolymorphicDependencyChild1.objects.create(name='dep1', name1='dep1-1',
                                                              parent=cls.child1)
        cls.dep2 = PolymorphicDependencyChild1.objects.create(name='dep2', name1='dep2-1',
                                                              parent=cls.child1)

    def set_version_state(self, version_state, obj=None):
        """Helper to set the version state of ``self.child1`` (or the given object)

        We use it in the tests instead the ``override_version_state`` context manager, to
        simulate a real usage.

        """

        if obj is None:
            obj = self.child1

        obj.refresh_from_db()
        obj.version_state = version_state
        obj.save()

    def test_duplicate(self):
        """Ensure that versionable duplicate works as expected."""

        # At the begining we have two ``PolymorphicBase``
        self.assertEqual(PolymorphicBase.objects.count(), 2)
        # and two ``PolymorphicDependencyBase``
        self.assertEqual(PolymorphicDependencyBase.objects.count(), 2)
        # and two ``PolymorphicDependencyChild1``
        self.assertEqual(PolymorphicDependencyChild1.objects.count(), 2)

        new_object = self.child1.duplicate()
        self.assertEqual(new_object.version_state, new_object.VERSION_STATES.COPIED)

        # We should have one more ``PolymorphicBase``
        self.assertEqual(PolymorphicBase.objects.count(), 3)
        # and two more ``PolymorphicDependencyBase``
        self.assertEqual(PolymorphicDependencyChild1.objects.count(), 4)
        # and two more ``PolymorphicDependencyBase``
        self.assertEqual(PolymorphicDependencyChild1.objects.count(), 4)

        old_deps = set(self.child1.children.all())
        new_deps = set(new_object.children.all()) - old_deps

        # check we have the correct dependencies, ie ones not on `self.child1`
        self.assertEqual(len(new_deps), 2)

    def test_locking_versionable(self):
        """Ensure that we can't edit polymorphic a versionable published instances."""

        # Ok if draft.
        self.set_version_state(self.child1.VERSION_STATES.DRAFT)
        self.child1.name = 'child1 foo1'

        @contextmanager
        def check_raise(state):
            """Helper checking that saving the instance raises correctly."""

            with self.assertRaises(NotEditable) as context:
                yield

            message = 'The "name" field of "%s" cannot be edited because it is in the "%s" state.'

            self.assertEqual(str(context.exception), message % (
                repr(self.child1),
                state
            ))
            self.assertEqual(context.exception.instance, self.child1)

        # Not ok in all other states.
        self.set_version_state(self.child1.VERSION_STATES.COPIED)
        with check_raise('Copied'):
            self.child1.name = 'child1 foo2'

        self.set_version_state(self.child1.VERSION_STATES.PUBLISHED)
        with check_raise('Published'):
            self.child1.name = 'child1 foo3'

        self.set_version_state(self.child1.VERSION_STATES.ARCHIVED)
        with check_raise('Archived'):
            self.child1.name = 'child1 foo4'

    def test_locking_dependency(self):
        """Ensure that we can't edit polymorphic a dependency of a published instances."""

        # Ok if draft.
        self.set_version_state(self.child1.VERSION_STATES.DRAFT)
        self.dep1.name = 'dep1 foo1'

        @contextmanager
        def check_raise(obj, state):
            """Helper checking that saving the instance raises correctly."""

            with self.assertRaises(NotEditable) as context:
                yield

            message = 'The "name" field of "%s" cannot be edited because "%s" is in the "%s" state.'

            self.assertEqual(str(context.exception), message % (
                repr(obj),
                repr(self.child1),
                state
            ))
            self.assertEqual(context.exception.instance, obj)

        # Not ok in all other states.
        self.set_version_state(self.child1.VERSION_STATES.COPIED)
        self.dep1.refresh_from_db()
        with check_raise(self.dep1, 'Copied'):
            self.dep1.name = 'dep1 foo2'

        self.set_version_state(self.child1.VERSION_STATES.PUBLISHED)
        self.dep1.refresh_from_db()
        with check_raise(self.dep1, 'Published'):
            self.dep1.name = 'dep1 foo3'

        self.set_version_state(self.child1.VERSION_STATES.ARCHIVED)
        self.dep1.refresh_from_db()
        with check_raise(self.dep1, 'Archived'):
            self.dep1.name = 'dep1 foo4'

    def test_locking_m2m(self):
        """"Test that editing a m2m dependency is forbidden if the versionable is published."""

        self.set_version_state(self.child1.VERSION_STATES.DRAFT)

        m2m1 = SubDependencyModel2.objects.create(name='m2m1')
        m2m2 = SubDependencyModel2.objects.create(name='m2m2')
        m2m3 = SubDependencyModel2.objects.create(name='m2m3')

        self.dep1.items = [m2m1, m2m2]

        # Add one, it's allowed in draft mode
        self.dep1.items.add(m2m3)

        # Go to published mode
        self.set_version_state(self.child1.VERSION_STATES.PUBLISHED)

        @contextmanager
        def check_raise(obj, state):
            """Helper checking that saving the instance raises correctly."""

            with self.assertRaises(NotEditable) as context:
                yield

            message = 'The "name" field of "%s" cannot be edited because "%s" is in the "%s" state.'

            self.assertEqual(str(context.exception), message % (
                repr(obj),
                repr(self.child1),
                state
            ))
            self.assertEqual(context.exception.instance, obj)

        # It's now forbidden
        with check_raise(self.dep1, 'Published'):
            self.dep1.name = 'dep1 foo3'

    def test_queryset(self):
        """Test the ``VersionableQuerySet`` util methods."""

        # Do some checks on the dependency before doing some duplications

        # Test some methods on ``objects`` from the default ``PolymorphicManager``
        self.assertSetEqual(
            set(PolymorphicDependencyBase.objects.instance_of(PolymorphicDependencyChild1)),
            {self.dep1, self.dep2}
        )

        # With our own ``dependency_objects`` (defined on the ``Dependency`` model)
        self.assertSetEqual(
            set(PolymorphicDependencyBase.dependency_objects.instance_of(
                PolymorphicDependencyChild1
            )),
            {self.dep1, self.dep2}
        )

        # Our own queryset/manager method must be on ``objects``

        # Access the new method on the new queryset
        with self.child1.override_version_state(self.child1.VERSION_STATES.DRAFT):
            self.dep1.name = 'foo'
            self.dep1.save()
            self.assertSetEqual(
                set(PolymorphicDependencyChild1.objects.name_is_foo()),
                {self.dep1}
            )

        # And access the new method on the new manager
        PolymorphicDependencyChild1.objects.do_nothing()

        # Now we'll test versionable managers

        self.set_version_state(self.child1.VERSION_STATES.PUBLISHED)
        self.set_version_state(self.child1.VERSION_STATES.ARCHIVED, self.child2)

        published = self.child1
        archived = self.child2
        draft = published.create_draft()
        copied = published.duplicate()

        # Test some methods from the default ``PolymorphicManager``
        self.assertSetEqual(
            set(PolymorphicBase.objects.instance_of(PolymorphicChild1)),
            {published, archived, draft, copied}
        )

        # With ``versionable_objects`` from the ``PolymorphicForVersionable`` model
        self.assertSetEqual(
            set(PolymorphicBase.versionable_objects.instance_of(PolymorphicChild1)),
            {published, archived, draft, copied}
        )

        # Test an excerpt of the methods added by the ``versionable`` app

        # Some on ``objects`` (redefined in our model)
        self.assertSetEqual(set(PolymorphicBase.objects.filter_is_draftable()),
                            {copied, published})

        self.assertSetEqual(set(PolymorphicBase.objects.exclude_is_draftable()),
                            {draft, archived})

        # And some on ``versionable_objects`` (from the ``PolymorphicForVersionable`` model)
        self.assertSetEqual(set(PolymorphicBase.versionable_objects.filter_is_draft()),
                            {draft})

        self.assertSetEqual(set(PolymorphicBase.versionable_objects.exclude_is_draft()),
                            {copied, published, archived})
