################################################################################
#
# Copyright (c) 2002-2005, Benjamin Saller <bcsaller@ideasuite.com>, and
#                              the respective authors. All rights reserved.
# For a list of Archetypes contributors see docs/CREDITS.txt.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
# * Neither the name of the author nor the names of its contributors may be used
#   to endorse or promote products derived from this software without specific
#   prior written permission.
#
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
################################################################################

from unittest import TestSuite, makeSuite
import sys

from ZPublisher.HTTPRequest import HTTPRequest
from Testing import ZopeTestCase

from Products.Archetypes.tests.atsitetestcase import ATSiteTestCase
from Products.Archetypes.tests.utils import mkDummyInContext
from Products.Archetypes.atapi import TextField, BaseSchema, Schema, BaseContent

from Products.Archetypes.ArchetypeTool import registerType

textfield1 = TextField('TEXTFIELD1', required=True, default='A')

textfield1b = TextField('TEXTFIELD1', required=False, default='A')

textfield2 = TextField('TEXTFIELD2', default='B')

schema1 = BaseSchema + Schema((
        textfield1,
        ))

schema2 = BaseSchema + Schema((
        textfield1b,
        textfield2,
        ))


class Dummy1(BaseContent):
    pass


class Dummy2(BaseContent):
    pass


class TestUpdateSchema(ZopeTestCase.Sandboxed, ATSiteTestCase):

    def afterSetUp(self):
        ATSiteTestCase.afterSetUp(self)
        self.attool = self.portal.archetype_tool
        # Calling mkDummyInContext adds content, but also registers
        # our classes and adds a copy of the schema.
        self._dummy1 = mkDummyInContext(
            Dummy1, oid='dummy1', context=self.portal, schema=schema1)
        self._dummy2 = mkDummyInContext(
            Dummy2, oid='dummy2', context=self.portal, schema=schema2)

    def test_instance_schema_is_harmful(self):
        """Show that having a schema in the instance is harmful.

        schema should be a class attribute, not an instance attribute.

        The only thing this really tests is that for AT >= 1.5.2,
        having a schema attribute on the instance is bad.  In earlier
        ATs this is no problem.  Nothing bad happens due to the
        earlier AT code.  But the newer ATs cannot handle older
        content that has had a schema update already.

        So: if you copy this test to an earlier Archetypes and it
        fails, that is okay really.  But in AT >= 1.5.2 it does *not*
        fail and this means that some code needs be added to migrate
        old content.
        """
        dummy = self._dummy1
        self.assertTrue(dummy._isSchemaCurrent())

        # You can make schema an instance attribute if you want (or if
        # you are not careful).
        self.assertFalse('schema' in dummy.__dict__)
        dummy.schema = dummy.__class__.schema
        self.assertTrue('schema' in dummy.__dict__)
        # The schema has not *really* changed:
        self.assertTrue(dummy._isSchemaCurrent())
        # But the damage has been done, as we will show soon.

        # We give the class of our content a different schema.
        dummy.__class__.schema = schema2.copy()
        # Reregister the type.  (Not needed in AT <= 1.5.1)
        registerType(Dummy1, 'Archetypes')
        # We are not testing the _isSchemaCurrent method here, so we
        # can simply cheat to let the object know that its schema is
        # not current anymore.
        dummy._signature = 'bogus'
        self.assertFalse(dummy._isSchemaCurrent())

        # Our class has a TEXTFIELD2, but our content does not now it
        # yet.  It *does* already have the getter for that field.
        dummy.getTEXTFIELD2
        self.assertRaises(KeyError, dummy.getTEXTFIELD2)

        # No problem, we just need to update the schema of the
        # content.  Might as well do that for all objects, as that is
        # what the user will do in practice.
        dummy._updateSchema()

        # And now we can get our second text field, right?  Wrong.
        # Only the getter is there and it does not work.
        self.assertTrue(hasattr(dummy, 'getTEXTFIELD2'))
        # Actually, the next two tests fail for AT <= 1.5.1, which is
        # actually good.
        self.assertRaises(KeyError, dummy.getTEXTFIELD2)
        self.assertFalse(hasattr(dummy, 'TEXTFIELD2'))

        # And the first field was required in the first schema but not
        # in the second.  This does not show yet.
        self.assertTrue(dummy.getField('TEXTFIELD1').required)

        # This can be fixed by deleting the schema attribute of the
        # instance.
        del dummy.schema
        self.assertFalse(dummy.getField('TEXTFIELD1').required)

        # At first, direct attribute access for the second field still
        # does not work:
        self.assertFalse(hasattr(dummy, 'TEXTFIELD2'))
        # But calling the getter works.
        self.assertEqual(dummy.getTEXTFIELD2(), 'B')
        # And after that call, direct attribute access works too.
        self.assertEqual(dummy.TEXTFIELD2(), 'B')
        # Note: TEXTFIELD is a BaseUnit, which means you need to call
        # it to get its value.

    def test_no_schema_attribute_added(self):
        """Does updating the schema mess things up?

        Updating the schema should not add the schema as instance
        attribute, unless you *really* know what you are doing.
        """
        dummy = self._dummy1
        dummy._updateSchema()
        self.assertFalse('schema' in dummy.__dict__)

    def test_detect_schema_change(self):
        dummy = self._dummy1
        self.assertTrue(dummy._isSchemaCurrent())
        dummy.__class__.schema = schema2.copy()
        # Reregister the type.  (Not needed in AT <= 1.5.1)
        registerType(Dummy1, 'Archetypes')
        self.assertFalse(dummy._isSchemaCurrent())
        dummy._updateSchema()
        self.assertTrue(dummy._isSchemaCurrent())

    def test_remove_instance_schemas(self):
        dummy = self._dummy1
        dummy.schema = schema2.copy()
        self.assertTrue('schema' in dummy.__dict__)
        dummy._updateSchema()
        self.assertTrue('schema' in dummy.__dict__)
        dummy._updateSchema(remove_instance_schemas=True)
        self.assertFalse('schema' in dummy.__dict__)

    def test_manage_update_schema(self):
        dummy = self._dummy1
        dummy.schema = schema2.copy()
        self.assertTrue('schema' in dummy.__dict__)
        self.assertFalse(dummy._isSchemaCurrent())

        # Now we want to update all schemas, but first archetype_tool
        # needs to know that our class needs updating.  The easiest of
        # course is to cheat.
        self.assertEqual(self.types_to_update(), [])
        self.attool._types['Archetypes.Dummy1'] = 'cheat'
        self.assertEqual(self.types_to_update(), ['Archetypes.Dummy1'])

        # Now we are ready to call manage_updateSchema
        self.attool.manage_updateSchema()
        # This will have no effect on the schema attribute:
        self.assertTrue('schema' in dummy.__dict__)
        # It *does* wrongly mark the schema as current.
        self.assertTrue(dummy._isSchemaCurrent())
        # So we cheat again and then it works.
        dummy._signature = 'bogus'
        self.assertFalse(dummy._isSchemaCurrent())

        # Let's try again.  But first we cheat again.
        self.assertEqual(self.types_to_update(), [])
        self.attool._types['Archetypes.Dummy1'] = 'cheat'
        self.assertEqual(self.types_to_update(), ['Archetypes.Dummy1'])

        # We need to call manage_updateSchema with an extra option.
        self.attool.manage_updateSchema(remove_instance_schemas=True)
        self.assertFalse('schema' in dummy.__dict__)

    def types_to_update(self):
        """Which types have a changed schema?
        """
        return [ti[0] for ti in self.attool.getChangedSchema() if ti[1]]


class TestBasicSchemaUpdate(ATSiteTestCase):
    """Tests for update schema behavior which depend only on the basic
       types, and examine baseline behavior when no real schema changes have
       happened."""

    def test_update_preserves_mimetype(self):
        self.folder.invokeFactory('DDocument', 'mydoc', title="My Doc")
        doc = self.folder.mydoc
        doc.setBody("""
An rst Document
===============

* Which

  * has

  * some

* bullet::

  points.

* for testing""",  mimetype="text/restructured")
        doc.reindexObject()
        mimetype = doc.getField('body').getContentType(doc)
        self.assertEqual(mimetype, 'text/x-rst')

        # update schema for all DDocuments and check if our type is preserved
        request = HTTPRequest(sys.stdin,
                              {'SERVER_NAME': 'test', 'SERVER_PORT': '8080'},
                              {})
        request.form['Archetypes.DDocument'] = True
        request.form['update_all'] = True
        self.portal.archetype_tool.manage_updateSchema(REQUEST=request)
        doc = self.folder.mydoc
        mimetype = doc.getField('body').getContentType(doc)
        self.assertEqual(mimetype, 'text/x-rst')


def test_suite():
    suite = TestSuite()
    suite.addTest(makeSuite(TestUpdateSchema))
    suite.addTest(makeSuite(TestBasicSchemaUpdate))
    return suite
