# encoding: utf-8
#
# ** header v3.0
# This file is a part of the CaosDB Project.
#
# Copyright (C) 2018 Research Group Biomedical Physics,
# Max-Planck-Institute for Dynamics and Self-Organization Göttingen
# Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2020 Florian Spreckelsen <f.spreckelsen@indiscale.com>
#
# 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 <https://www.gnu.org/licenses/>.
#
# ** end header
#
"""Test different entity errors.

Created on 19.02.2015.

@author: tf

"""
import linkahead as db
from caosdb.exceptions import (AmbiguousEntityError,
                               EntityDoesNotExistError, EntityError,
                               EntityHasNoDatatypeError,
                               TransactionError, UniqueNamesError,
                               UnqualifiedParentsError,
                               UnqualifiedPropertiesError)
import pytest


def setup_function(function):
    try:
        db.execute_query("FIND ENTITY").delete()
    except BaseException:
        pass


def teardown_function(function):
    """Delete everything."""
    setup_function(function)


def test_retrieval_no_exception_raised():
    """Test whether retrieval fails but error is suppressed."""
    p = db.Property(name="TestNon-ExsistentProperty").retrieve(
        unique=True, raise_exception_on_error=False)
    assert not p.is_valid()
    assert (p.id is None or p.id < 0)


def test_retrieval_exception_raised():
    """Test if a TransactionError with the correct child is raised, and if
    the child has the correct super classes.

    """
    propname = "TestNon-ExistentProperty"
    with pytest.raises(TransactionError) as te:
        db.Property(name="TestNon-ExistentProperty").retrieve(unique=True,
                                                              raise_exception_on_error=True)
    assert len(te.value.errors) == 1
    ee = te.value.errors[0]
    # Check for type incl. inheritance
    assert isinstance(ee, EntityDoesNotExistError)
    assert isinstance(ee, EntityError)
    assert isinstance(ee, TransactionError)
    # Correct entity causing the error:
    assert ee.entity is not None
    assert ee.entity.name == propname
    assert not ee.entity.is_valid()
    assert ee.entity.has_errors()


@pytest.mark.xfail(reason=("Error treatment on server-side"
                           "has to be implemented first."))
def test_ambiguous_retrieval():
    """Test if a TransactionError containing an AmbiguousEntityError is
    raised correctly if there are two possible candidates.

    """
    db.RecordType(name="TestType").insert()
    db.Record(name="TestRec").add_parent(name="TestType").insert()
    # Insert twice, so unique=False
    db.Record(name="TestRec").add_parent(name="TestType").insert(unique=False)
    with pytest.raises(TransactionError) as te:
        db.Record(name="TestRec").retrieve()
    assert te.value.has_error(AmbiguousEntityError)
    assert te.value.errors[0].entity.name == "TestRec"


def test_insertion_no_exception_raised():
    """Test whether insertion fails but no error is raised."""
    p = db.Property(name="TestNoTypeProperty").insert(
        raise_exception_on_error=False)
    assert not p.is_valid()
    assert (p.id is None or p.id < 0)


def test_insertion_exception_raised():
    """Test insertion of a property with missing datatype."""
    p = db.Property(name="TestNoTypeProperty")
    with pytest.raises(TransactionError) as te:
        p.insert(raise_exception_on_error=True)
    assert te.value.has_error(EntityHasNoDatatypeError)


def test_insertion_with_invalid_parents():
    with pytest.raises(TransactionError) as te:
        p = db.Property(
            name="TestNoTypeProperty",
            datatype="Text").add_parent(
            id=-1)
        p.insert(raise_exception_on_error=True)
    upe = te.value.errors[0]
    print(upe)
    assert isinstance(upe, UnqualifiedParentsError)
    assert upe.entity is not None
    assert upe.entity.name == p.name
    assert upe.entity.id == p.id
    assert upe.entity.has_errors()
    assert not p.is_valid()
    assert not upe.entity.is_valid()
    assert upe.entities is not None


def test_insertion_with_invalid_properties():
    with pytest.raises(TransactionError) as te:
        p = db.Property(
            name="TestNoTypeProperty",
            datatype="Text").add_property(
            id=-1)
        p.insert(raise_exception_on_error=True)
    upe = te.value.errors[0]
    assert isinstance(upe, UnqualifiedPropertiesError)
    assert upe.entity is not None
    assert upe.entity.name == p.name
    assert upe.entity.has_errors()
    assert p.has_errors()
    assert not p.is_valid()
    assert not upe.entity.is_valid()


def test_entity_does_not_exist():
    """When retrieving a container with existing and non-existing
    entities, only those that don't exist should cause
    EntityDoesNotExistErrors.

    """
    p1 = db.Property(name="TestNon-ExistentProperty1").retrieve(
        raise_exception_on_error=False)
    p2 = db.Property(name="TestNon-ExistentProperty2").retrieve(
        raise_exception_on_error=False)
    p3 = db.Property(name="TestNon-ExistentProperty3").retrieve(
        raise_exception_on_error=False)
    # None of them should exist
    assert not p1.is_valid()
    assert (p1.id is None or p1.id < 0)
    assert not p2.is_valid()
    assert (p2.id is None or p2.id < 0)
    assert not p3.is_valid()
    assert (p3.id is None or p3.id < 0)

    pe = db.Property(name="TestExistentProperty", datatype="text").insert()

    c = db.Container().extend(
        [
            db.Property(
                name="TestNon-ExistentProperty1"),
            db.Property(
                name="TestNon-ExistentProperty2"),
            db.Property(
                name="TestNon-ExistentProperty3"),
            db.Property(
                name="TestExistentProperty")])

    with pytest.raises(TransactionError) as te:
        c.retrieve()
    te = te.value
    assert te.has_error(EntityDoesNotExistError)
    # Only non-existing entities caused the container error
    assert not pe.name in [x.name for x in te.all_entities]
    for p in (p1, p2, p3):
        assert p.name in [x.name for x in te.all_entities]


def test_insert_existent_entity():
    """Insertion of an already existing entity should cause a
    UniqueNamesError.

    """
    p1 = db.Property(name="TestNon-ExistentProperty1").retrieve(
        raise_exception_on_error=False)
    p2 = db.Property(name="TestNon-ExistentProperty2").retrieve(
        raise_exception_on_error=False)
    p3 = db.Property(name="TestNon-ExistentProperty3").retrieve(
        raise_exception_on_error=False)
    # None of them should exist
    assert not p1.is_valid()
    assert (p1.id is None or p1.id < 0)
    assert not p2.is_valid()
    assert (p2.id is None or p2.id < 0)
    assert not p3.is_valid()
    assert (p3.id is None or p3.id < 0)

    pe = db.Property(name="TestExistentProperty", datatype="text").insert()
    assert pe.is_valid()

    c = db.Container().extend(
        [
            db.Property(
                name="TestNon-ExistentProperty1",
                datatype="text"),
            db.Property(
                name="TestNon-ExistentProperty2",
                datatype="text"),
            db.Property(
                name="TestNon-ExistentProperty3",
                datatype="text"),
            db.Property(
                name="TestExistentProperty",
                datatype="text")])

    with pytest.raises(TransactionError) as te:
        c.insert(unique=True)
    te = te.value
    assert te.has_error(UniqueNamesError)
    une = te.errors[0]
    assert une.entity is not None
    assert pe.name == une.entity.name
    for p in (p1, p2, p3):
        assert p not in te.all_entities


def test_double_insertion():
    c1 = db.Container()

    c1.append(
        db.Property(
            name="TestSimpleTextProperty",
            description="simple text property (from test_error_stuff.py)",
            datatype='text'))
    c1.append(
        db.Property(
            name="TestSimpleDoubleProperty",
            description="simple double property (from test_error_stuff.py)",
            datatype='double'))
    c1.append(
        db.Property(
            name="TestSimpleIntegerProperty",
            description="simple integer property (from test_error_stuff.py)",
            datatype='integer'))
    c1.append(
        db.Property(
            name="TestSimpleDatetimeProperty",
            description="simple datetime property (from test_error_stuff.py)",
            datatype='datetime'))

    c1.append(
        db.RecordType(
            name="TestSimpleRecordType",
            description="simple recordType (from test_error_stuff.py)").add_property(
                name='TestSimpleTextProperty').add_property(
                    name='TestSimpleDoubleProperty').add_property(
                        name='TestSimpleIntegerProperty').add_property(
                            name='TestSimpleDatetimeProperty'))

    c1.insert()

    c2 = db.Container()
    c2.append(
        db.Property(
            name="TestSimpleTextProperty",
            description="simple text property (from test_error_stuff.py)",
            datatype='text'))
    c2.append(
        db.Property(
            name="TestSimpleDoubleProperty",
            description="simple double property (from test_error_stuff.py)",
            datatype='double'))
    c2.append(
        db.Property(
            name="TestSimpleIntegerProperty",
            description="simple integer property (from test_error_stuff.py)",
            datatype='integer'))
    c2.append(
        db.Property(
            name="TestSimpleDatetimeProperty",
            description="simple datetime property (from test_error_stuff.py)",
            datatype='datetime'))

    c2.append(
        db.RecordType(
            name="TestSimpleRecordType",
            description="simple recordType (from test_error_stuff.py)").add_property(
                name='TestSimpleTextProperty').add_property(
                    name='TestSimpleDoubleProperty').add_property(
                        name='TestSimpleIntegerProperty').add_property(
                            name='TestSimpleDatetimeProperty'))
    with pytest.raises(TransactionError) as te:
        c2.insert()
    te = te.value
    assert te.has_error(UniqueNamesError)
    # c2 caused the ContainerError
    assert te.container == c2
    # exactly 5 faulty entities in c2
    assert len(te.entities) == 5


def test_update_acl_errors():
    """Test the special cases of entity errors when using
    `Entity.update_acl`

    """
    rec_ne = db.Record("TestRecordNonExisting")

    with pytest.raises(TransactionError) as te:

        rec_ne.update_acl()

    assert te.value.has_error(EntityDoesNotExistError)
    assert te.value.errors[0].entity.name == rec_ne.name

    rt = db.RecordType(name="TestType").insert()
    rec = db.Record(name="TestRecord").add_parent(rt).insert()
    db.Record(name=rec.name).add_parent(rt).insert(unique=False)

    with pytest.raises(TransactionError) as te:

        db.Record(name=rec.name).update_acl()

    assert te.value.has_error(AmbiguousEntityError)


def test_URI_too_long():
    """Some tests for variours URI too long commands.

    See for example https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/180

    """

    short = 100
    uri_long = 819
    header_long = 815

    with pytest.raises(db.TransactionError) as excinfo:
        db.execute_query("0123456789" * short)
    assert "Parsing" in excinfo.value.msg

    with pytest.raises(db.HTTPURITooLongError) as excinfo:
        db.execute_query("0123456789" * uri_long)
    assert "414" in excinfo.value.msg

    with pytest.raises(db.HTTPURITooLongError) as excinfo:
        db.execute_query("0123456789" * header_long)
    assert "431" in excinfo.value.msg