# Copyright 2020 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

import asyncio
from dataclasses import asdict
from unittest.mock import patch
from urllib.parse import quote

import ldap3
import pytest
from faker import Faker

from ucsschool.kelvin.client import (
    InvalidRequest,
    NoObject,
    SchoolClass,
    SchoolClassResource,
    Session,
)

fake = Faker()


API_VERSION = "v1"
URL_BASE = "https://{host}/ucsschool/kelvin"
URL_TOKEN = f"{URL_BASE}/token"
URL_CLASS_RESOURCE = f"{URL_BASE}/{API_VERSION}/classes/"
URL_CLASS_COLLECTION = f"{URL_CLASS_RESOURCE}?school={{school}}"
URL_CLASS_OBJECT = f"{URL_CLASS_RESOURCE}{{school}}/{{name}}"
URL_SCHOOL_RESOURCE = f"{URL_BASE}/{API_VERSION}/schools/"
URL_SCHOOL_COLLECTION = URL_SCHOOL_RESOURCE
URL_SCHOOL_OBJECT = f"{URL_SCHOOL_COLLECTION}{{name}}"


@pytest.mark.asyncio
async def test_search_no_name_arg(
    compare_kelvin_obj_with_test_data,
    new_school,
    kelvin_session_kwargs,
    new_school_class,
):
    school = new_school(1)[0]
    school_name = school.name
    sc1_dn, sc1_attr = await new_school_class(school=school_name)
    sc2_dn, sc2_attr = await new_school_class(school=school_name)

    async with Session(**kelvin_session_kwargs) as session:
        objs = [
            obj async for obj in SchoolClassResource(session=session).search(school=school_name)
        ]

    assert objs, f"No SchoolClass in school {school!r} found."
    assert len(objs) >= 2
    assert sc1_dn in [sc.dn for sc in objs]
    assert sc2_dn in [sc.dn for sc in objs]
    for obj in objs:
        if obj.dn == sc1_dn:
            compare_kelvin_obj_with_test_data(obj, **sc1_attr)
        if obj.dn == sc2_dn:
            compare_kelvin_obj_with_test_data(obj, **sc2_attr)


@pytest.mark.asyncio
async def test_search_partial_name_arg(
    compare_kelvin_obj_with_test_data,
    new_school,
    kelvin_session_kwargs,
    new_school_class,
):
    school = new_school(1)[0]
    school_name = school.name
    # don't use usual name starting with 'test.', as leftovers of previous
    # tests will also match 'test.*'
    name = f"{fake.first_name()}{fake.first_name()}"
    sc1_dn, sc1_attr = await new_school_class(school=school_name, name=name)
    name_len = len(name)
    name_begin = name[: int(name_len / 2)]
    name_end = name[len(name_begin) :]

    async with Session(**kelvin_session_kwargs) as session:
        objs1 = [
            obj
            async for obj in SchoolClassResource(session=session).search(
                school=school_name, name=f"{name_begin}*"
            )
        ]
        objs2 = [
            obj
            async for obj in SchoolClassResource(session=session).search(
                school=school_name, name=f"*{name_end}"
            )
        ]
    assert objs1, f"No SchoolClass for school={school_name!r} and name='{name_begin}*' found."
    assert len(objs1) == 1
    assert sc1_dn == objs1[0].dn
    assert objs2, f"No SchoolClass for school={school_name!r} and name='*{name_end}' found."
    assert len(objs2) == 1
    assert sc1_dn == objs2[0].dn


@pytest.mark.asyncio
async def test_search_inexact_school(new_school, kelvin_session_kwargs):
    school = new_school(1)[0]
    school_name = school.name
    school_name_len = len(school_name)
    school_name_begin = school_name[: int(school_name_len / 2)]

    async with Session(**kelvin_session_kwargs) as session:
        with pytest.raises(InvalidRequest):
            assert [
                obj
                async for obj in SchoolClassResource(session=session).search(
                    school=f"{school_name_begin}*"
                )
            ]


@pytest.mark.asyncio
async def test_get_from_url(
    compare_kelvin_obj_with_test_data, kelvin_session_kwargs, new_school_class
):
    sc1_dn, sc1_attr = await new_school_class()
    school = sc1_attr["school"]
    name = sc1_attr["name"]
    url = URL_CLASS_OBJECT.format(host=kelvin_session_kwargs["host"], school=school, name=name)
    async with Session(**kelvin_session_kwargs) as session:
        obj = await SchoolClassResource(session=session).get_from_url(url)
    compare_kelvin_obj_with_test_data(obj, dn=sc1_dn, **sc1_attr)


@pytest.mark.asyncio
async def test_get_no_users(
    compare_kelvin_obj_with_test_data,
    kelvin_session_kwargs,
    new_school,
    new_school_class,
):
    sc_dn, sc_attr = await new_school_class()
    async with Session(**kelvin_session_kwargs) as session:
        obj = await SchoolClassResource(session=session).get(
            school=sc_attr["school"], name=sc_attr["name"]
        )
    compare_kelvin_obj_with_test_data(obj, dn=sc_dn, **sc_attr)


@pytest.mark.asyncio
async def test_get_with_users(
    compare_kelvin_obj_with_test_data,
    kelvin_session_kwargs,
    new_school,
    new_school_class,
    new_school_user,
):
    user1 = await new_school_user()
    user2 = await new_school_user(school=user1.school)
    sc_dn, sc_attr = await new_school_class(school=user1.school, users=[user1.name, user2.name])
    async with Session(**kelvin_session_kwargs) as session:
        obj = await SchoolClassResource(session=session).get(
            school=sc_attr["school"], name=sc_attr["name"]
        )
    assert set(obj.users) == {user1.name, user2.name}
    compare_kelvin_obj_with_test_data(obj, dn=sc_dn, **sc_attr)


@pytest.mark.parametrize("create_share", [True, False], ids=lambda x: f"create_share={x}")
@pytest.mark.asyncio
async def test_create(
    compare_kelvin_obj_with_test_data,
    kelvin_session_kwargs,
    ldap_access,
    mail_domain,
    new_school_class_test_obj,
    schedule_delete_obj,
    new_school_user,
    test_server_configuration,
    create_share,
):
    sc_data = new_school_class_test_obj()
    user1 = await new_school_user(school=sc_data.school)
    user2 = await new_school_user(school=sc_data.school)
    sc_data.users = [user1.name, user2.name]
    sc_data.create_share = create_share
    async with Session(**kelvin_session_kwargs) as session:
        sc_kwargs = asdict(sc_data)
        sc_obj = SchoolClass(session=session, **sc_kwargs)
        sc_obj.udm_properties = {"mailAddress": f"{fake.first_name()}@{mail_domain}"}
        schedule_delete_obj(object_type="class", school=sc_data.school, name=sc_data.name)
        await sc_obj.save()
        print(f"Created new SchoolClass: {sc_obj.as_dict()!r}")

    ldap_filter = f"(&(cn={sc_data.school}-{sc_data.name})(objectClass=ucsschoolGroup))"
    ldap_objs = await ldap_access.search(filter_s=ldap_filter)
    assert len(ldap_objs) == 1
    ldap_obj = ldap_objs[0]
    assert ldap_obj.entry_dn == sc_obj.dn
    assert ldap_obj["cn"].value == f"{sc_data.school}-{sc_data.name}"
    assert ldap_obj["ucsschoolRole"].value == f"school_class:school:{sc_data.school}"
    assert ldap_obj["description"].value == sc_obj.description
    assert set(ldap_obj["uniqueMember"].value) == {user1.dn, user2.dn}
    assert ldap_obj["mailPrimaryAddress"] == sc_obj.udm_properties["mailAddress"]
    share_ldap_filter = f"(&(cn={sc_data.school}-{sc_data.name})(objectClass=ucsschoolShare))"
    share_ldap_objs = await ldap_access.search(filter_s=share_ldap_filter)
    if create_share:
        assert len(share_ldap_objs) == 1
        share_ldap_obj = share_ldap_objs[0]
        assert share_ldap_obj["cn"].value == f"{sc_data.school}-{sc_data.name}"
    else:
        assert len(share_ldap_objs) == 0


@pytest.mark.asyncio
async def test_modify(
    compare_kelvin_obj_with_test_data,
    kelvin_session_kwargs,
    mail_domain,
    new_school_class,
    new_school_class_test_obj,
    test_server_configuration,
):
    sc1_dn, sc1_attr = await new_school_class(
        udm_properties={"mailAddress": f"{fake.first_name()}@{mail_domain}"}
    )
    new_data = asdict(new_school_class_test_obj())
    school = sc1_attr["school"]
    name = sc1_attr["name"]
    async with Session(**kelvin_session_kwargs) as session:
        sc_resource = SchoolClassResource(session=session)
        obj: SchoolClass = await sc_resource.get(school=school, name=name)
        compare_kelvin_obj_with_test_data(obj, dn=sc1_dn, **sc1_attr)
        for k, v in new_data.items():
            if k not in ("school", "name", "dn", "url", "ucsschool_roles"):
                setattr(obj, k, v)
        new_obj: SchoolClass = await obj.save()
        assert new_obj is obj
        assert new_obj.as_dict() == obj.as_dict()
        # load fresh object
        fresh_obj: SchoolClass = await sc_resource.get(school=school, name=name)
        assert fresh_obj.as_dict() == new_obj.as_dict()
        compare_kelvin_obj_with_test_data(fresh_obj, **obj.as_dict())


@pytest.mark.asyncio
async def test_move_change_name(
    compare_kelvin_obj_with_test_data,
    kelvin_session_kwargs,
    new_school_class,
    schedule_delete_obj,
):
    sc1_dn, sc1_attr = await new_school_class()
    school = sc1_attr["school"]
    old_name = sc1_attr["name"]
    new_name = fake.first_name()
    assert old_name != new_name
    async with Session(**kelvin_session_kwargs) as session:
        sc_resource = SchoolClassResource(session=session)
        obj: SchoolClass = await sc_resource.get(school=school, name=old_name)
        assert obj.name == old_name
        compare_kelvin_obj_with_test_data(obj, dn=sc1_dn, **sc1_attr)
        obj.name = new_name
        old_url = obj.url
        schedule_delete_obj(object_type="class", school=school, name=new_name)

        new_obj: SchoolClass = await obj.save()
        assert new_obj is obj
        assert new_obj.name == new_name
        assert new_obj.url != old_url
        # load fresh object
        fresh_obj: SchoolClass = await sc_resource.get(school=school, name=new_name)
        assert fresh_obj.name == new_name
        assert fresh_obj.url != old_url
        compare_kelvin_obj_with_test_data(fresh_obj, **new_obj.as_dict())


@pytest.mark.asyncio
async def test_move_change_school(
    compare_kelvin_obj_with_test_data,
    new_school,
    kelvin_session_kwargs,
    new_school_class,
    schedule_delete_obj,
):
    sc1_dn, sc1_attr = await new_school_class()
    old_school = sc1_attr["school"]
    school1, school2 = new_school(2)
    ou1, ou2 = school1.name, school2.name
    name = sc1_attr["name"]
    new_school_ = ou1 if old_school == ou2 else ou2
    assert old_school != new_school_
    async with Session(**kelvin_session_kwargs) as session:
        sc_resource = SchoolClassResource(session=session)
        obj: SchoolClass = await sc_resource.get(school=old_school, name=name)
        assert obj.school == old_school
        compare_kelvin_obj_with_test_data(obj, dn=sc1_dn, **sc1_attr)
        obj.school = new_school_
        schedule_delete_obj(object_type="class", school=new_school_, name=name)

        with pytest.raises(InvalidRequest) as exc_info:
            await obj.save()
        assert "Moving of class to other school is not allowed" in exc_info.value.args[0]


@pytest.mark.asyncio
async def test_delete(kelvin_session_kwargs, new_school_class):
    sc1_dn, sc1_attr = await new_school_class()
    school = sc1_attr["school"]
    name = sc1_attr["name"]
    async with Session(**kelvin_session_kwargs) as session:
        obj = await SchoolClassResource(session=session).get(school=school, name=name)
        assert obj
        res = await obj.delete()
        assert res is None

    async with Session(**kelvin_session_kwargs) as session:
        with pytest.raises(NoObject):
            await SchoolClassResource(session=session).get(school=school, name=name)


@pytest.mark.asyncio
async def test_reload(
    compare_kelvin_obj_with_test_data,
    kelvin_session_kwargs,
    ldap_access,
    new_school_class,
):
    sc1_dn, sc1_attr = await new_school_class()
    school = sc1_attr["school"]
    name = sc1_attr["name"]
    description_old = sc1_attr["description"]
    description_new = fake.text(max_nb_chars=50)

    async with Session(**kelvin_session_kwargs) as session:
        obj: SchoolClass = await SchoolClassResource(session=session).get(school=school, name=name)
        assert obj.description == description_old
        await obj.reload()
        assert obj.description == description_old
        await ldap_access.modify(
            obj.dn, {"description": [(ldap3.MODIFY_REPLACE, [description_new])]}
        )
        await obj.reload()
        assert obj.description == description_new


@pytest.mark.asyncio
async def test_exists(kelvin_session_kwargs, new_school_class):
    sc1_dn, sc1_attr = await new_school_class()
    async with Session(**kelvin_session_kwargs) as session:
        assert await SchoolClassResource(session=session).exists(
            name=sc1_attr["name"], school=sc1_attr["school"]
        )
        assert not await SchoolClassResource(session=session).exists(
            name="DOESNOTEXIST", school=sc1_attr["school"]
        )
        with patch("ucsschool.kelvin.client.session.Session.head") as mock:
            f = asyncio.Future()
            f.set_result(True)
            mock.return_value = f
            await SchoolClassResource(session=session).exists(
                name=sc1_attr["name"], school=sc1_attr["school"]
            )
            mock.assert_called_once()


@pytest.mark.parametrize("quoted", (True, False))
def test__from_kelvin_response_unquote(quoted: bool):
    expected_school = "☁"
    url_school = quote(expected_school) if quoted else expected_school
    expected_users = ["☃", "☼", "AÇIKGÜN", "ÇAĞACAN"]
    url_users = [quote(user) if quoted else user for user in expected_users]
    test_response = {
        "name": "Democlass",
        "school": f"https://dummy.fqdn/ucsschool/kelvin/v1/schools/{url_school}",
        "users": [f"https://dummy.fqdn/ucsschool/kelvin/v1/users/{user}" for user in url_users],
    }
    sc = SchoolClass._from_kelvin_response(response=test_response)
    assert sc.users == expected_users
    assert sc.school == expected_school
