import os
from zope.configuration import xmlconfig, config
from zope.component.hooks import setHooks
from zope.testing.cleanup import cleanUp
from zope import component
from zope.event import notify
from zope.app.publication.zopepublication import ZopePublication
from zope.app.publication.httpfactory import HTTPPublicationRequestFactory

import zope.processlifetime

from zope.app import wsgi

from ZODB.DB import DB
from ZODB.DemoStorage import DemoStorage
import ZODB.interfaces

import transaction


def create_app(request, site_root):
    """Get a WSGI publisher application.

    Returns a :class:`zope.app.wsgi.WSGIPublisherApplication` with
    `site_root` stored in the root folder under the name ``test``.

    This function is meant to be used as setup/teardown handler
    deploying a `pytest_funcarg__`_ function.

    It sets up a ZODB_ connection to a ZODB_ DemoStorage and adds
    finalizers (`tear_down`-functionality for people from other
    testing frameworks) to shutdown the whole setup after the test
    session.

    The `request` parameter is expected to be a py.test
    :class:`FuncargRequest` (not an HTTP request or similar) which is
    generated by py.test while running tests.
    
    The :attr:`handle_errors` attribute of the returned application is
    set to ``False`` so that errors are handled by the server.

    To make use of this setup, you have to configure the environment
    properly before. Most notably you have to register basic
    components that subscribe to IDatabaseOpenedEvents, etc. This can
    normally be done using :func:`configure` or :func:`setup_config`.

    .. seealso:: `pytest_funcarg__`_ docs on pytest.org.
    
    .. _pytest_funcarg__: http://pytest.org/funcargs.html
    """
    db = setup_db()
    connection = setup_connection(db)
    root = setup_root(connection)
    root['test'] = site_root

    wsgi_app = wsgi.WSGIPublisherApplication(
        db,
        HTTPPublicationRequestFactory,
        True)

    transaction.commit()

    def finalize():
        teardown_root(root)
        teardown_connection(connection)
        teardown_db(db)

    request.addfinalizer(finalize)

    # turn this off to let the errors be handled by the server
    # this is useful for testing the server's error handling
    wsgi_app.handleErrors = False

    return wsgi_app

def configure(request, module, zcml):
    """Configure the environment.

    Performs a ZCML-driven configuration.
    
    The `request` parameter
      is expected to be a py.test
      :class:`FuncargRequest` (not an HTTP request or similar) which is
      generated by py.test while running tests.

    `module`
      is a Python module in which the ZCML file is looked up.

    `zcml`
      is a string containing the name of the ZCML file to parse. In
      many projects this configuration file for tests is called
      ``ftesting.zcml``.

    You normally run this function before calling :func:`create_app`
    as it registers basic components and event subscribers that become
    active when a ZODB_ connection is created and opened.

    This function is normally called in a `pytest_funcarg__`_ function
    which will be executed before your test.

    It adds cached setup and teardown code to be run before and after
    your session. That means the configuration setup and teardown will
    happen only once for a complete test run.
    
    .. seealso:: `pytest_funcarg__`_ docs on pytest.org.
    
    .. _pytest_funcarg__: http://pytest.org/funcargs.html

    """
    def setup_function():
        return setup_config(module, zcml)

    return request.cached_setup(setup=setup_function,
                                teardown=teardown_config,
                                scope='session')

def setup_config(package, zcml_file):
    """Setup a configuration.

    Execute the configuration specified by `package` and `zcml_file`.

    `package`
      is a Python package in which the `zcml_file` is looked up.

    `zcml_file`
      is a string giving the name of a ZCML_ file to parse.

    This is a helper function used by :func:`configure`. You normally
    use :func:`configure` to setup tests.

    The function registers the most common directives and then tries
    to parse the and execute the configuration as given in the ZCML_
    file.

    Returns the resulting ZCML_ configuration.
    """
    zcml_file = os.path.join(os.path.dirname(package.__file__),
                             zcml_file)

    setHooks()
    context = config.ConfigurationMachine()
    xmlconfig.registerCommonDirectives(context)

    return xmlconfig.file(zcml_file,
                          package=package,
                          context=context, execute=True)

def teardown_config(config):
    """Clean up a ZCML configuration.

    Unregisters components registered.
    """
    cleanUp()

def setup_db():
    """Create a ZODB_ DB with a demo storage.

    Creates a ZODB :class:`DemoStorage`, turns it into a ZODB_ DB
    named ``main`` and sends a :class:`DatebaseOpened` event to inform
    other components.

    You normally have to configure the Zope Component Architechture
    using :func:`setup_config` or (preferably) :func:`configure`
    before you call this function.
    
    Returns a ZODB DB.
    """
    name = 'main'
    storage = DemoStorage(name)
    db = DB(storage, database_name=name)
    db.setActivityMonitor(ZODB.ActivityMonitor.ActivityMonitor())

    # DB are registered as utilities
    component.provideUtility(db, ZODB.interfaces.IDatabase, name)

    # And we send a event that our DB is available
    notify(zope.processlifetime.DatabaseOpened(db))

    return db

def teardown_db(db):
    """Unregister ZODB_ DB.

    Unregisters the ``main`` database with the global site manager and
    closes the DB.
    """
    # Need to unregister DB
    base = component.getGlobalSiteManager()
    base.unregisterUtility(
        db, ZODB.interfaces.IDatabase, 'main')
    db.close()

def setup_connection(db):
    """Open a connection to `db`.

    `db`
      is a ZODB_ DB as returned from :func:`setup_db`.

    Returns a connection to the database.
    """
    return db.open()

def teardown_connection(connection):
    """Shutdown ZODB_ DB connection.

    `connection`
      is a (normally open) ZODB_ DB connection as returned by
      :func:`setup_connection`.
    
    Aborts any running transaction and closes the connection.
    """
    transaction.abort()
    connection.close()

def setup_root(connection):
    """Get the application root.

    Returns the Zope application root from a ZODB DB connected to by
    `connection`.

    `connection`
      is an (already opened) connection to a ZODB_ DB.

    Return the application root, i.e. the object that contains the
    global site manager and other fundamental ZCA-related stuff.
    """
    return connection.root()[ZopePublication.root_name]

def teardown_root(root):
    """Shutdown the application root.

    `root`
      is a ZODB_ DB application root.

    This function does actually nothing.
    """
    pass
