#!/usr/bin/env python3

# Portions of this script contributed by NIST are governed by the
# following license:
#
# This software was developed at the National Institute of Standards
# and Technology by employees of the Federal Government in the course
# of their official duties. Pursuant to title 17 Section 105 of the
# United States Code this software is not subject to copyright
# protection and is in the public domain. NIST assumes no
# responsibility whatsoever for its use by other parties, and makes
# no guarantees, expressed or implied, about its quality,
# reliability, or any other characteristic.
#
# We would appreciate acknowledgement if the software is used.

from __future__ import annotations

import logging
from typing import Set, Tuple

from rdflib import Graph, URIRef
from rdflib.query import ResultRow


def test_nested_filter_outer_binding_propagation() -> None:
    expected: Set[URIRef] = {
        URIRef("http://example.org/Superclass"),
    }
    computed: Set[URIRef] = set()
    graph_data = """\
@prefix ex: <http://example.org/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

ex:Superclass
  a owl:Class ;
.
ex:Subclass1
  a owl:Class ;
  rdfs:subClassOf ex:Superclass ;
.
ex:Subclass2
  a owl:Class ;
  rdfs:subClassOf ex:Superclass ;
  owl:deprecated true ;
.
"""
    query = """\
SELECT ?class
WHERE {
  ?class a owl:Class .
  FILTER EXISTS {
    ?subclass rdfs:subClassOf ?class .
    FILTER NOT EXISTS { ?subclass owl:deprecated true }
  }
}
"""
    graph = Graph()
    graph.parse(data=graph_data)
    for result in graph.query(query):
        assert isinstance(result, ResultRow)
        assert isinstance(result[0], URIRef)
        computed.add(result[0])
    assert expected == computed


def test_nested_filter_outermost_binding_propagation() -> None:
    """
    This test implements a query that requires functionality of nested FILTER NOT EXISTS query components.

    It encodes a single ground truth positive query result, a tuple where:
    * The first member is a HistoricAction,
    * The second member is a wholly redundant HistoricRecord in consideration of latter HistoricRecords that cover all non-HistoricRecord inputs to the Action, and
    * The third member is the superseding record.
    """
    expected: Set[Tuple[URIRef, URIRef, URIRef]] = {
        (
            URIRef("http://example.org/kb/action-1-2"),
            URIRef("http://example.org/kb/record-123-1"),
            URIRef("http://example.org/kb/record-1-2"),
        )
    }
    computed: Set[Tuple[URIRef, URIRef, URIRef]] = set()

    historic_ontology_graph_data = """\
@prefix case-investigation: <https://ontology.caseontology.org/case/investigation/> .
@prefix ex: <http://example.org/ontology/> .
@prefix kb: <http://example.org/kb/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://example.org/ontology>
  a owl:Ontology ;
  rdfs:comment "This example ontology represents a history-analyzing application, where notes of things' handling are created and accompany the things as they are used in actions.  For the sake of demonstration, classes and properties implemented here are simplifications of other ontologies' classes and properties.  Otherwise, this ontology is narrowly similar to an application of the CASE and PROV-O ontologies."@en ;
  rdfs:seeAlso <https://github.com/casework/CASE-Implementation-PROV-O> ;
  .

# Begin ontology (TBox).

ex:HistoricThing
  a owl:Class ;
  rdfs:subClassOf owl:Thing ;
  rdfs:comment "A thing generated by some HistoricAction with an accompanying HistoricRecord, and is the input to other HistoricActions.  When a HistoricThing is the input to a HistoricAction, a new HistoricRecord should be emitted by the HistoricAction."@en ;
  rdfs:seeAlso prov:Entity ;
  .

ex:HistoricRecord
  a owl:Class ;
  rdfs:subClassOf ex:HistoricThing ;
  rdfs:comment
    "An example class analagous to PROV-O's Collection and CASE's ProvenanceRecord."@en ,
    "Only the latest HistoricRecord for an object should be an input to a HistoricAction."@en
    ;
  rdfs:seeAlso
    case-investigation:ProvenanceRecord ,
    prov:Collection
    ;
  .

ex:HistoricAction
  a owl:Class ;
  rdfs:subClassOf owl:Thing ;
  rdfs:comment "An example class analagous to PROV-O's Activity and CASE's InvestigativeAction."@en ;
  rdfs:seeAlso
    case-investigation:InvestigativeAction ,
    prov:Activity
    ;
  owl:disjointWith ex:HistoricThing ;
  .

ex:hadMember
  a owl:ObjectProperty ;
  rdfs:domain ex:HistoricRecord ;
  rdfs:range ex:HistoricThing ;
  rdfs:seeAlso prov:hadMember ;
  .

ex:generated
  a owl:ObjectProperty ;
  rdfs:domain ex:HistoricAction ;
  rdfs:range ex:HistoricThing ;
  rdfs:seeAlso prov:wasGeneratedBy ;
  .

ex:used
  a owl:ObjectProperty ;
  rdfs:domain ex:HistoricAction ;
  rdfs:range ex:HistoricThing ;
  rdfs:seeAlso prov:used ;
  .

ex:wasDerivedFrom
  a owl:ObjectProperty ;
  rdfs:domain owl:Thing ;
  rdfs:range owl:Thing ;
  rdfs:seeAlso prov:wasDerivedFrom ;
  .

# Begin knowledge base (ABox).

kb:record-123-1
  a ex:HistoricRecord ;
  rdfs:comment "This is a first record of having handled thing-1, thing-2, and thing-3."@en ;
  ex:hadMember
    kb:thing-1 ,
    kb:thing-2
    ;
  .

kb:record-1-2
  a ex:HistoricRecord ;
  rdfs:comment "This is a second record of having handled thing-1."@en ;
  ex:hadMember kb:thing-1 ;
  ex:wasDerivedFrom kb:record-123-1 ;
  .

kb:record-2-2
  a ex:HistoricRecord ;
  rdfs:comment "This is a second record of having handled thing-2."@en ;
  ex:hadMember kb:thing-2 ;
  ex:wasDerivedFrom kb:record-123-1 ;
  .

kb:record-4-1
  a ex:HistoricRecord ;
  rdfs:comment "This is a first record of having handled thing-4.  thing-4 is independent in history of thing-1 and thing-2."@en ;
  ex:hadMember kb:thing-4 ;
  .

kb:thing-1
  a ex:HistoricThing ;
  .

kb:thing-2
  a ex:HistoricThing ;
  .

kb:thing-3
  a ex:HistoricThing ;
  .

kb:thing-4
  a ex:HistoricThing ;
  .

kb:action-123-0
  a ex:HistoricAction ;
  rdfs:comment "Generate things 1, 2, and 3."@en ;
  ex:generated
    kb:record-123-1 ,
    kb:thing-1 ,
    kb:thing-2 ,
    kb:thing-3
    .

kb:action-4-0
  a ex:HistoricAction ;
  rdfs:comment "Generate thing 4."@en ;
  ex:generated
    kb:record-4-1 ,
    kb:thing-4
    .

kb:action-1-1
  a ex:HistoricAction ;
  rdfs:comment "Handle thing-1."@en ;
  ex:used
    kb:record-123-1 ,
    kb:thing-1
    ;
  ex:generated kb:record-1-2 ;
  .

kb:action-2-1
  a ex:HistoricAction ;
  rdfs:comment "Handle thing-2."@en ;
  ex:used
    kb:record-123-1 ,
    kb:thing-2
    ;
  ex:generated kb:record-2-2 ;
  .

kb:action-1-2
  a ex:HistoricAction ;
  rdfs:comment "This node SHOULD be found by the query.  record-123-1 is wholly redundant with record-1-2 with respect to the collective whole of action inputs."@en ;
  ex:used
    kb:record-123-1 ,
    kb:record-1-2 ,
    kb:thing-1
    ;
  .

kb:action-12-2
  a ex:HistoricAction ;
  rdfs:comment "This node SHOULD NOT be found by the query.  record-123-1 is partially, but not wholly, redundant with record-1-2, due to to thing-2 having record-123-1 as its only accompanying historic record."@en ;
  ex:used
    kb:record-123-1 ,
    kb:record-1-2 ,
    kb:thing-1 ,
    kb:thing-2
    ;
  .

kb:action-123-2
  a ex:HistoricAction ;
  rdfs:comment "This node SHOULD NOT be found by the query.  record-123-1 is partially, but not wholly, redundant with record-1-2 and record-2-2, due to thing-3 having record-123-1 as its only accompanying historic record."@en ;
  ex:used
    kb:record-123-1 ,
    kb:record-1-2 ,
    kb:record-2-2 ,
    kb:thing-1 ,
    kb:thing-2 ,
    kb:thing-3
    ;
  .

kb:action-1234-2
  a ex:HistoricAction ;
  rdfs:comment "This node SHOULD NOT be found by the query.  record-123-1 is partially, but not wholly, redundant with record-1-2 and record-2-2, due to thing-3 having record-123-1 as its only accompanying historic record.  thing-4 also has no shared history with thing-1, -2, or -3."@en ;
  ex:used
    kb:record-123-1 ,
    kb:record-1-2 ,
    kb:record-2-2 ,
    kb:record-4-1 ,
    kb:thing-1 ,
    kb:thing-2 ,
    kb:thing-3 ,
    kb:thing-4
    ;
  .
"""

    # See 'TEST OBJECTIVE' annotation.
    query = """\
PREFIX ex: <http://example.org/ontology/>
SELECT ?nAction ?nRedundantRecord ?nSupersedingRecord
WHERE {
  ?nAction
    ex:used
      ?nThing1 ,
      ?nRedundantRecord ,
      ?nSupersedingRecord
      ;
    .
  ?nRedundantRecord
    a ex:HistoricRecord ;
    ex:hadMember ?nThing1 ;
    .
  ?nSupersedingRecord
    a ex:HistoricRecord ;
    ex:wasDerivedFrom+ ?nRedundantRecord ;
    ex:hadMember ?nThing1 ;
    .
  FILTER NOT EXISTS {
    ?nAction ex:used ?nThing2 .
    ?nRedundantRecord ex:hadMember ?nThing2 .
    FILTER ( ?nThing1 != ?nThing2 )
    FILTER NOT EXISTS {
      ####
      #
      # TEST OBJECTIVE:
      # nThing2 must be passed from the outermost context.
      #
      ####
      ?nSupersedingRecord ex:hadMember ?nThing2 .
    }
  }
}
"""

    graph = Graph()
    graph.parse(data=historic_ontology_graph_data)
    logging.debug(len(graph))

    for result in graph.query(query):
        assert isinstance(result, ResultRow)
        assert isinstance(result[0], URIRef)
        assert isinstance(result[1], URIRef)
        assert isinstance(result[2], URIRef)

        computed.add((result[0], result[1], result[2]))

    assert expected == computed
