# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

import random
from functools import partial

import mango
import copy
import unittest

DOCS = [
    {"_id": "1", "name": "Jimi", "age": 10, "cars": 1},
    {"_id": "2", "name": "kate", "age": 8, "cars": 0},
]


class IndexCrudTests(mango.DbPerClass):
    def setUp(self):
        super().setUp(db_per_test=True)

    def test_bad_fields(self):
        bad_fields = [
            None,
            True,
            False,
            "bing",
            2.0,
            {"foo": "bar"},
            [{"foo": 2}],
            [{"foo": "asc", "bar": "desc"}],
            [{"foo": "asc"}, {"bar": "desc"}],
            [""],
        ]
        for fields in bad_fields:
            try:
                self.db.create_index(fields)
            except Exception as e:
                self.assertEqual(e.response.status_code, 400)
            else:
                raise AssertionError("bad create index")

    def test_bad_types(self):
        bad_types = [
            None,
            True,
            False,
            1.5,
            "foo",  # Future support
            "geo",  # Future support
            {"foo": "bar"},
            ["baz", 3.0],
        ]
        for bt in bad_types:
            try:
                self.db.create_index(["foo"], idx_type=bt)
            except Exception as e:
                self.assertEqual(
                    e.response.status_code, 400, (bt, e.response.status_code)
                )
            else:
                raise AssertionError("bad create index")

    def test_bad_names(self):
        bad_names = ["", True, False, 1.5, {"foo": "bar"}, [None, False]]
        for bn in bad_names:
            try:
                self.db.create_index(["foo"], name=bn)
            except Exception as e:
                self.assertEqual(e.response.status_code, 400)
            else:
                raise AssertionError("bad create index")

    def test_bad_ddocs(self):
        bad_ddocs = ["", "_design/", True, False, 1.5, {"foo": "bar"}, [None, False]]
        for bd in bad_ddocs:
            try:
                self.db.create_index(["foo"], ddoc=bd)
            except Exception as e:
                self.assertEqual(e.response.status_code, 400)
            else:
                raise AssertionError("bad create index")

    def test_bad_urls(self):
        # These are only the negative test cases because ideally the
        # positive ones are implicitly tested by other ones.

        all_methods = [
            ("PUT", self.db.sess.put),
            ("GET", self.db.sess.get),
            ("POST", self.db.sess.post),
            ("PATCH", self.db.sess.get),
            ("DELETE", self.db.sess.delete),
            ("HEAD", self.db.sess.head),
            ("COPY", partial(self.db.sess.request, "COPY")),
            ("OPTIONS", partial(self.db.sess.request, "OPTIONS")),
            ("TRACE", partial(self.db.sess.request, "TRACE")),
            ("CONNECT", partial(self.db.sess.request, "CONNECT")),
        ]

        def all_but(method):
            return list(filter(lambda x: x[0] != method, all_methods))

        # Three-element subpaths are used as a shorthand to delete
        # indexes via design documents, see below.
        for subpath in ["a", "a/b", "a/b/c/d", "a/b/c/d/e", "a/b/c/d/e/f"]:
            path = self.db.path("_index/{}".format(subpath))
            for method_name, method in all_methods:
                with self.subTest(path=path, method=method_name):
                    r = method(path)
                    self.assertEqual(r.status_code, 404)

        for method_name, method in all_but("POST"):
            path = self.db.path("_index/_bulk_delete")
            with self.subTest(path=path, method=method_name):
                r = method(path)
                self.assertEqual(r.status_code, 405)

        fields = ["foo", "bar"]
        ddoc = "dd"
        idx = "idx_01"
        ret = self.db.create_index(fields, name=idx, ddoc=ddoc)
        assert ret is True
        for subpath in [
            "{}/json/{}".format(ddoc, idx),
            "_design/{}/json/{}".format(ddoc, idx),
        ]:
            path = self.db.path("_index/{}".format(subpath))
            for method_name, method in all_but("DELETE"):
                r = method(path)
                with self.subTest(path=path, method=method_name):
                    self.assertEqual(r.status_code, 405)

    def test_create_idx_01(self):
        fields = ["foo", "bar"]
        ret = self.db.create_index(fields, name="idx_01")
        assert ret is True
        for idx in self.db.list_indexes():
            if idx["name"] != "idx_01":
                continue
            self.assertEqual(idx["def"]["fields"], [{"foo": "asc"}, {"bar": "asc"}])
            return
        raise AssertionError("index not created")

    def test_create_idx_01_exists(self):
        fields = ["foo", "bar"]
        ret = self.db.create_index(fields, name="idx_01")
        assert ret is True
        ret = self.db.create_index(fields, name="idx_01")
        assert ret is False

    def test_create_idx_02(self):
        fields = ["baz", "foo"]
        ret = self.db.create_index(fields, name="idx_02")
        assert ret is True
        for idx in self.db.list_indexes():
            if idx["name"] != "idx_02":
                continue
            self.assertEqual(idx["def"]["fields"], [{"baz": "asc"}, {"foo": "asc"}])
            return
        raise AssertionError("index not created")

    def test_read_idx_doc(self):
        self.db.create_index(["foo", "bar"], name="idx_01")
        self.db.create_index(["hello", "bar"])
        for idx in self.db.list_indexes():
            if idx["type"] == "special":
                continue
            ddocid = idx["ddoc"]
            doc = self.db.open_doc(ddocid)
            self.assertEqual(doc["_id"], ddocid)
            info = self.db.ddoc_info(ddocid)
            self.assertEqual(info["name"], ddocid.split("_design/")[-1])

    def test_delete_idx_escaped(self):
        self.db.create_index(["foo", "bar"], name="idx_01")
        pre_indexes = self.db.list_indexes()
        ret = self.db.create_index(["bing"], name="idx_del_1")
        assert ret is True
        for idx in self.db.list_indexes():
            if idx["name"] != "idx_del_1":
                continue
            self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}])
            self.db.delete_index(idx["ddoc"].replace("/", "%2F"), idx["name"])
        post_indexes = self.db.list_indexes()
        self.assertEqual(pre_indexes, post_indexes)

    def test_delete_idx_unescaped(self):
        pre_indexes = self.db.list_indexes()
        ret = self.db.create_index(["bing"], name="idx_del_2")
        assert ret is True
        for idx in self.db.list_indexes():
            if idx["name"] != "idx_del_2":
                continue
            self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}])
            self.db.delete_index(idx["ddoc"], idx["name"])
        post_indexes = self.db.list_indexes()
        self.assertEqual(pre_indexes, post_indexes)

    def test_delete_idx_no_design(self):
        pre_indexes = self.db.list_indexes()
        ret = self.db.create_index(["bing"], name="idx_del_3")
        assert ret is True
        for idx in self.db.list_indexes():
            if idx["name"] != "idx_del_3":
                continue
            self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}])
            self.db.delete_index(idx["ddoc"].split("/")[-1], idx["name"])
        post_indexes = self.db.list_indexes()
        self.assertEqual(pre_indexes, post_indexes)

    def test_bulk_delete(self):
        fields = ["field1"]
        ret = self.db.create_index(fields, name="idx_01")
        assert ret is True

        fields = ["field2"]
        ret = self.db.create_index(fields, name="idx_02")
        assert ret is True

        fields = ["field3"]
        ret = self.db.create_index(fields, name="idx_03")
        assert ret is True

        docids = []

        for idx in self.db.list_indexes():
            if idx["ddoc"] is not None:
                docids.append(idx["ddoc"])

        docids.append("_design/this_is_not_an_index_name")

        ret = self.db.bulk_delete(docids)

        self.assertEqual(ret["fail"][0]["id"], "_design/this_is_not_an_index_name")
        self.assertEqual(len(ret["success"]), 3)

        for idx in self.db.list_indexes():
            assert idx["type"] != "json"
            assert idx["type"] != "text"

    def test_recreate_index(self):
        pre_indexes = self.db.list_indexes()
        for i in range(5):
            ret = self.db.create_index(["bing"], name="idx_recreate")
            assert ret is True
            for idx in self.db.list_indexes():
                if idx["name"] != "idx_recreate":
                    continue
                self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}])
                self.db.delete_index(idx["ddoc"], idx["name"])
                break
            post_indexes = self.db.list_indexes()
            self.assertEqual(pre_indexes, post_indexes)

    def test_delete_missing(self):
        # Missing design doc
        try:
            self.db.delete_index("this_is_not_a_design_doc_id", "foo")
        except Exception as e:
            self.assertEqual(e.response.status_code, 404)
        else:
            raise AssertionError("bad index delete")

        # Missing view name
        ret = self.db.create_index(["fields"], name="idx_01")
        indexes = self.db.list_indexes()
        not_special = [idx for idx in indexes if idx["type"] != "special"]
        idx = random.choice(not_special)
        ddocid = idx["ddoc"].split("/")[-1]
        try:
            self.db.delete_index(ddocid, "this_is_not_an_index_name")
        except Exception as e:
            self.assertEqual(e.response.status_code, 404)
        else:
            raise AssertionError("bad index delete")

        # Bad view type
        try:
            self.db.delete_index(ddocid, idx["name"], idx_type="not_a_real_type")
        except Exception as e:
            self.assertEqual(e.response.status_code, 404)
        else:
            raise AssertionError("bad index delete")

    def test_limit_skip_index(self):
        fields = ["field1"]
        ret = self.db.create_index(fields, name="idx_01")
        assert ret is True

        fields = ["field2"]
        ret = self.db.create_index(fields, name="idx_02")
        assert ret is True

        fields = ["field3"]
        ret = self.db.create_index(fields, name="idx_03")
        assert ret is True

        fields = ["field4"]
        ret = self.db.create_index(fields, name="idx_04")
        assert ret is True

        fields = ["field5"]
        ret = self.db.create_index(fields, name="idx_05")
        assert ret is True

        self.assertEqual(len(self.db.list_indexes(limit=2)), 2)
        self.assertEqual(len(self.db.list_indexes(limit=5, skip=4)), 2)
        self.assertEqual(len(self.db.list_indexes(skip=5)), 1)
        self.assertEqual(len(self.db.list_indexes(skip=6)), 0)
        self.assertEqual(len(self.db.list_indexes(skip=100)), 0)
        self.assertEqual(len(self.db.list_indexes(limit=10000000)), 6)

        try:
            self.db.list_indexes(skip=-1)
        except Exception as e:
            self.assertEqual(e.response.status_code, 500)

        try:
            self.db.list_indexes(limit=0)
        except Exception as e:
            self.assertEqual(e.response.status_code, 500)

    def test_out_of_sync(self):
        self.db.save_docs(copy.deepcopy(DOCS))
        self.db.create_index(["age"], name="age")

        selector = {"age": {"$gt": 0}}
        docs = self.db.find(
            selector, use_index="_design/a017b603a47036005de93034ff689bbbb6a873c4"
        )
        self.assertEqual(len(docs), 2)

        self.db.delete_doc("1")

        docs1 = self.db.find(
            selector,
            update="False",
            use_index="_design/a017b603a47036005de93034ff689bbbb6a873c4",
        )
        self.assertEqual(len(docs1), 1)


@unittest.skipUnless(mango.has_text_service(), "requires text service")
class IndexCrudTextTests(mango.DbPerClass):
    def setUp(self):
        super().setUp(db_per_test=True)

    def test_create_text_idx(self):
        fields = [
            {"name": "stringidx", "type": "string"},
            {"name": "booleanidx", "type": "boolean"},
        ]
        ret = self.db.create_text_index(fields=fields, name="text_idx_01")
        assert ret is True
        for idx in self.db.list_indexes():
            if idx["name"] != "text_idx_01":
                continue
            self.assertEqual(
                idx["def"]["fields"],
                [{"stringidx": "string"}, {"booleanidx": "boolean"}],
            )
            return
        raise AssertionError("index not created")

    def test_create_bad_text_idx(self):
        bad_fields = [
            True,
            False,
            "bing",
            2.0,
            ["foo", "bar"],
            [{"name": "foo2"}],
            [{"name": "foo3", "type": "garbage"}],
            [{"type": "number"}],
            [{"name": "age", "type": "number"}, {"name": "bad"}],
            [{"name": "age", "type": "number"}, "bla"],
            [{"name": "", "type": "number"}, "bla"],
        ]
        for fields in bad_fields:
            try:
                self.db.create_text_index(fields=fields)
            except Exception as e:
                self.assertEqual(e.response.status_code, 400)
            else:
                raise AssertionError("bad create text index")

    def test_limit_skip_index(self):
        fields = ["field1"]
        ret = self.db.create_index(fields, name="idx_01")
        assert ret is True

        fields = ["field2"]
        ret = self.db.create_index(fields, name="idx_02")
        assert ret is True

        fields = ["field3"]
        ret = self.db.create_index(fields, name="idx_03")
        assert ret is True

        fields = ["field4"]
        ret = self.db.create_index(fields, name="idx_04")
        assert ret is True

        fields = [
            {"name": "stringidx", "type": "string"},
            {"name": "booleanidx", "type": "boolean"},
        ]
        ret = self.db.create_text_index(fields=fields, name="idx_05")
        assert ret is True

        self.assertEqual(len(self.db.list_indexes(limit=2)), 2)
        self.assertEqual(len(self.db.list_indexes(limit=5, skip=4)), 2)
        self.assertEqual(len(self.db.list_indexes(skip=5)), 1)
        self.assertEqual(len(self.db.list_indexes(skip=6)), 0)
        self.assertEqual(len(self.db.list_indexes(skip=100)), 0)
        self.assertEqual(len(self.db.list_indexes(limit=10000000)), 6)

        try:
            self.db.list_indexes(skip=-1)
        except Exception as e:
            self.assertEqual(e.response.status_code, 500)

        try:
            self.db.list_indexes(limit=0)
        except Exception as e:
            self.assertEqual(e.response.status_code, 500)
