from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.testing import FunctionalTesting
from plone.app.testing import helpers
from plone.app.testing import IntegrationTesting
from plone.app.testing import MOCK_MAILHOST_FIXTURE
from plone.app.testing import PloneSandboxLayer
from plone.app.testing.interfaces import SITE_OWNER_NAME
from plone.testing import Layer
from plone.testing import zope as zope_testing
from plone.testing.zope import WSGI_SERVER_FIXTURE
from ploneintranet.notifications.sql import saconfig
from ploneintranet.notifications.sql.saconfig import Base
from z3c.saconfig.interfaces import IEngineFactory
from zope.component import getUtility
from zope.configuration import xmlconfig

import os
import ploneintranet.notifications.tests
import unittest

zcml = """<configure xmlns="http://namespaces.zope.org/zope"
    xmlns:db="http://namespaces.zope.org/db"
    i18n_domain="untranslated"
>
<include package="z3c.saconfig" file="meta.zcml" />
<db:engine name="ploneintranet_notifications" url="DSN" />
<db:session name="ploneintranet_notifications" engine="ploneintranet_notifications" />

</configure>
"""

postgres_active = all(
    [
        name in os.environ
        for name in {"PGUSERNAME", "PGPASSWORD", "PGHOSTNAME", "PGDATABASE", "PGTEST"}
    ]
)


class SQLLayer(Layer):
    """A layer that tests against either postgres or sqlite.

    Because the sqlalchemy session is a global utility, I was unable to provide
    separate test layers for postgres versus sqlite. Instead, I've made this
    test layer switchable.

    Postgres timezone
    -----------------

    To really test this you need to reconfigure your Postgres to a different timezone
    than your application server runs in.

    - connect to your postgres server as admin
    - ALTER DATABASE quaivedev SET timezone TO 'Asia/Tokyo';
    - restart postgres

    Postgres is now running in UTC+9.

    Postgres unittests
    ------------------

    To test against postgres, provide database variables in the environment:
    PGUSERNAME, PGPASSWORD, PGHOSTNAME, PGDATABASE

    Then use PGTEST as a switch between postgres and sqlite testing:
    bin/test -s ploneintranet.notifications -m sql               # sqlite
    PGTEST=true bin/test -s ploneintranet.notifications -m sql   # postgres

    Note that running the full test suite will wipe your database. You may
    want to provide a separate test database if that bothers you.

    Postgres manual testing
    -----------------------

    To verify notification scheduling with a live Quaive site using Postgres:

    - connect to postgres as the normal notifications user in a terminal:
        psql -h ${PGHOSTNAME} -U ${PGUSERNAME} ${PGDATABASE}

    - verify that the server is running in UTC+9 with:
        SELECT NOW();

    - configure your session to UTC:
        SET TIME ZONE UTC;

    - verify that your session is running in UTC with:
        SELECT NOW();

    Your test terminal now displays stored data in UTC, while the server still
    defaults to UTC+9 for other sessions, like the sqlalchemy interactions.

    Now create logged events and notifications as usual in Quaive. Verify in
    your psql terminal, that the database stored dates are sane when viewed in
    UTC.
    """

    def setUp(self):
        if postgres_active:
            dsn = (
                f"postgresql+psycopg2://{os.environ['PGDATABASE']}"
                f":{os.environ['PGPASSWORD']}"
                f"@{os.environ['PGHOSTNAME']}:5432"
                f"/{os.environ['PGDATABASE']}"
            )
        else:
            dsn = "sqlite:///"
        xmlconfig.string(zcml.replace("DSN", dsn))

    def testSetUp(self):
        """XXX testSetUp and testTearDown should not be necessary, but it seems
        SQL data is not correctly cleared at the end of a test method run, even
        if testTearDown does an explicit transaction.abort()"""
        engine_factory = getUtility(IEngineFactory, name="ploneintranet_notifications")
        engine = engine_factory()
        Base.metadata.create_all(engine, checkfirst=True)
        # Clearing the tables is necessary for postgres
        saconfig.session.query(saconfig.Notification).delete()
        saconfig.session.query(saconfig.LoggedEvent).delete()

    def testTearDown(self):
        # Remove all data from our tables
        saconfig.session.query(saconfig.Notification).delete()
        saconfig.session.query(saconfig.LoggedEvent).delete()


SQL_FIXTURE = SQLLayer()


class PloneintranetNotificationsLayer(PloneSandboxLayer):
    defaultBases = (
        SQL_FIXTURE,
        PLONE_APP_CONTENTTYPES_FIXTURE,
    )

    def setUpZope(self, app, configurationContext):
        xmlconfig.file(
            "configure.zcml",
            ploneintranet.notifications.tests,
            context=configurationContext,
        )
        # Required for pi.userprofile
        zope_testing.installProduct(app, "Products.membrane")

    def tearDownZope(self, app):
        pass

    def setUpPloneSite(self, portal):
        helpers.applyProfile(portal, "ploneintranet.notifications.tests:testing")


PLONEINTRANET_NOTIFICATIONS_FIXTURE = PloneintranetNotificationsLayer()

bases = (PLONEINTRANET_NOTIFICATIONS_FIXTURE, MOCK_MAILHOST_FIXTURE)

INTEGRATION_TESTING = IntegrationTesting(
    bases=bases, name="PloneintranetNotificationsLayer:Integration"
)

bases += (WSGI_SERVER_FIXTURE,)
FUNCTIONAL_TESTING = FunctionalTesting(
    bases=bases, name="PloneintranetNotificationsLayer:Functional"
)


class IntegrationTestCase(unittest.TestCase):
    """Base class for integration tests."""

    layer = INTEGRATION_TESTING

    def setUp(self):
        self.app = self.layer["app"]
        self.portal = self.layer["portal"]
        self.request = self.portal.REQUEST

    def login_as_portal_owner(self):
        """
        helper method to login as site admin
        """
        zope_testing.login(self.app["acl_users"], SITE_OWNER_NAME)


class FunctionalTestCase(unittest.TestCase):
    """Base class for functional tests."""

    layer = FUNCTIONAL_TESTING
