# 
# Blue Brain Nexus Forge is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# Blue Brain Nexus Forge 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 Lesser
# General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public License
# along with Blue Brain Nexus Forge. If not, see <https://choosealicense.com/licenses/lgpl-3.0/>.

import os
from typing import Callable, List, Union, Dict, Optional
from uuid import uuid4

import pytest
from pytest_bdd import given, parsers, then, when

from kgforge.core import Resource
from kgforge.core.commons.actions import Action
from kgforge.core.commons.context import Context
from kgforge.core.conversions.rdf import _merge_jsonld, Form


def do(fun: Callable, data: Union[Resource, List[Resource]], *args) -> None:
    if isinstance(data, List) and all(isinstance(x, Resource) for x in data):
        for x in data:
            fun(x, *args)
    elif isinstance(data, Resource):
        fun(data, *args)
    else:
        raise TypeError("not a Resource nor a list of Resource")


# It seems using 'given' with both 'fixture' and 'target_fixture' does not work.
# This implies that (in)valid resources creation steps need their dedicated function.


# Resource(s) creation.

def resource(valid: bool, index: int = 0) -> Resource:
    rid = str(uuid4())
    r = Resource(type="Person", id=rid)
    if valid:
        r.name = f"resource {index}"
    return r


def resources(valid: bool) -> List[Resource]:
    r1 = resource(valid, 0)
    r2 = resource(valid, 1)
    return [r1, r2]


@given("A valid resource.", target_fixture="data")
def valid_resource():
    return resource(True)


@given("Valid resources.", target_fixture="data")
def valid_resources():
    return resources(True)


@given("An invalid resource.", target_fixture="data")
def invalid_resource():
    return resource(False)


@given("Invalid resources.", target_fixture="data")
def invalid_resources():
    return resources(False)


# Resource(s) modifications.

@when("I modify the resource.")
def modify_resource(data):
    data.name = "other"


# Resource(s) verifications.

@then(parsers.parse("The '{attr}' status of {} resource should be '{value}'."))
def check_resource_status(data, attr, value):
    def fun(x): assert str(getattr(x, attr)) == value
    do(fun, data)


# Action(s) verifications.

def check_report(capsys, rc, err, msg, op):
    out = capsys.readouterr().out[:-1]
    heads = {
        "": "",
        "s": "<count> 2\n",
    }
    head = heads[rc]
    tails = {
        None: f"False\n<error> {msg}",
        " not": "True",
    }
    tail = tails[err]
    assert out == f"{head}<action> {op}\n<succeeded> {tail}"


@then("I should be able to access the report of the action on a resource.")
def check_action(data):
    def fun(x): assert isinstance(x._last_action, Action)
    do(fun, data)


@then(parsers.parse("The report should say that the operation was '{value}'."))
def check_action_operation(data, value):
    def fun(x): assert x._last_action.operation == value
    do(fun, data)


@then(parsers.parse("The report should say that the operation success is '{value}'."))
def check_action_success(data, value):
    def fun(x): assert str(x._last_action.succeeded) == value
    do(fun, data)


@then(parsers.parse("The report should say that the error was '{value}'."))
def check_action_error(data, value):
    def fun(x): assert str(x._last_action.error) == value
    do(fun, data)


# Fixtures for Resource to JSON-LD conversion and vice versa

@pytest.fixture
def custom_context():
    return {
        "@context": {
            "@base": "http://example.org/",
            "Person": "http://xmlns.com/foaf/0.1/Person",
            "name": "http://xmlns.com/foaf/0.1/name",
        }
    }


@pytest.fixture
def metadata_context() -> Context:
    document = {
        "deprecated": "https://store.net/vocabulary/deprecated",
        "version": "https://store.net/vocabulary/version"
    }
    return Context(document, "http://store.org/metadata.json")


@pytest.fixture
def metadata_data_compacted():
    return {
        "deprecated": False,
        "version": 1
    }


@pytest.fixture
def metadata_data_expanded():
    return {
        "https://store.net/vocabulary/deprecated": False,
        "https://store.net/vocabulary/version": 1
    }


@pytest.fixture
def person(custom_context):
    return Resource(context=custom_context, type="Person", name="Jami Booth")


@pytest.fixture
def organization(registered_person_custom_context, store_metadata_value):
    contribution = Resource(type="Organization", name="Reichel Inc",
                            founder=registered_person_custom_context)
    return contribution


@pytest.fixture
def building():
    type_ = "Building"
    name = "The Empire State Building"
    description = "The Empire State Building is a 102-story landmark in New York City."
    image = "http://www.civil.usherbrooke.ca/cours/gci215a/empire-state-building.jpg"
    geo = {"latitude": "40.75"}
    return Resource(type=type_, name=name, description=description, image=image, geo=geo)


@pytest.fixture(scope="session")
def context_file_path():
    return os.sep.join((os.path.abspath("."), "tests/data/shacl-model/context.json"))


@pytest.fixture(scope="session")
def context_iri_file(context_file_path):
    return f"file://{context_file_path}"


@pytest.fixture(scope="session")
def context_iri():
    return f"http://example.org/context"


@pytest.fixture(scope="session")
def model_context(context_iri_file) -> Context:
    return Context(context_iri_file, context_iri_file)


@pytest.fixture(scope="session")
def model_prefixes():
    return {
        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
        "prov": "http://www.w3.org/ns/prov#",
        "schema": "http://schema.org/",
        "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
        "skos": "http://www.w3.org/2004/02/skos/core#",
        "nsg": "https://neuroshapes.org/",
        "owl": "http://www.w3.org/2002/07/owl#",
        "xsd": "http://www.w3.org/2001/XMLSchema#"
    }

@pytest.fixture
def building_jsonld(metadata_context, metadata_data_compacted, metadata_data_expanded):
    def _make_jsonld_expanded(resource, store_metadata, context):
        data = dict()
        if hasattr(resource, "id"):
            data["@id"] = resource.id
        ctx = Context(resource.context) if hasattr(resource, "context") else Context(context)
        latitude_term = ctx.terms.get("latitude")
        if latitude_term.type:
            latitude_node = {
                "@type": latitude_term.type,
                "@value": resource.geo["latitude"]
            }
        else:
            latitude_node = resource.geo["latitude"]
        geo_expanded = {
            latitude_term.id: latitude_node
        }
        data.update({
            "@type": ctx.expand(resource.type),
            ctx.expand("description"): resource.description,
            ctx.expand("geo"): geo_expanded,
            ctx.expand("image"): {"@id": resource.image},
            ctx.expand("name"): resource.name
        })
        if store_metadata and resource._store_metadata is not None:
            data.update(metadata_data_expanded)
        return data

    def _make_jsonld_compacted(resource, store_metadata, context):
        data = dict()
        data_context = resource.context if hasattr(resource, "context") else context
        if store_metadata and resource._store_metadata is not None:
            metadata_context_output = metadata_context.iri if metadata_context.is_http_iri() else metadata_context.document["@context"]
            data["@context"] = _merge_jsonld(data_context, metadata_context_output)
        else:
            data["@context"] = data_context
        if hasattr(resource, "id"):
            data["@id"] = resource.id
        data.update({
            "@type": resource.type,
            "description": resource.description,
            "geo": resource.geo,
            "image": resource.image,
            "name": resource.name
        })
        if store_metadata and resource._store_metadata is not None:
            data.update(metadata_data_compacted)
        return data

    def _make_jsonld(rsrc: Resource, form: str, store_metadata: bool,
                     context: Optional[Union[Dict, List, str]]):
        if form is Form.COMPACTED.value:
            return _make_jsonld_compacted(rsrc, store_metadata, context)
        elif form is Form.EXPANDED.value:
            return _make_jsonld_expanded(rsrc, store_metadata, context)
    return _make_jsonld
