"""Base module for unittesting."""

from collective.mustread.testing import FunctionalBaseTestCase as MustReadCase
from collective.mustread.testing import TestingAssignable
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.testing import applyProfile
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import TEST_USER_PASSWORD
from plone.testing import Layer
from plone.testing.zope import WSGI_SERVER_FIXTURE
from ploneintranet.asynctasks.celerytasks import app
from zope.component import provideAdapter

import base64
import os
import socket
import subprocess
import sys
import time
import unittest

try:
    from plone.base.utils import safe_bytes
    from plone.base.utils import safe_text
except ImportError:
    from Products.CMFPlone.utils import safe_bytes
    from Products.CMFPlone.utils import safe_text


# /app/parts/test/../../bin => /app/bin
_BUILDOUT_BIN_DIR = os.path.abspath(
    os.path.join(os.getcwd(), os.pardir, os.pardir, "bin")
)


class CeleryLayer(Layer):
    """A Celery test layer that fires up and shuts down a Celery worker,
    but only if there's not already a Celery worker running.

    You can start a celery worker manually for test debugging:
    bin/celery -A ploneintranet.asynctasks.celerytasks worker -l DEBUG
    """

    tasks = "ploneintranet.asynctasks.celerytasks"

    def setUp(self):
        """Check whether celery is already running, else start a worker"""
        super().setUp()
        self.worker = None
        if self._celery_running():
            sys.stdout.write("[Re-using existing Celery worker] ")
        else:
            self._celery_worker()
            i = 0
            while i < 10:
                if self._celery_running():
                    sys.stdout.write("[Celery connected] ")
                    break
                else:
                    i += 1
                    time.sleep(1)
        if not self._celery_running():
            # Do not raise. Let test_dispatch figure out what is wrong.
            print("Celery could not be started (is Redis running?)")

    def tearDown(self):
        """Stop celery but only if we started it"""
        if self.worker:
            self.worker.terminate()
        super().tearDown()

    def _celery_worker(self):
        # allow root worker for gitlab-ci, only impacts tests run as root
        # does not run as root unless the tests are also run as root
        command = ["%s/celery" % _BUILDOUT_BIN_DIR, "-A", self.tasks, "worker"]
        env = {"C_FORCE_ROOT": "true"}
        env["PATH"] = os.environ.get("PATH", _BUILDOUT_BIN_DIR)
        self.worker = subprocess.Popen(
            command,
            close_fds=True,
            cwd=_BUILDOUT_BIN_DIR,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=env,
        )
        print(f"Celery worker (PID:{self.worker.pid})")

    def _celery_running(self):
        command = ["%s/celery" % _BUILDOUT_BIN_DIR, "-A", self.tasks, "status"]
        try:
            res = subprocess.check_output(command, stderr=subprocess.PIPE)
            return b"online" in res
        except subprocess.CalledProcessError:
            return False


CELERY_FIXTURE = CeleryLayer()


class PloneintranetAsyncLayer(PloneSandboxLayer):

    defaultBases = (
        PLONE_APP_CONTENTTYPES_FIXTURE,
        CELERY_FIXTURE,
    )

    def setUp(self):
        """Activate the asynctasks stack"""
        super().setUp()
        # force async regardless of buildout config
        app.conf.CELERY_ALWAYS_EAGER = False

    def setUpZope(self, app, configurationContext):
        """Set up Zope."""
        # Load ZCML
        import ploneintranet.asynctasks

        self.loadZCML(package=ploneintranet.asynctasks)
        import ploneintranet.docconv.client

        self.loadZCML(package=ploneintranet.docconv.client)

    def setUpPloneSite(self, portal):
        """Set up Plone."""
        # Install into Plone site using portal_setup
        applyProfile(portal, "ploneintranet.asynctasks:default")
        applyProfile(portal, "ploneintranet.docconv.client:default")
        setRoles(portal, TEST_USER_ID, ["Manager"])

    def tearDownZope(self, app):
        """Tear down Zope."""


class AsyncMustreadLayer(PloneintranetAsyncLayer):
    def setUpZope(self, app, configurationContext):
        super().setUpZope(app, configurationContext)
        import collective.mustread

        self.loadZCML(package=collective.mustread)
        provideAdapter(TestingAssignable)

    def setUpPloneSite(self, portal):
        super().setUpPloneSite(portal)
        applyProfile(portal, "collective.mustread:default")


FIXTURE = PloneintranetAsyncLayer()
INTEGRATION_TESTING = IntegrationTesting(
    bases=(FIXTURE,), name="PloneintranetAsyncLayer:Integration"
)
FUNCTIONAL_TESTING = FunctionalTesting(
    bases=(FIXTURE, WSGI_SERVER_FIXTURE), name="PloneintranetAsyncLayer:Functional"
)
MUSTREAD_FIXTURE = AsyncMustreadLayer()
MUSTREAD_FUNCTIONAL_TESTING = FunctionalTesting(
    bases=(MUSTREAD_FIXTURE, WSGI_SERVER_FIXTURE), name="AsyncMustreadLayer:Functional"
)


class BaseTestCase(unittest.TestCase):
    """Shared utils for integration and functional tests"""

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

    def basic_auth(self, username=TEST_USER_NAME, password=TEST_USER_PASSWORD):
        # fake needed credentials at Post.__init__
        auth = safe_bytes(f"{username}:{password}")
        cred = base64.encodebytes(auth)
        self.request._auth = "Basic %s" % safe_text(cred).strip()

    def redis_running(self):
        """All tests require redis."""
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        res = sock.connect_ex(("127.0.0.1", 6379))
        sock.close()
        return res == 0

    def waitfor(self, result, timeout=3.0):
        """Wait until result succeeds"""
        i = 0.0
        while not result.ready():
            time.sleep(0.1)
            i += 0.1
            if i >= timeout:
                self.fail("Did not get a result in %s seconds" % i)


class IntegrationTestCase(BaseTestCase):
    """Base class for integration tests."""

    layer = INTEGRATION_TESTING


class FunctionalTestCase(BaseTestCase):
    """Base class for functional tests."""

    layer = FUNCTIONAL_TESTING


class MustReadFunctionalTestCase(BaseTestCase, MustReadCase):

    layer = MUSTREAD_FUNCTIONAL_TESTING

    def setUp(self):
        BaseTestCase.setUp(self)
        MustReadCase.setUp(self)
