# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2023 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import base64
import os
import re
from unittest import mock
from unittest.mock import Mock, call

import py
import pytest
import responses
from cmislib import CmisClient
from cmislib.exceptions import (
    CmisException,
    InvalidArgumentException,
    ObjectNotFoundException,
    PermissionDeniedException,
    UpdateConflictException,
)
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.encoding import force_bytes, force_str

from passerelle.apps.cmis.models import CmisConnector
from passerelle.base.models import AccessRight, ApiUser
from tests.test_manager import login


def b64encode(content):
    return force_str(base64.b64encode(force_bytes(content)))


@pytest.fixture()
def setup(db):
    api = ApiUser.objects.create(username='all', keytype='', key='')
    conn = CmisConnector.objects.create(
        cmis_endpoint='http://example.com/cmisatom', username='admin', password='admin', slug='slug-cmis'
    )
    obj_type = ContentType.objects.get_for_model(conn)
    AccessRight.objects.create(
        codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=conn.pk
    )
    return conn


def test_uploadfile(app, setup, tmpdir, monkeypatch):
    class FakeCMISGateway:
        def __init__(self, *args, **kwargs):
            pass

        def create_doc(
            self,
            file_name,
            file_path,
            file_byte_content,
            content_type=None,
            object_type=None,
            properties=None,
        ):
            assert content_type == "image/jpeg"
            with open(file_name, 'wb') as f:
                f.write(file_byte_content)
                return Mock(properties={"toto": "tata"})

    file_name = "testfile.whatever"
    file_content = 'aaaa'
    monkeypatch.chdir(tmpdir)
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CMISGateway', FakeCMISGateway)
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": file_name, "content": b64encode(file_content), "content_type": "image/jpeg"},
        },
    )
    result_file = py.path.local(file_name)
    assert result_file.exists()
    with result_file.open('rb'):
        assert result_file.read() == file_content
    json_result = response.json
    assert json_result['err'] == 0
    assert json_result['data']['properties'] == {"toto": "tata"}

    file_name_overwrite = "testfile.whatever.overwrite"
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": file_name, "content": b64encode(file_content), "content_type": "image/jpeg"},
            "filename": file_name_overwrite,
        },
    )
    result_file = py.path.local(file_name_overwrite)
    assert result_file.exists()
    with result_file.open('rb'):
        assert result_file.read() == file_content
    json_result = response.json
    assert json_result['err'] == 0
    assert json_result['data']['properties'] == {"toto": "tata"}


def test_upload_file_metadata(app, setup, monkeypatch):
    class FakeFolder:
        def createDocument(self, filename, contentFile, properties, contentType=None):
            return Mock(properties=properties)

    from passerelle.apps.cmis.models import CMISGateway

    monkeypatch.setattr(CMISGateway, '_get_or_create_folder', lambda x, y: FakeFolder())
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": "bla", "content": b64encode('bla')},
            "object_type": "D:dui:type",
            "properties": {
                "cmis:description": "Coucou",
                "dui:tnumDossier": "42",
            },
            "properties/dui:ttypeStructure": "Accueil de loisirs",
        },
    )
    assert response.json['data']['properties'] == {
        "cmis:objectTypeId": "D:dui:type",
        "cmis:description": "Coucou",
        "dui:tnumDossier": "42",
        "dui:ttypeStructure": "Accueil de loisirs",
    }


def test_uploadfile_error_if_no_file_name(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"content": b64encode('aaaa'), "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'].startswith('"filename" or "file[\'filename\']" is required')


def test_uploadfile_error_if_non_string_file_name(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": 1, "content": b64encode('aaaa'), "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'] == "file/filename: 1 is not of type 'string'"

    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"content": b64encode('aaaa'), "content_type": "image/jpeg"},
            "filename": 1,
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'] == "filename: 1 is not of type 'string'"


def test_uploadfile_error_if_non_valid_file_name(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": ",.,", "content": b64encode('aaaa'), "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert "',.,' does not match " in response.json['err_desc']

    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"content": b64encode('aaaa'), "content_type": "image/jpeg"},
            "filename": ",.,",
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert "',.,' does not match " in response.json['err_desc']


def test_uploadfile_error_if_no_path(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "file": {"filename": 'somefile.txt', "content": b64encode('aaaa'), "content_type": "image/jpeg"}
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'] == "'path' is a required property"


def test_uploadfile_error_if_non_string_path(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": 1,
            "file": {"filename": 'somefile.txt', "content": b64encode('aaaa'), "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'] == "path: 1 is not of type 'string'"


def test_uploadfile_error_if_no_regular_path(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "no/leading/slash",
            "file": {"filename": 'somefile.txt', "content": b64encode('aaaa'), "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert "'no/leading/slash' does not match " in response.json['err_desc']


def test_uploadfile_error_if_no_file_content(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": 'somefile.txt', "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'] == "file: 'content' is a required property"


def test_uploadfile_error_if_non_string_file_content(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": 'somefile.txt', "content": 1, "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'] == "file/content: 1 is not of type 'string'"


def test_uploadfile_error_if_no_proper_base64_encoding(app, setup):
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": 'somefile.txt', "content": "1", "content_type": "image/jpeg"},
        },
        expect_errors=True,
    )
    assert response.status_code == 400
    assert response.json['err'] == 1
    assert response.json['err_desc'].startswith('"file[\'content\']" must be a valid base64 string')


def test_uploadfile_cmis_gateway_error(app, setup, monkeypatch):
    from passerelle.utils.jsonresponse import APIError

    cmis_gateway = Mock()
    cmis_gateway.create_doc.side_effect = APIError("some error")
    cmis_gateway_cls = Mock(return_value=cmis_gateway)
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CMISGateway', cmis_gateway_cls)
    response = app.post_json(
        '/cmis/slug-cmis/uploadfile',
        params={
            "path": "/some/folder/structure",
            "file": {"filename": "file_name", "content": b64encode('aaaa'), "content_type": "image/jpeg"},
        },
    )
    assert response.json['err'] == 1
    assert response.json['err_desc'].startswith("some error")


def test_get_or_create_folder_already_existing(monkeypatch):
    default_repository = Mock()
    default_repository.getObjectByPath.return_value = 'folder'
    cmis_client_cls = Mock(return_value=Mock(spec=CmisClient, defaultRepository=default_repository))
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CmisClient', cmis_client_cls)
    gateway = passerelle.apps.cmis.models.CMISGateway('cmis_endpoint', 'user', 'pass', Mock())
    assert gateway._get_or_create_folder('/whatever') == 'folder'
    default_repository.getObjectByPath.assert_has_calls([call('/whatever')])


def test_get_or_create_folder_one_level_creation(monkeypatch):
    root_folder = Mock()
    root_folder.createFolder.return_value = 'folder'
    default_repository = Mock(
        rootFolder=root_folder, **{'getObjectByPath.side_effect': ObjectNotFoundException()}
    )
    cmis_client_cls = Mock(return_value=Mock(spec=CmisClient, defaultRepository=default_repository))
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CmisClient', cmis_client_cls)
    gateway = passerelle.apps.cmis.models.CMISGateway('cmis-url', 'user', 'password', Mock())
    assert gateway._get_or_create_folder('/whatever') == 'folder'
    default_repository.getObjectByPath.assert_has_calls([call('/whatever'), call('/whatever')])
    root_folder.createFolder.assert_called_once_with('whatever')


def test_get_or_create_folder_two_level_creation(monkeypatch):
    whatever_folder = Mock()
    whatever_folder.createFolder.return_value = 'folder'
    root_folder = Mock()
    root_folder.createFolder.return_value = whatever_folder
    default_repository = Mock(rootFolder=root_folder)
    default_repository.getObjectByPath.side_effect = ObjectNotFoundException()
    cmis_client_cls = Mock(return_value=Mock(spec=CmisClient, defaultRepository=default_repository))
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CmisClient', cmis_client_cls)
    gateway = passerelle.apps.cmis.models.CMISGateway('cmis_url', 'user', 'password', Mock())
    assert gateway._get_or_create_folder('/whatever/man') == 'folder'
    default_repository.getObjectByPath.assert_has_calls(
        [call('/whatever/man'), call('/whatever'), call('/whatever/man')]
    )
    root_folder.createFolder.assert_called_once_with('whatever')
    whatever_folder.createFolder.assert_called_once_with('man')


def test_get_or_create_folder_with_some_existing_and_some_not(monkeypatch):
    whatever_folder = Mock()
    whatever_folder.createFolder.return_value = 'folder'

    def getObjectByPath(path):
        if path == '/whatever':
            return whatever_folder
        elif path == '/whatever/man':
            raise ObjectNotFoundException()
        else:
            raise Exception("I should not be called with: %s" % path)

    root_folder = Mock()
    default_repository = Mock(rootFolder=root_folder)
    default_repository.getObjectByPath.side_effect = getObjectByPath
    cmis_client_cls = Mock(return_value=Mock(spec=CmisClient, defaultRepository=default_repository))
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CmisClient', cmis_client_cls)
    gateway = passerelle.apps.cmis.models.CMISGateway('cmis_url', 'user', 'password', Mock())
    assert gateway._get_or_create_folder('/whatever/man') == 'folder'
    root_folder.createFolder.assert_not_called()
    whatever_folder.createFolder.assert_called_once_with('man')


def test_create_doc():
    from passerelle.apps.cmis.models import CMISGateway

    gateway = CMISGateway('cmis_url', 'user', 'password', Mock())
    folder = Mock()
    folder.createDocument.return_value = 'doc'
    gateway._get_or_create_folder = Mock(return_value=folder)
    assert gateway.create_doc('filename', '/some/path', b'file_content') == 'doc'
    gateway._get_or_create_folder.assert_called_once_with('/some/path')
    args, kwargs = folder.createDocument.call_args
    assert args[0] == 'filename'
    content_file = kwargs['contentFile']
    assert content_file.read() == b'file_content'


@pytest.mark.parametrize(
    "cmis_exc,err_msg",
    [
        (PermissionDeniedException, "permission denied"),
        (UpdateConflictException, "update conflict"),
        (InvalidArgumentException, "invalid property"),
        (CmisException, "cmis binding error"),
    ],
)
def test_wrap_cmis_error(app, setup, monkeypatch, cmis_exc, err_msg):
    from passerelle.apps.cmis.models import wrap_cmis_error
    from passerelle.utils.jsonresponse import APIError

    @wrap_cmis_error
    def dummy_func():
        raise cmis_exc("some error")

    with pytest.raises(APIError) as excinfo:
        dummy_func()
    assert str(excinfo.value).startswith(err_msg)


def test_re_file_path():
    from passerelle.apps.cmis.models import FILE_PATH_PATTERN

    RE_FILE_PATH = re.compile(FILE_PATH_PATTERN)
    assert RE_FILE_PATH.match('/')
    assert RE_FILE_PATH.match('/some')
    assert RE_FILE_PATH.match('/some/path')
    assert RE_FILE_PATH.match('/SOME/PATH')
    assert RE_FILE_PATH.match('/some/long/path')
    assert RE_FILE_PATH.match('/some/digits/12/and/CAPITALS')
    assert RE_FILE_PATH.match('/some/!#$%&+-^_`~;[]{}+=~')
    assert not RE_FILE_PATH.match('/trailing/slash/')
    assert not RE_FILE_PATH.match('no/leading/slash')
    assert not RE_FILE_PATH.match('/multiple//slash')
    assert not RE_FILE_PATH.match('')


def test_re_file_name():
    from passerelle.apps.cmis.models import FILE_NAME_PATTERN

    RE_FILE_NAME = re.compile(FILE_NAME_PATTERN)
    assert RE_FILE_NAME.match('toto.tata')
    assert RE_FILE_NAME.match('TOTO.TATA')


def test_cmis_types_view(setup, app, admin_user, monkeypatch):
    class FakeCmisType:
        class FakeCmisProperty:
            def __init__(self, id):
                self.id = id
                self.description = 'cmis:prop'
                self.propertyType = 'string'
                self.required = True

        def __init__(self, id, children=None):
            self.id = id
            self.description = 'hop'
            prop = self.FakeCmisProperty('cmis:prop')
            prop2 = self.FakeCmisProperty('cmis:prop2')
            self.properties = {prop.id: prop, prop2.id: prop2}
            self.children = children or []

    class FakeCmisRepo:
        def __init__(self, root_types):
            self.root_types = root_types

        def getTypeDefinition(self, id):
            if id in self.root_types:
                return self.root_types[id]
            for type in self.root_types.values():
                for child in type.children:
                    if child.id == id:
                        return child
            raise ObjectNotFoundException

        def getTypeDefinitions(self):
            return self.root_types.values()

        def getTypeChildren(self, id):
            return self.getTypeDefinition(id).children

    children = [FakeCmisType('cmis:child1'), FakeCmisType('cmis:child2')]
    root_type1 = FakeCmisType('cmis:root1', children=children)
    root_type2 = FakeCmisType('cmis:root2')
    root_types = {root_type1.id: root_type1, root_type2.id: root_type2}
    repo = FakeCmisRepo(root_types)

    cmis_client_cls = Mock(return_value=Mock(spec=CmisClient, defaultRepository=repo))
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CmisClient', cmis_client_cls)
    app = login(app)
    resp = app.get('/cmis/slug-cmis/')

    resp = resp.click('Explore available object types')
    assert all(id in resp.text for id in root_types)
    assert 'Back to' not in resp.text
    assert 'Children' not in resp.text
    assert 'Properties' not in resp.text

    resp = resp.click(root_type1.id)
    assert all(id in resp.text for id in root_type1.properties)
    assert all(child.id in resp.text for child in root_type1.children)

    resp = resp.click(children[0].id)
    assert "No more children." in resp.text

    resp = resp.click("Back to base types list")
    resp = resp.click(root_type2.id)
    assert "No more children." in resp.text

    resp = app.get('/manage/cmis/slug-cmis/type?id=wrong', status=404)


@pytest.mark.parametrize('debug', (False, True))
@responses.activate
def test_raw_uploadfile(app, setup, debug, caplog):
    """ Simulate the bellow bash query :
    $ http https://passerelle.dev.publik.love/cmis/ged/uploadfile \
           file:='{"filename": "test2", "content": "c2FsdXQK"}' path=/test-eo
    """
    caplog.set_level('DEBUG')
    file_name = "test2"
    file_content = 'salut\n'
    path = "/test-eo"
    url = reverse(
        'generic-endpoint', kwargs={'connector': 'cmis', 'endpoint': 'uploadfile', 'slug': setup.slug}
    )
    if debug:
        setup.set_log_level('DEBUG')

    with open('%s/tests/data/cmis/cmis1.out.xml' % os.getcwd(), 'rb') as fd:
        cmis1_body = fd.read()

    with open('%s/tests/data/cmis/cmis2.out.xml' % os.getcwd(), 'rb') as fd:
        cmis2_body = fd.read()

    with open('%s/tests/data/cmis/cmis3.out.xml' % os.getcwd(), 'rb') as fd:
        cmis3_body = fd.read()

    responses.add(responses.GET, 'http://example.com/cmisatom', body=cmis1_body, status=200)

    responses.add(responses.GET, 'http://example.com/cmisatom/test/path', body=cmis2_body, status=200)

    responses.add(responses.POST, 'http://example.com/cmisatom/test/children', body=cmis3_body, status=200)

    params = {
        "path": path,
        "file": {"filename": file_name, "content": b64encode(file_content), "content_type": "image/jpeg"},
    }
    response = app.post_json(url, params=params)
    json_result = response.json
    assert json_result['err'] == 0
    assert json_result['data']['properties']['cmis:objectTypeId'] == "cmis:document"
    assert json_result['data']['properties']['cmis:name'] == file_name

    assert not any('cmislib' in record.name for record in caplog.records)


def test_cmis_check_status(app, setup, monkeypatch):
    cmis_gateway = Mock()
    type(cmis_gateway).repo = mock.PropertyMock(side_effect=CmisException)
    cmis_gateway_cls = Mock(return_value=cmis_gateway)
    import passerelle.apps.cmis.models

    monkeypatch.setattr(passerelle.apps.cmis.models, 'CMISGateway', cmis_gateway_cls)

    with pytest.raises(CmisException):
        setup.check_status()


@responses.activate
def test_get_file(app, setup):
    url = (
        reverse('generic-endpoint', kwargs={'connector': 'cmis', 'endpoint': 'getfile', 'slug': setup.slug})
        + '?raise=1'
    )

    with open('%s/tests/data/cmis/cmis1.out.xml' % os.getcwd(), 'rb') as fd:
        cmis1_body = fd.read()

    with open('%s/tests/data/cmis/cmis3.out.xml' % os.getcwd(), 'rb') as fd:
        cmis3_body = fd.read()

    responses.add(responses.GET, 'http://example.com/cmisatom', body=cmis1_body, status=200)

    responses.add(responses.GET, 'http://example.com/cmisatom/test/path', body=cmis3_body, status=200)

    responses.add(responses.GET, 'http://example.com/cmisatom/test/id', body=cmis3_body, status=200)

    responses.add(
        responses.GET,
        'http://example.com/cmisatom/test/content/test2?id=L3Rlc3QtZW8vdGVzdDI%3D',
        body=b'hello world',
        status=200,
    )

    response = app.get(url, params={'object_id': '/test/file'})
    assert response.content_type == 'application/octet-stream'
    assert response.content == b'hello world'

    response = app.get(url, params={'object_id': 'c4bc9d00-5bf0-404d-8f0a-a6260f6d21ae;1.0'})
    assert response.content_type == 'application/octet-stream'
    assert response.content == b'hello world'


@responses.activate
def test_get_metadata(app, setup):
    url = reverse(
        'generic-endpoint', kwargs={'connector': 'cmis', 'endpoint': 'getmetadata', 'slug': setup.slug}
    )

    with open('%s/tests/data/cmis/cmis1.out.xml' % os.getcwd(), 'rb') as fd:
        cmis1_body = fd.read()

    with open('%s/tests/data/cmis/cmis3.out.xml' % os.getcwd(), 'rb') as fd:
        cmis3_body = fd.read()

    responses.add(responses.GET, 'http://example.com/cmisatom', body=cmis1_body, status=200)

    responses.add(responses.GET, 'http://example.com/cmisatom/test/path', body=cmis3_body, status=200)

    responses.add(responses.GET, 'http://example.com/cmisatom/test/id', body=cmis3_body, status=200)

    responses.add(
        responses.GET,
        'http://example.com/cmisatom/test/content/test2?id=L3Rlc3QtZW8vdGVzdDI%3D',
        body=b'hello world',
        status=200,
    )

    response = app.get(url, params={'object_id': 'c4bc9d00-5bf0-404d-8f0a-a6260f6d21ae;1.0'})
    assert response.json['data']['cmis']['contentStreamFileName'] == 'test2'
    assert response.json['data']['rsj']['idInsertis'] == '21N284563'

    response = app.get(url, params={'object_id': '/test/file'})
    assert response.json['data']['cmis']['contentStreamFileName'] == 'test2'
    assert response.json['data']['rsj']['idInsertis'] == '21N284563'
