from functools import partial
from glob import glob
from importlib.resources import files
from logging import getLogger
from plone import api
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from plone.testing import Layer
from plone.testing import zope as zope_testing
from plone.testing.zope import WSGI_SERVER_FIXTURE
from ploneintranet.asynctasks.celerytasks import app
from ploneintranet.search import testing as base_testing
from ploneintranet.search.interfaces import IPloneintranetSearchLayer
from ploneintranet.search.testing import login_session
from ploneintranet.search.testing import TEST_USER_1_NAME
from Products.CMFCore.indexing import processQueue
from shutil import rmtree
from tempfile import mkdtemp
from zope.component import getUtility
from zope.configuration import xmlconfig
from zope.interface import alsoProvides

import collections
import datetime
import os
import requests
import subprocess
import sys
import time

logger = getLogger(__name__)

# This works well in a docker container
# /app/parts/test/../../bin => /app/bin
_BUILDOUT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))

if not os.path.isdir(os.path.join(_BUILDOUT_DIR, "parts")):
    # This works well when using robot-server
    _BUILDOUT_DIR = os.getcwd()

SOLR_BUILDOUT_DIR = os.environ.get("SOLR_BUILDOUT_DIR", _BUILDOUT_DIR)
SOLR_TEST_PORT = int(os.environ.get("SOLR_TEST_PORT", 8984))


def load_solr_zcml():
    SOLR_ZCML = os.environ.get("SOLR_ZCML")
    if not SOLR_ZCML:
        SOLR_ZCML = str(files("ploneintranet.search.solr") / "testing.zcml")
    xmlconfig.xmlconfig(open(SOLR_ZCML))


class SolrLayer(Layer):
    """A SOLR test layer that fires up and shuts down a SOLR instance.

    This layer can be used to unit test a Solr configuration without having to
    fire up Plone.
    """

    proc = None

    def __init__(
        self,
        bases=None,
        name=None,
        module=None,
        solr_host="localhost",
        solr_port=SOLR_TEST_PORT,
        solr_basepath="/solr",
    ):
        name = name if name is not None else type(self).__name__
        super().__init__(bases, name, module)
        self.solr_host = solr_host
        self.solr_port = solr_port
        self.solr_basepath = solr_basepath
        self.solr_url = f"http://{solr_host}:{solr_port}{solr_basepath}"
        self.solr_backup_dir = mkdtemp(prefix="ploneintranet-solr-tests-")
        self.solr_snapshot_name = "test"
        self.solr_snapshot_path = os.path.join(
            self.solr_backup_dir, f"snapshot.{self.solr_snapshot_name}"
        )

    def _solr_snapshot_exists(self):
        return os.path.isdir(self.solr_snapshot_path)

    def _solr_snapshot_remove(self):
        return rmtree(self.solr_snapshot_path)

    def _solr_snapshot(self):
        """Make a snapshot of the Solr data"""
        if self._solr_snapshot_exists():
            self._solr_snapshot_remove()
        requests.get(
            "{}/{}".format(self.solr_url, "core1/replication"),
            params={
                "command": "backup",
                "name": self.solr_snapshot_name,
                "location": self.solr_backup_dir,
            },
        )

    def _solr_verify_restore_not_in_progress(self):
        """Ping Solr until it says we do have a running restore"""
        while True:
            time.sleep(0.1)
            # Check the status of the restore, continue when ready
            status = requests.get(
                "{}/{}".format(self.solr_url, "core1/replication"),
                params={"command": "restorestatus"},
            )
            if "In Progress" not in status.text:
                return True

    def _solr_restore(self):
        """Make a restore of the Solr data snapshot"""
        requests.get(
            "{}/{}".format(self.solr_url, "core1/replication"),
            params={
                "command": "restore",
                "name": self.solr_snapshot_name,
                "location": self.solr_backup_dir,
            },
        )
        self._solr_verify_restore_not_in_progress()

    def _solr_start(self):
        self.proc = subprocess.Popen(
            [
                "/usr/bin/env",
                "java",
                "-jar",
                "start.jar",
                "--module=http",
                "jetty.host=0.0.0.0",
                f"jetty.port={self.solr_port}",
            ],
            cwd=os.path.join(SOLR_BUILDOUT_DIR, "parts", "solr-test"),
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        logger.info(f"SOLR instance (PID: {self.proc.pid})")

    def _solr_stop(self):
        if getattr(self, "proc", None):
            self.proc.kill()
            # wait for the process to finish
            self.proc.communicate()
            delattr(self, "proc")
        list(
            map(
                rmtree,
                glob(
                    os.path.join(
                        SOLR_BUILDOUT_DIR,
                        "var",
                        "solr-test",
                        "solr",
                        "data",
                        "core1",
                        "restore*",
                    )
                ),
            )
        )

    def setUp(self):
        """Start Solr and poll until it is up and running."""
        self._solr_start()
        # Poll Solr until it is up and running
        solr_ping_url = f"{self.solr_url}/core1/admin/ping"
        n_attempts = 10
        i = 0
        while i < n_attempts:
            try:
                response = requests.get(solr_ping_url, timeout=1)
                if response.status_code == 200:
                    if '<str name="status">OK</str>' in response.text:
                        sys.stdout.write("[Solr Layer Connected] ")
                        sys.stdout.flush()
                        break
            except requests.ConnectionError:
                pass
            time.sleep(1)
            sys.stdout.write(".")
            sys.stdout.flush()
            i += 1
        else:  # no break
            self._solr_stop()
            raise OSError("Solr Test Instance could not be started")

    def tearDown(self):
        """Stop Solr."""
        self._solr_stop()


SOLR_FIXTURE = SolrLayer()


class SolrIntegrationTesting(IntegrationTesting):
    """Factory for solr-enabled test layers.

    Used in workspace.
    """

    def testSetUp(self):
        """Before each test also resore the Solr snapshot"""
        super().testSetUp()
        SOLR_FIXTURE._solr_restore()


class SolrFunctionalTesting(FunctionalTesting):
    """Factory for solr-enabled test layers.

    Used in suite and workspace.
    """

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

    def testSetUp(self):
        """Before each test also restore the Solr snapshot"""
        super().testSetUp()
        SOLR_FIXTURE._solr_restore()


NamedBaseLayers = collections.namedtuple("NamedBaseLayers", ("pisearch", "solr"))


class PloneIntranetSearchSolrLayer(PloneSandboxLayer):
    """Basic Plone layer with SOLR support"""

    defaultBases = NamedBaseLayers(base_testing.FIXTURE, SOLR_FIXTURE)

    def setUpZope(self, app, configuration_context):

        import z3c.jbot

        self.loadZCML(package=z3c.jbot)

        import ploneintranet.search.solr

        self.loadZCML(package=ploneintranet.search.solr)

        load_solr_zcml()

    def testTearDown(self):
        self.purge_solr()

    def purge_solr(self):
        from .interfaces import IMaintenance

        getUtility(IMaintenance).purge()


class PloneIntranetSearchSolrTestContentLayer(PloneIntranetSearchSolrLayer):
    """Layer with SOLR support *and* example content"""

    defaultBases = (base_testing.FIXTURE, SOLR_FIXTURE, PLONE_APP_CONTENTTYPES_FIXTURE)

    def setUpZope(self, app, configuration_context):
        super().setUpZope(app, configuration_context)

        import ploneintranet.theme

        self.loadZCML(package=ploneintranet.theme)

        import ploneintranet.layout

        self.loadZCML(package=ploneintranet.layout)

        import ploneintranet.suite

        self.loadZCML(package=ploneintranet.suite)

        zope_testing.installProduct(app, "collective.workspace")
        zope_testing.installProduct(app, "Products.membrane")

    def solr_sync(self, portal):
        """Sync the portal content in to Solr"""
        request = portal.REQUEST
        alsoProvides(request, IPloneintranetSearchLayer)
        view = api.content.get_view("solr-maintenance", portal, request)
        view.sync()

    def setUpPloneSite(self, portal):
        # in case a previous teardown failed
        self.purge_solr()
        # setup the default workflow
        portal.portal_workflow.setDefaultChain("simple_publication_workflow")
        # Install into Plone site using portal_setup
        self.applyProfile(portal, "ploneintranet.suite:testing")
        self.solr_sync(portal)
        SOLR_FIXTURE._solr_snapshot()

    def tearDownPloneSite(self, portal):
        self.applyProfile(portal, "ploneintranet.suite:uninstall")
        self.purge_solr()

    def testTearDown(self):
        pass


class PloneintranetSearchLayerWithDummyContent(PloneIntranetSearchSolrLayer):
    def _create_content(self, **kw):
        obj = api.content.create(**kw)
        obj.reindexObject()
        return obj

    def setUpPloneSite(self, portal):
        super().setUpPloneSite(portal)
        self.purge_solr()
        self.date_1994 = datetime.datetime(1994, 4, 5, 2, 3, 4)
        self.date_1999 = datetime.datetime(1999, 1, 11, 2, 3, 8)
        self.date_2001 = datetime.datetime(2001, 9, 11, 0, 10, 1)
        self.date_2002 = datetime.datetime(2002, 9, 11, 1, 10, 1)

        create_doc = partial(
            self._create_content, type="Document", container=portal, safe_id=False
        )

        self.doc1 = create_doc(
            title="Test Doc 1",
            description=(
                "This is a test document. " "Hopefully some stuff will be indexed."
            ),
            subject=("test", "my-tag"),
        )
        self.doc1.modification_date = self.date_2001
        setattr(self.doc1, "creation_date", self.date_2001)

        self.doc2 = create_doc(
            title="Test Doc 2",
            description=(
                "This is another test document. " "Please let some stuff be indexed. "
            ),
            subject=("test", "my-other-tag"),
        )
        self.doc2.modification_date = self.date_2002
        setattr(self.doc2, "creation_date", self.date_2002)

        self.doc3 = create_doc(
            title="Lucid Dreaming",
            description=(
                "An interesting prose by Richard Feynman "
                "which may leave the casual reader perplexed. "
                "Nothing to do with the weather."
            ),
        )
        self.doc3.modification_date = self.date_2001
        setattr(self.doc3, "creation_date", self.date_2001)

        self.doc4 = create_doc(
            title="Weather in Wales",
            description="Its raining cats and dogs, usually.",
            subject=("trivia", "boredom", "british-hangups"),
        )
        self.doc4.modification_date = self.date_2002
        setattr(self.doc4, "creation_date", self.date_2002)

        self.doc5 = create_doc(
            title="Sorted and indexed.",
            description="Not relevant",
            subject=("solr", "boost", "values"),
        )
        self.doc5.modification_date = self.date_1999
        setattr(self.doc5, "creation_date", self.date_1999)

        self.doc6 = create_doc(
            title="Another relevant title",
            description="Is Test Doc 2 Sorted and indexed?",
            subject=("abra", "cad", "abra"),
        )
        self.doc6.modification_date = self.date_1994
        setattr(self.doc6, "creation_date", self.date_1994)
        processQueue()
        SOLR_FIXTURE._solr_snapshot()


FIXTURE = PloneIntranetSearchSolrLayer()
TEST_CONTENT_FIXTURE = PloneIntranetSearchSolrTestContentLayer()

INTEGRATION_TESTING = IntegrationTesting(
    bases=(FIXTURE,), name="PloneIntranetSearchSolrLayer:Integration"
)

# no testcontent. create another fixture if you want that
FUNCTIONAL_TESTING = FunctionalTesting(
    bases=(FIXTURE, WSGI_SERVER_FIXTURE), name="PloneIntranetSearchSolrLayer:Functional"
)


FIXTURE_WITH_DUMMY_CONTENT = PloneintranetSearchLayerWithDummyContent()

FUNCTIONAL_TESTING_WITH_DUMMY_CONTENT = FunctionalTesting(
    bases=(FIXTURE_WITH_DUMMY_CONTENT,),
    name="PloneintranetSearchLayerWithDummyContent:Functional",
)


class IntegrationTestCase(base_testing.IntegrationTestCase):

    layer = INTEGRATION_TESTING


class FunctionalTestCase(base_testing.FunctionalTestCase):

    layer = FUNCTIONAL_TESTING


class FunctionalTestCaseWithDummyContent(base_testing.FunctionalTestCase):
    layer = FUNCTIONAL_TESTING_WITH_DUMMY_CONTENT

    def _make_utility(self, *args, **kw):
        from ploneintranet.search.solr.solr_search import SiteSearch

        return SiteSearch()

    def _query(self, util, *args, **kw):
        response = util.query(*args, **kw)
        return response

    def setUp(self):
        super().setUp()
        # Important! restore the Solr snapshot before running the tests
        SOLR_FIXTURE._solr_restore()

        # XXX This sucks but otherwise I have to rewrite all the tests ;)
        layer_base = self.layer.__bases__[0]
        self.portal = self.layer["portal"]
        self.request = self.layer["request"]
        # The objects we want to modify must be woke up from the portal
        self.doc1 = self.portal["test-doc-1"]
        self.doc2 = self.portal["test-doc-2"]
        # We can get the other ones from the layer base
        for idx in range(3, 7):
            attribute = f"doc{idx}"
            setattr(self, attribute, getattr(layer_base, attribute))
        login_session(TEST_USER_1_NAME)
