from plone import api
from plone.app.testing import logout
from plone.app.testing import TEST_USER_NAME
from ploneintranet.search.interfaces import ISearchResponse
from ploneintranet.search.solr import testing
from ploneintranet.search.solr.indexers import solr_indexing_disable
from ploneintranet.search.solr.indexers import solr_indexing_enable
from ploneintranet.search.solr.solr_search import SiteSearch
from ploneintranet.search.testing import login_session
from ploneintranet.search.testing import TEST_USER_1_NAME
from transaction import commit
from zope.interface.verify import verifyObject

import transaction
import unittest


class TestConnectionConfig(unittest.TestCase):
    """Unittests for ZCML directive."""

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

        return ConnectionConfig(*args, **kw)

    def test_interface_compliance(self):
        from ploneintranet.search.solr.interfaces import IConnectionConfig

        obj = self._make_utility("localhost", "1111", "/solr", "core1")
        verifyObject(IConnectionConfig, obj)

    def test_url(self):
        obj = self._make_utility("localhost", "1111", "/solr", "core1")
        self.assertEqual(obj.url, "http://localhost:1111/solr/core1")


class TestSolrSearch(testing.FunctionalTestCaseWithDummyContent):
    """Tests for SiteSearch utility.

    Must be functional because some of the (inherited) tests do commit()
    """

    def test_query_with_complex_filters(self):
        util = self._make_utility()
        Q = util.Q
        filters = Q(Title="Test Doc 1") | Q(Title="Test Doc 2")
        filters &= Q(portal_type="Document")
        response = util.query(phrase="Test Doc", filters=filters, debug=True)
        self.assertEqual(response.total_results, 2)

    def test_raw_query_with_complex_filters(self):
        util = self._make_utility()
        query = util.connection.query("Test Doc")
        query = query.filter(
            query.Q(Title="Test Doc 1")
            | query.Q(Title="Test Doc 2") & query.Q(portal_type="Document")
        )
        response = ISearchResponse(util.execute(query))
        self.assertEqual(response.total_results, 2)

    def test_partial_updates(self):
        """Partial updates are not supported."""
        old_title = self.doc1.title
        old_description = self.doc1.description
        self.doc1.title = "Star Wars Part 7"
        self.doc1.reindexObject(idxs=["Title"])
        transaction.commit()

        util = self._make_utility()

        response = util.query(phrase="Wars")
        self.assertEqual(response.total_results, 1)

        # Change a index without changing object.
        self.doc1.reindexObject(idxs=["review_state"])
        transaction.commit()
        response = util.query(phrase="Wars")
        self.assertEqual(response.total_results, 1)

        self.doc1.description = "Luke Skywalker"
        self.doc1.reindexObject(idxs=["Description", "NotASolrIndex"])
        transaction.commit()

        response = util.query(phrase="Skywalker")
        self.assertEqual(response.total_results, 1)

        self.doc1.title = "JaJa Binks"
        self.doc1.reindexObject(idxs=["NotASolrIndex"])
        transaction.commit()

        response = util.query(phrase="JarJar")
        self.assertEqual(response.total_results, 0)

        # Reset this stuff, we are in a functional test
        self.doc1.title = old_title
        self.doc1.description = old_description
        self.doc1.reindexObject()
        transaction.commit()

        response = util.query(phrase="Skywalker")
        self.assertEqual(response.total_results, 0)

    def test_make_boosted_field_query(self):
        """Test that a query is boosted"""
        util = self._make_utility()
        # portal_type is not boosted
        self.assertEqual(
            str(util._make_boosted_field_query("portal_type", "File")),
            "portal_type:File",
        )
        # Title is boosted
        self.assertEqual(
            str(util._make_boosted_field_query("Title", "Hello")), "Title:Hello^5"
        )
        # if we have an OR operator it will be correctly interpreted
        self.assertEqual(
            str(util._make_boosted_field_query("Title", "Hello OR world")),
            "(Title:Hello OR Title:world)^5",
        )

    def test_create_lucene_query(self):
        util = self._make_utility()
        self.assertEqual(str(util.create_lucene_query(None)), "")
        self.assertEqual(str(util.create_lucene_query("")), "")
        self.assertEqual(
            str(util.create_lucene_query("a")),
            "Title:a^5 OR Description:a^3 OR tags:a^4 OR SearchableText:a^1",
        )
        self.assertEqual(
            str(util.create_lucene_query("a OR b")),
            (
                "(Title:a OR Title:b)^5 OR "
                "(Description:a OR Description:b)^3 OR "
                "(tags:a OR tags:b)^4 OR "
                "(SearchableText:a OR SearchableText:b)^1"
            ),
        )
        self.assertEqual(
            str(util.create_lucene_query("a AND b")),
            (
                "(Title:a^5 OR Description:a^3 OR tags:a^4 OR SearchableText:a^1) AND "  # noqa: E501
                "(Title:b^5 OR Description:b^3 OR tags:b^4 OR SearchableText:b^1)"  # noqa: E501
            ),
        )
        self.assertEqual(
            str(util.create_lucene_query("a AND b OR c AND d")),
            (
                "(Title:a^5 OR Description:a^3 OR tags:a^4 OR SearchableText:a^1) AND "  # noqa: E501
                "((Title:b OR Title:c)^5 OR (Description:b OR Description:c)^3 OR (tags:b OR tags:c)^4 OR (SearchableText:b OR SearchableText:c)^1) AND "  # noqa: E501
                "(Title:d^5 OR Description:d^3 OR tags:d^4 OR SearchableText:d^1)"  # noqa: E501
            ),
        )

    def test_boolean_queries(self):
        util = self._make_utility()
        self.assertSetEqual(
            {x.description for x in util.query(phrase="hopefully OR please")},
            {
                "This is a test document. " "Hopefully some stuff will be indexed.",
                "This is another test document. " "Please let some stuff be indexed. ",
            },
        )
        self.assertSetEqual(
            {x.description for x in util.query(phrase="hopefully AND please")}, set()
        )
        self.assertSetEqual(
            {x.description for x in util.query(phrase="stuff AND please")},
            {"This is another test document. " "Please let some stuff be indexed. "},
        )
        self.assertSetEqual(
            {x.description for x in util.query(phrase="stuff -please")},
            {"This is a test document. " "Hopefully some stuff will be indexed."},
        )

    def test_piped_filters(self):
        """This will submit to Solr a query like Title:Doc OR tag:Doc

        We can find documents that match just one index
        """
        util = self._make_utility()
        # Test match title but not subject
        response = util.query(filters={"Title|Subject": "Weather"})
        observed = {
            ("weather" in x.title.lower(), "weather" in x.get("Subject"))
            for x in response
        }
        self.assertSetEqual(observed, {(True, False)})
        # Test match subject but not title
        response = util.query(filters={"Title|Subject": "trivia"})
        observed = {
            ("trivia" in x.title.lower(), "trivia" in x.get("Subject"))
            for x in response
        }
        self.assertSetEqual(observed, {(False, True)})

    def _test_solr_index_disable_enable(self, request=None):
        util = self._make_utility()

        # 1st run, indexing disabled
        if request is not None:
            solr_indexing_disable(request)
        else:
            solr_indexing_disable()

        response = self._query(util, "doc")
        self.assertEqual(response.total_results, 3)

        obj1 = api.content.create(
            container=self.portal,
            type="Document",
            id="temp-doc",
            title="Temporary Document",
        )
        obj1.reindexObject()
        commit()

        # The new document is not registered in Solr
        response = self._query(util, "doc")
        self.assertEqual(response.total_results, 3)

        # 2nd run, indexing re-enabled
        if request is not None:
            solr_indexing_enable(request)
        else:
            solr_indexing_enable()

        obj1.reindexObject()
        obj2 = api.content.create(
            container=self.portal,
            type="Document",
            id="temp-doc-2",
            title="Temporary Document",
        )
        commit()

        # Both the reindexed and the just created one are registered in Solr
        response = self._query(util, "doc")
        self.assertEqual(response.total_results, 5)

        # Clean up
        api.content.delete(obj1)
        api.content.delete(obj2)
        commit()

        # We are back to 3 now
        response = self._query(util, "doc")
        self.assertEqual(response.total_results, 3)

    def test_solr_index_disable_enable(self):
        """Test the utility functions
        - solr_indexing_disable
        - solr_indexing_enable

        that accept an optional request parameter
        """
        self._test_solr_index_disable_enable()
        self._test_solr_index_disable_enable(self.request)


class TestSolrPermissions(testing.FunctionalTestCaseWithDummyContent):
    """Integration tests for SiteSearch permissions."""

    def setUp(self):
        super().setUp()
        login_session(TEST_USER_NAME)

    def test_documents_owned_by_other_not_visible_same_role(self):
        with login_session(TEST_USER_1_NAME):
            util = self._make_utility()
            response = self._query(util, "Test Doc")
            self.assertEqual(response.total_results, 0)
            response = self._query(util, "Test Doc", secure=False)
            self.assertEqual(response.total_results, 3)

    def test_review_state_changes(self):
        """Does the search respect view permissions?"""
        api.content.transition(obj=self.doc2, transition="publish")
        self.doc2.reindexObject()
        transaction.commit()

        logout()
        util = self._make_utility()

        # Check anonymous access
        response = self._query(util, "hopefully")
        self.assertEqual(response.total_results, 0)
        response = self._query(util, "another")
        self.assertEqual(response.total_results, 1)

        with login_session(TEST_USER_1_NAME):
            response = self._query(util, "hopefully")
            self.assertEqual(response.total_results, 0)
            response = self._query(util, "another")
            self.assertEqual(response.total_results, 1)

            with api.env.adopt_roles(["Manager"]):
                api.content.transition(obj=self.doc1, transition="publish")
                self.doc1.reindexObject()
                transaction.commit()

            response = self._query(util, "hopefully")
            self.assertEqual(response.total_results, 1)

        # Bring things back to normal
        with api.env.adopt_roles(["Manager"]):
            for obj in (self.doc1, self.doc2):
                api.content.transition(obj=obj, to_state="private")
                obj.reindexObject()
            transaction.commit()

    def test_group_changes(self):
        """Does the search respect group permissions?"""
        api.group.create(groupname="TestUsers")
        api.group.grant_roles(groupname="TestUsers", obj=self.doc1, roles=["Owner"])
        self.doc1.reindexObject()
        transaction.commit()

        logout()

        util = self._make_utility()

        with login_session(TEST_USER_1_NAME):
            response = self._query(util, "hopefully")
            self.assertEqual(response.total_results, 0)

        # Add user to the group - they should now doc1
        # the item in search results
        api.group.add_user(groupname="TestUsers", username=TEST_USER_1_NAME)

        with login_session(TEST_USER_1_NAME):
            response = self._query(util, "hopefully")
            self.assertEqual(response.total_results, 1)


class TestCheckNegatives(unittest.TestCase):
    """Extract negative filters from search phrase"""

    @property
    def check_negatives(self):
        return SiteSearch().check_negatives

    def test_regular_phrase(self):
        phrase, filters = self.check_negatives("foo bar", {})
        self.assertEqual(phrase, "foo bar")
        self.assertDictEqual(filters, {})

    def test_phrase_with_negative(self):
        phrase, filters = self.check_negatives("foo -bar", {})
        self.assertEqual(phrase, "foo")
        self.assertDictEqual(filters, {"-SearchableText": "bar"})

    def test_phrase_start_with_negative(self):
        phrase, filters = self.check_negatives("-foo bar", {})
        self.assertEqual(phrase, "bar")
        self.assertDictEqual(filters, {"-SearchableText": "foo"})

    def test_only__negative(self):
        phrase, filters = self.check_negatives("-foobar", {})
        self.assertEqual(phrase, "")
        self.assertDictEqual(filters, {"-SearchableText": "foobar"})

    def test_phrase_with_dashes_not_parsed_as_negative(self):
        phrase, filters = self.check_negatives("VRT-CAR-003", {})
        self.assertEqual(phrase, "VRT-CAR-003")
        self.assertDictEqual(filters, {})

    def test_phrase_with_dashes_and_negative(self):
        phrase, filters = self.check_negatives("VRT-CAR-003 -fysio", {})
        self.assertEqual(phrase, "VRT-CAR-003")
        self.assertDictEqual(filters, {"-SearchableText": "fysio"})

    def test_phrase_starts_with_dashed_negative(self):
        phrase, filters = self.check_negatives("-VRT-CAR-003 fysio", {})
        self.assertEqual(phrase, "fysio")
        self.assertDictEqual(filters, {"-SearchableText": "VRT-CAR-003"})
