# Xandikos
# Copyright (C) 2016-2017 Jelmer Vernooĳ <jelmer@jelmer.uk>, et al.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 3
# of the License or (at your option) any later version of
# the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA  02110-1301, USA.

from io import BytesIO
import logging
import unittest
from wsgiref.util import setup_testing_defaults

from xandikos import webdav
from xandikos.webdav import Collection, ET, Property, Resource, WebDAVApp


class WebTestCase(unittest.TestCase):
    def setUp(self):
        super(WebTestCase, self).setUp()
        logging.disable(logging.WARNING)
        self.addCleanup(logging.disable, logging.NOTSET)

    def makeApp(self, resources, properties):
        class Backend(object):
            get_resource = resources.get

        app = WebDAVApp(Backend())
        app.register_properties(properties)
        return app


class WebTests(WebTestCase):
    def _method(self, app, method, path):
        environ = {"PATH_INFO": path, "REQUEST_METHOD": method}
        setup_testing_defaults(environ)
        _code = []
        _headers = []

        def start_response(code, headers):
            _code.append(code)
            _headers.extend(headers)

        contents = b"".join(app(environ, start_response))
        return _code[0], _headers, contents

    def lock(self, app, path):
        return self._method(app, "LOCK", path)

    def mkcol(self, app, path):
        environ = {
            "PATH_INFO": path,
            "REQUEST_METHOD": "MKCOL",
        }
        setup_testing_defaults(environ)
        _code = []
        _headers = []

        def start_response(code, headers):
            _code.append(code)
            _headers.extend(headers)

        contents = b"".join(app(environ, start_response))
        return _code[0], _headers, contents

    def delete(self, app, path):
        environ = {"PATH_INFO": path, "REQUEST_METHOD": "DELETE"}
        setup_testing_defaults(environ)
        _code = []
        _headers = []

        def start_response(code, headers):
            _code.append(code)
            _headers.extend(headers)

        contents = b"".join(app(environ, start_response))
        return _code[0], _headers, contents

    def get(self, app, path):
        environ = {"PATH_INFO": path, "REQUEST_METHOD": "GET"}
        setup_testing_defaults(environ)
        _code = []
        _headers = []

        def start_response(code, headers):
            _code.append(code)
            _headers.extend(headers)

        contents = b"".join(app(environ, start_response))
        return _code[0], _headers, contents

    def put(self, app, path, contents):
        environ = {
            "PATH_INFO": path,
            "REQUEST_METHOD": "PUT",
            "wsgi.input": BytesIO(contents),
        }
        setup_testing_defaults(environ)
        _code = []
        _headers = []

        def start_response(code, headers):
            _code.append(code)
            _headers.extend(headers)

        list(app(environ, start_response))
        return _code[0], _headers

    def propfind(self, app, path, body):
        environ = {
            "PATH_INFO": path,
            "REQUEST_METHOD": "PROPFIND",
            "CONTENT_TYPE": "text/xml",
            "wsgi.input": BytesIO(body),
        }
        setup_testing_defaults(environ)
        _code = []
        _headers = []

        def start_response(code, headers):
            _code.append(code)
            _headers.extend(headers)

        contents = b"".join(app(environ, start_response))
        return _code[0], _headers, contents

    def test_not_found(self):
        app = self.makeApp({}, [])
        code, headers, contents = self.get(app, "/.well-known/carddav")
        self.assertEqual("404 Not Found", code)

    def test_get_body(self):
        class TestResource(Resource):
            async def get_body(self):
                return [b"this is content"]

            def get_last_modified(self):
                raise KeyError

            def get_content_language(self):
                raise KeyError

            async def get_etag(self):
                return "myetag"

            def get_content_type(self):
                return "text/plain"

        app = self.makeApp({"/.well-known/carddav": TestResource()}, [])
        code, headers, contents = self.get(app, "/.well-known/carddav")
        self.assertEqual("200 OK", code)
        self.assertEqual(b"this is content", contents)

    def test_set_body(self):
        new_body = []

        class TestResource(Resource):
            def set_body(self, body, replace_etag=None):
                new_body.extend(body)

            async def get_etag(self):
                return '"blala"'

        app = self.makeApp({"/.well-known/carddav": TestResource()}, [])
        code, headers = self.put(app, "/.well-known/carddav", b"New contents")
        self.assertEqual("204 No Content", code)
        self.assertEqual([b"New contents"], new_body)

    def test_lock_not_allowed(self):
        app = self.makeApp({}, [])
        code, headers, contents = self.lock(app, "/resource")
        self.assertEqual("405 Method Not Allowed", code)
        self.assertIn(
            (
                "Allow",
                (
                    "DELETE, GET, HEAD, MKCOL, OPTIONS, "
                    "POST, PROPFIND, PROPPATCH, PUT, REPORT"
                ),
            ),
            headers,
        )
        self.assertEqual(b"", contents)

    def test_mkcol_ok(self):
        class Backend(object):
            def create_collection(self, relpath):
                pass

            def get_resource(self, relpath):
                return None

        app = WebDAVApp(Backend())
        code, headers, contents = self.mkcol(app, "/resource/bla")
        self.assertEqual("201 Created", code)
        self.assertEqual(b"", contents)

    def test_mkcol_exists(self):
        app = self.makeApp({"/resource": Resource(), "/resource/bla": Resource()}, [])
        code, headers, contents = self.mkcol(app, "/resource/bla")
        self.assertEqual("405 Method Not Allowed", code)
        self.assertEqual(b"", contents)

    def test_delete(self):
        class TestResource(Collection):
            async def get_etag(self):
                return '"foo"'

            def delete_member(unused_self, name, etag=None):
                self.assertEqual(name, "resource")

        app = self.makeApp({"/": TestResource(), "/resource": TestResource()}, [])
        code, headers, contents = self.delete(app, "/resource")
        self.assertEqual("204 No Content", code)
        self.assertEqual(b"", contents)

    def test_delete_not_found(self):
        class TestResource(Collection):
            pass

        app = self.makeApp({"/resource": TestResource()}, [])
        code, headers, contents = self.delete(app, "/resource")
        self.assertEqual("404 Not Found", code)
        self.assertTrue(contents.endswith(b"/resource not found."))

    def test_propfind_prop_does_not_exist(self):
        app = self.makeApp({"/resource": Resource()}, [])
        code, headers, contents = self.propfind(
            app,
            "/resource",
            b"""\
<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype /></d:prop></d:propfind>""",
        )
        self.assertMultiLineEqual(
            contents.decode("utf-8"),
            '<ns0:multistatus xmlns:ns0="DAV:"><ns0:response>'
            "<ns0:href>/resource</ns0:href>"
            "<ns0:propstat><ns0:status>HTTP/1.1 404 Not Found</ns0:status>"
            "<ns0:prop><ns0:resourcetype /></ns0:prop></ns0:propstat>"
            "</ns0:response></ns0:multistatus>",
        )
        self.assertEqual(code, "207 Multi-Status")

    def test_propfind_prop_not_present(self):
        class TestProperty(Property):
            name = "{DAV:}current-user-principal"

            async def get_value(self, href, resource, ret, environ):
                raise KeyError

        app = self.makeApp({"/resource": Resource()}, [TestProperty()])
        code, headers, contents = self.propfind(
            app,
            "/resource",
            b"""\
<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype /></d:prop></d:propfind>""",
        )
        self.assertMultiLineEqual(
            contents.decode("utf-8"),
            '<ns0:multistatus xmlns:ns0="DAV:"><ns0:response>'
            "<ns0:href>/resource</ns0:href>"
            "<ns0:propstat><ns0:status>HTTP/1.1 404 Not Found</ns0:status>"
            "<ns0:prop><ns0:resourcetype /></ns0:prop></ns0:propstat>"
            "</ns0:response></ns0:multistatus>",
        )
        self.assertEqual(code, "207 Multi-Status")

    def test_propfind_found(self):
        class TestProperty(Property):
            name = "{DAV:}current-user-principal"

            async def get_value(self, href, resource, ret, environ):
                ET.SubElement(ret, "{DAV:}href").text = "/user/"

        app = self.makeApp({"/resource": Resource()}, [TestProperty()])
        code, headers, contents = self.propfind(
            app,
            "/resource",
            b"""\
<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/>\
</d:prop></d:propfind>""",
        )
        self.assertMultiLineEqual(
            contents.decode("utf-8"),
            '<ns0:multistatus xmlns:ns0="DAV:"><ns0:response>'
            "<ns0:href>/resource</ns0:href>"
            "<ns0:propstat><ns0:status>HTTP/1.1 200 OK</ns0:status>"
            "<ns0:prop><ns0:current-user-principal><ns0:href>/user/</ns0:href>"
            "</ns0:current-user-principal></ns0:prop></ns0:propstat>"
            "</ns0:response></ns0:multistatus>",
        )
        self.assertEqual(code, "207 Multi-Status")

    def test_propfind_found_multi(self):
        class TestProperty1(Property):
            name = "{DAV:}current-user-principal"

            async def get_value(self, href, resource, el, environ):
                ET.SubElement(el, "{DAV:}href").text = "/user/"

        class TestProperty2(Property):
            name = "{DAV:}somethingelse"

            async def get_value(self, href, resource, el, environ):
                pass

        app = self.makeApp(
            {"/resource": Resource()}, [TestProperty1(), TestProperty2()]
        )
        code, headers, contents = self.propfind(
            app,
            "/resource",
            b"""\
<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/>\
<d:somethingelse/></d:prop></d:propfind>""",
        )
        self.maxDiff = None
        self.assertMultiLineEqual(
            contents.decode("utf-8"),
            '<ns0:multistatus xmlns:ns0="DAV:"><ns0:response>'
            "<ns0:href>/resource</ns0:href>"
            "<ns0:propstat><ns0:status>HTTP/1.1 200 OK</ns0:status>"
            "<ns0:prop><ns0:current-user-principal><ns0:href>/user/</ns0:href>"
            "</ns0:current-user-principal><ns0:somethingelse /></ns0:prop>"
            "</ns0:propstat></ns0:response></ns0:multistatus>",
        )
        self.assertEqual(code, "207 Multi-Status")

    def test_propfind_found_multi_status(self):
        class TestProperty(Property):
            name = "{DAV:}current-user-principal"

            async def get_value(self, href, resource, ret, environ):
                ET.SubElement(ret, "{DAV:}href").text = "/user/"

        app = self.makeApp({"/resource": Resource()}, [TestProperty()])
        code, headers, contents = self.propfind(
            app,
            "/resource",
            b"""\
<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/>\
<d:somethingelse/></d:prop></d:propfind>""",
        )
        self.maxDiff = None
        self.assertEqual(code, "207 Multi-Status")
        self.assertMultiLineEqual(
            contents.decode("utf-8"),
            """\
<ns0:multistatus xmlns:ns0="DAV:"><ns0:response><ns0:href>/resource</ns0:href>\
<ns0:propstat><ns0:status>HTTP/1.1 200 OK</ns0:status><ns0:prop>\
<ns0:current-user-principal><ns0:href>/user/</ns0:href>\
</ns0:current-user-principal></ns0:prop></ns0:propstat><ns0:propstat>\
<ns0:status>HTTP/1.1 404 Not Found</ns0:status><ns0:prop>\
<ns0:somethingelse /></ns0:prop></ns0:propstat>\
</ns0:response>\
</ns0:multistatus>""",
        )


class PickContentTypesTests(unittest.TestCase):
    def test_not_acceptable(self):
        self.assertRaises(
            webdav.NotAcceptableError,
            webdav.pick_content_types,
            [("text/plain", {})],
            ["text/html"],
        )
        self.assertRaises(
            webdav.NotAcceptableError,
            webdav.pick_content_types,
            [("text/plain", {}), ("text/html", {"q": "0"})],
            ["text/html"],
        )

    def test_highest_q(self):
        self.assertEqual(
            ["text/plain"],
            webdav.pick_content_types(
                [("text/html", {"q": "0.3"}), ("text/plain", {"q": "0.4"})],
                ["text/plain", "text/html"],
            ),
        )
        self.assertEqual(
            ["text/html", "text/plain"],
            webdav.pick_content_types(
                [("text/html", {}), ("text/plain", {"q": "1"})],
                ["text/plain", "text/html"],
            ),
        )

    def test_no_q(self):
        self.assertEqual(
            ["text/html", "text/plain"],
            webdav.pick_content_types(
                [("text/html", {}), ("text/plain", {})],
                ["text/plain", "text/html"],
            ),
        )

    def test_wildcard(self):
        self.assertEqual(
            ["text/plain"],
            webdav.pick_content_types(
                [("text/*", {"q": "0.3"}), ("text/plain", {"q": "0.4"})],
                ["text/plain", "text/html"],
            ),
        )
        self.assertEqual(
            set(["text/plain", "text/html"]),
            set(
                webdav.pick_content_types(
                    [("text/*", {"q": "0.4"}), ("text/plain", {"q": "0.3"})],
                    ["text/plain", "text/html"],
                )
            ),
        )
        self.assertEqual(
            ["application/html"],
            webdav.pick_content_types(
                [
                    ("application/*", {"q": "0.4"}),
                    ("text/plain", {"q": "0.3"}),
                ],
                ["text/plain", "application/html"],
            ),
        )


class ParseAcceptHeaderTests(unittest.TestCase):
    def test_parse(self):
        self.assertEqual([], webdav.parse_accept_header(""))
        self.assertEqual(
            [("text/plain", {"q": "0.1"})],
            webdav.parse_accept_header("text/plain; q=0.1"),
        )
        self.assertEqual(
            [("text/plain", {"q": "0.1"}), ("text/plain", {})],
            webdav.parse_accept_header("text/plain; q=0.1, text/plain"),
        )


class ETagMatchesTests(unittest.TestCase):
    def test_matches(self):
        self.assertTrue(webdav.etag_matches("etag1, etag2", "etag1"))
        self.assertFalse(webdav.etag_matches("etag3, etag2", "etag1"))
        self.assertFalse(webdav.etag_matches("etag1 etag2", "etag1"))
        self.assertFalse(webdav.etag_matches("etag1, etag2", None))
        self.assertTrue(webdav.etag_matches("*, etag2", "etag1"))
        self.assertTrue(webdav.etag_matches("*", "etag1"))
        self.assertFalse(webdav.etag_matches("*", None))


class PropstatByStatusTests(unittest.TestCase):
    def test_none(self):
        self.assertEqual({}, webdav.propstat_by_status([]))

    def test_one(self):
        self.assertEqual(
            {("200 OK", None): ["foo"]},
            webdav.propstat_by_status([webdav.PropStatus("200 OK", None, "foo")]),
        )

    def test_multiple(self):
        self.assertEqual(
            {
                ("200 OK", None): ["foo"],
                ("404 Not Found", "Cannot find"): ["bar"],
            },
            webdav.propstat_by_status(
                [
                    webdav.PropStatus("200 OK", None, "foo"),
                    webdav.PropStatus("404 Not Found", "Cannot find", "bar"),
                ]
            ),
        )


class PropstatAsXmlTests(unittest.TestCase):
    def test_none(self):
        self.assertEqual([], list(webdav.propstat_as_xml([])))

    def test_one(self):
        self.assertEqual(
            [
                b'<ns0:propstat xmlns:ns0="DAV:"><ns0:status>HTTP/1.1 200 '
                b"OK</ns0:status><ns0:prop><foo /></ns0:prop></ns0:propstat>"
            ],
            [
                ET.tostring(x)
                for x in webdav.propstat_as_xml(
                    [webdav.PropStatus("200 OK", None, ET.Element("foo"))]
                )
            ],
        )


class PathFromEnvironTests(unittest.TestCase):
    def test_ascii(self):
        self.assertEqual(
            "/bla",
            webdav.path_from_environ({"PATH_INFO": "/bla"}, "PATH_INFO"),
        )

    def test_recode(self):
        self.assertEqual(
            "/blü",
            webdav.path_from_environ({"PATH_INFO": "/bl\xc3\xbc"}, "PATH_INFO"),
        )
