#
# This file is a part of the CaosDB Project.
#
# Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
# Copyright (C) 2022 Florian Spreckelsen <f.spreckelsen@indiscale.com>
# Copyright (C) 2022 Daniel Hornung <d.hornung@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/>.
#

# @review Daniel Hornung 2022-02-18

import os
import pytest

import caosdb as db
from caosadvancedtools.models.parser import (parse_model_from_json_schema,
                                             JsonSchemaDefinitionError)

FILEPATH = os.path.join(os.path.dirname(
    os.path.abspath(__file__)), 'json-schema-models')


def test_rt_with_string_properties():
    """Test datamodel parsing of datamodel_string_properties.schema.json"""
    # @author Florian Spreckelsen
    # @date 2022-02-17

    model = parse_model_from_json_schema(
        os.path.join(FILEPATH,
                     "datamodel_string_properties.schema.json"))
    assert "Dataset" in model
    dataset_rt = model["Dataset"]
    assert isinstance(dataset_rt, db.RecordType)
    assert dataset_rt.name == "Dataset"
    assert dataset_rt.description == ""
    assert len(dataset_rt.get_properties()) == 4

    assert dataset_rt.get_property("title") is not None
    assert dataset_rt.get_property("campaign") is not None
    assert dataset_rt.get_property("method") is not None

    assert dataset_rt.get_property("The title") is not None
    assert dataset_rt.get_property("titled") is None

    title_prop = dataset_rt.get_property("title")
    assert title_prop.datatype == db.TEXT
    assert dataset_rt.get_importance(title_prop.name) == db.OBLIGATORY

    campaign_prop = dataset_rt.get_property("campaign")
    assert campaign_prop.datatype == db.TEXT
    assert dataset_rt.get_importance(campaign_prop.name) == db.RECOMMENDED

    method_prop = dataset_rt.get_property("method")
    assert method_prop.datatype == db.TEXT
    assert dataset_rt.get_importance(method_prop.name) == db.RECOMMENDED


def test_datamodel_with_atomic_properties():
    """Test read-in of two separate record types with atomic-typed properties."""
    # @author Florian Spreckelsen
    # @date 2022-02-18

    model = parse_model_from_json_schema(os.path.join(
        FILEPATH, "datamodel_atomic_properties.schema.json"))
    assert "Dataset1" in model
    assert "Dataset2" in model

    rt1 = model["Dataset1"]
    assert isinstance(rt1, db.RecordType)
    assert rt1.name == "Dataset1"
    assert rt1.description == "Some description"
    assert len(rt1.get_properties()) == 3

    assert rt1.get_property("title") is not None
    assert rt1.get_property("campaign") is not None
    assert rt1.get_property("number_prop") is not None

    title_prop = rt1.get_property("title")
    assert title_prop.datatype == db.TEXT
    assert rt1.get_importance(title_prop.name) == db.OBLIGATORY

    campaign_prop = rt1.get_property("campaign")
    assert campaign_prop.datatype == db.TEXT
    assert rt1.get_importance(campaign_prop.name) == db.RECOMMENDED

    float_prop = rt1.get_property("number_prop")
    assert float_prop.datatype == db.DOUBLE
    assert rt1.get_importance(float_prop.name) == db.OBLIGATORY

    rt2 = model["Dataset2"]
    assert isinstance(rt2, db.RecordType)
    assert rt2.name == "Dataset2"
    assert not rt2.description
    assert len(rt2.get_properties()) == 5

    date_prop = rt2.get_property("date")
    assert date_prop.datatype == db.DATETIME

    datetime_prop = rt2.get_property("date_time")
    assert date_prop.datatype == db.DATETIME

    int_prop = rt2.get_property("integer")
    assert int_prop.datatype == db.INTEGER
    assert int_prop.description == "Some integer property"

    bool_prop = rt2.get_property("boolean")
    assert bool_prop.datatype == db.BOOLEAN

    float_prop2 = rt2.get_property("number_prop")
    assert float_prop.datatype == float_prop2.datatype


def test_required_no_list():
    """Exception must be raised when "required" is not a list."""
    # @author Daniel Hornung
    # @date 2022-02-18

    with pytest.raises(JsonSchemaDefinitionError) as err:
        parse_model_from_json_schema(
            os.path.join(FILEPATH,
                         "datamodel_required_no_list.schema.json"))
    assert "'Dataset' is not of type 'array'" in str(err.value)


def test_missing_property_type():
    """Exception must be raised when "type" is missing."""
    with pytest.raises(JsonSchemaDefinitionError) as err:
        parse_model_from_json_schema(
            os.path.join(FILEPATH,
                         "datamodel_missing_property_type.schema.json"))
    assert "`type` is missing" in str(err.value)


def test_enum():
    """Enums are represented in references to records of a specific type."""
    # @author Florian Spreckelsen
    # @date 2022-03-16

    model = parse_model_from_json_schema(os.path.join(
        FILEPATH, "datamodel_enum_prop.schema.json"))
    licenses = ["CC-BY", "CC-BY-SA", "CC0", "restricted access"]
    for name in ["Dataset", "license"] + licenses:
        assert name in model

    assert isinstance(model["Dataset"], db.RecordType)
    assert model["Dataset"].get_property("license") is not None
    assert model["Dataset"].get_property("license").is_reference()
    assert model["Dataset"].get_property("license").datatype.name == "license"
    assert isinstance(model["license"], db.RecordType)

    for name in licenses:
        assert isinstance(model[name], db.Record)
        assert model[name].name == name
        assert len(model[name].parents) == 1
        assert model[name].has_parent(model["license"])

    # Also allow enums with non-string types
    number_enums = ["1.1", "2.2", "3.3"]
    for name in ["number_enum"] + number_enums:
        assert name in model

    assert isinstance(model["number_enum"], db.RecordType)
    assert model["Dataset"].get_property("number_enum") is not None
    assert model["Dataset"].get_property("number_enum").is_reference()
    assert model["Dataset"].get_property(
        "number_enum").datatype.name == "number_enum"

    for name in number_enums:
        assert isinstance(model[name], db.Record)
        assert model[name].name == name
        assert len(model[name].parents) == 1
        assert model[name].has_parent(model["number_enum"])


@pytest.mark.xfail(reason="Don't allow integer enums until https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/224 has been fixed")
def test_int_enum():
    """Check an enum property with type: integer"""
    # @author Florian Spreckelsen
    # @date 2022-03-22

    model = parse_model_from_json_schema(os.path.join(
        FILEPATH, "datamodel_int_enum_broken.schema.json"))
    int_enums = ["1", "2", "3"]
    for name in ["Dataset", "int_enum"] + int_enums:
        assert name in model

    assert isinstance(model["Dataset"], db.RecordType)
    assert model["Dataset"].get_property("int_enum") is not None
    assert model["Dataset"].get_property("int_enum").is_reference
    assert model["Dataset"].get_property(
        "int_enum").datatype.name == "int_enum"
    assert isinstance(model["int_enum"], db.RecordType)

    for name in int_enums:
        assert isinstance(model[name], db.Record)
        assert model[name].name == name
        assert len(model[name].parents) == 1
        assert model[name].has_parent(model["int_enum"])


def test_references():
    """Test reference properties"""
    # @author Florian Spreckelsen
    # @date 2022-03-17

    model = parse_model_from_json_schema(os.path.join(
        FILEPATH, "datamodel_references.schema.json"))
    for name in ["Dataset", "event", "longitude", "latitude", "location"]:
        assert name in model

    assert isinstance(model["Dataset"], db.RecordType)
    assert model["Dataset"].get_property("event") is not None
    assert model["Dataset"].get_importance("event") == db.OBLIGATORY
    assert model["Dataset"].get_property("event").is_reference()
    assert model["Dataset"].get_property("event").datatype.name == "event"

    assert isinstance(model["event"], db.RecordType)
    assert model["event"].get_property("longitude") is not None
    assert model["event"].get_importance("longitude") == db.OBLIGATORY
    assert model["event"].get_property("longitude").datatype == db.DOUBLE

    assert model["event"].get_property("latitude") is not None
    assert model["event"].get_importance("latitude") == db.OBLIGATORY
    assert model["event"].get_property("latitude").datatype == db.DOUBLE

    assert model["event"].get_property("location") is not None
    assert model["event"].get_importance("location") == db.RECOMMENDED
    assert model["event"].get_property("location").datatype == db.TEXT

    assert isinstance(model["longitude"], db.Property)
    assert model["longitude"].datatype == db.DOUBLE

    assert isinstance(model["latitude"], db.Property)
    assert model["latitude"].datatype == db.DOUBLE

    assert isinstance(model["location"], db.Property)
    assert model["location"].datatype == db.TEXT
    assert model["location"].description == "geographical location (e.g., North Sea; Espoo, Finland)"


def test_list():
    """Test list properties with all possible datatypes."""
    # @author Florian Spreckelsen
    # @date 2022-03-17

    model = parse_model_from_json_schema(os.path.join(
        FILEPATH, "datamodel_list_properties.schema.json"))
    licenses = ["CC-BY", "CC-BY-SA", "CC0", "restricted access"]
    names = ["Dataset", "keywords", "booleans", "integers", "floats",
             "datetimes", "dates", "reference", "reference_with_name", "event",
             "license"]
    for name in names + licenses:
        assert name in model

    dataset_rt = model["Dataset"]
    assert dataset_rt.get_property("keywords") is not None
    assert dataset_rt.get_property("keywords").datatype == db.LIST(db.TEXT)
    assert isinstance(model["keywords"], db.Property)
    assert model["keywords"].name == "keywords"
    assert model["keywords"].datatype == db.LIST(db.TEXT)

    assert dataset_rt.get_property("booleans") is not None
    assert dataset_rt.get_property("booleans").datatype == db.LIST(db.BOOLEAN)
    assert isinstance(model["booleans"], db.Property)
    assert model["booleans"].name == "booleans"
    assert model["booleans"].datatype == db.LIST(db.BOOLEAN)

    assert dataset_rt.get_property("integers") is not None
    assert dataset_rt.get_property("integers").datatype == db.LIST(db.INTEGER)
    assert isinstance(model["integers"], db.Property)
    assert model["integers"].name == "integers"
    assert model["integers"].datatype == db.LIST(db.INTEGER)

    assert dataset_rt.get_property("floats") is not None
    assert dataset_rt.get_property("floats").datatype == db.LIST(db.DOUBLE)
    assert isinstance(model["floats"], db.Property)
    assert model["floats"].name == "floats"
    assert model["floats"].datatype == db.LIST(db.DOUBLE)

    assert dataset_rt.get_property("datetimes") is not None
    assert dataset_rt.get_property(
        "datetimes").datatype == db.LIST(db.DATETIME)
    assert isinstance(model["datetimes"], db.Property)
    assert model["datetimes"].name == "datetimes"
    assert model["datetimes"].datatype == db.LIST(db.DATETIME)

    assert dataset_rt.get_property("dates") is not None
    assert dataset_rt.get_property(
        "dates").datatype == db.LIST(db.DATETIME)
    assert isinstance(model["dates"], db.Property)
    assert model["dates"].name == "dates"
    assert model["dates"].datatype == db.LIST(db.DATETIME)

    # Simple reference list property
    assert dataset_rt.get_property("reference") is not None
    assert dataset_rt.get_property("reference").is_reference()
    assert dataset_rt.get_property(
        "reference").datatype == db.LIST("reference")
    assert isinstance(model["reference"], db.RecordType)
    assert model["reference"].name == "reference"
    assert dataset_rt.get_property(
        "reference").datatype == db.LIST(model["reference"])

    # Reference list with name
    assert dataset_rt.get_property("reference_with_name") is not None
    assert dataset_rt.get_property("reference_with_name").is_reference()
    assert dataset_rt.get_property(
        "reference_with_name").datatype == db.LIST("event")
    assert isinstance(model["event"], db.RecordType)
    assert model["event"].name == "event"
    assert dataset_rt.get_property(
        "reference_with_name").datatype == db.LIST(model["event"])
    assert isinstance(model["reference_with_name"], db.Property)
    assert model["reference_with_name"].name == "reference_with_name"
    assert model["reference_with_name"].datatype == db.LIST(model["event"])

    # References to enum types
    assert dataset_rt.get_property("license") is not None
    assert dataset_rt.get_property("license").is_reference()
    assert dataset_rt.get_property("license").datatype == db.LIST("license")
    assert isinstance(model["license"], db.RecordType)
    assert model["license"].name == "license"
    assert dataset_rt.get_property(
        "license").datatype == db.LIST(model["license"])

    for name in licenses:
        assert isinstance(model[name], db.Record)
        assert model[name].name == name
        assert len(model[name].parents) == 1
        assert model[name].has_parent(model["license"])


def test_name_property():
    model = parse_model_from_json_schema(os.path.join(
        FILEPATH, "datamodel_name.schema.json"))

    dataset_rt = model["Dataset"]
    assert dataset_rt.get_property("name") is None
    assert "name" not in model

    with pytest.raises(JsonSchemaDefinitionError) as err:
        broken = parse_model_from_json_schema(os.path.join(
            FILEPATH, "datamodel_name_wrong_type.schema.json"))
    assert str(err.value).startswith(
        "The 'name' property must be string-typed, otherwise it cannot be identified with CaosDB's "
        "name property.")
