/******************************************************************************* 
 * Copyright (c) 2021 Eclipse RDF4J contributors. 
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Distribution License v1.0 
 * which accompanies this distribution, and is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php. 
 *******************************************************************************/
package org.eclipse.rdf4j.query.algebra.evaluation.impl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.rdf4j.model.util.Values.iri;

import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath;
import org.eclipse.rdf4j.query.algebra.Bound;
import org.eclipse.rdf4j.query.algebra.Difference;
import org.eclipse.rdf4j.query.algebra.Exists;
import org.eclipse.rdf4j.query.algebra.Extension;
import org.eclipse.rdf4j.query.algebra.ExtensionElem;
import org.eclipse.rdf4j.query.algebra.Filter;
import org.eclipse.rdf4j.query.algebra.Join;
import org.eclipse.rdf4j.query.algebra.LeftJoin;
import org.eclipse.rdf4j.query.algebra.Not;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.StatementPattern;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.Union;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryOptimizer;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryOptimizerTest;
import org.eclipse.rdf4j.query.impl.EmptyBindingSet;
import org.eclipse.rdf4j.query.impl.SimpleDataset;
import org.eclipse.rdf4j.query.parser.ParsedTupleQuery;
import org.eclipse.rdf4j.query.parser.QueryParserUtil;
import org.junit.jupiter.api.Test;

/**
 * @author Jeen
 */
public class BindingSetAssignmentInlinerTest extends QueryOptimizerTest {

	@Test
	public void testOptimizeAssignsVars() {
		String query = "select * \n"
				+ "where { values ?z { <urn:z1> } \n"
				+ "        ?x <urn:pred1> ?y ; \n"
				+ "           (<urn:pred2>/<urn:pred3>)* ?z . \n"
				+ "}";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();
		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Join join = (Join) projection.getArg();
		assertThat(join.getRightArg()).isInstanceOf(ArbitraryLengthPath.class);

		ArbitraryLengthPath path = (ArbitraryLengthPath) join.getRightArg();
		assertThat(path.getObjectVar().getName()).isEqualTo("z");
		assertThat(path.getObjectVar().getValue()).isEqualTo(iri("urn:z1"));
	}

	@Test
	public void testEmptyValues() {
		String query = "select * \n"
				+ "where { values ?z { } \n"
				+ "        ?x <urn:pred1> ?y ; \n"
				+ "           (<urn:pred2>/<urn:pred3>)* ?z . \n"
				+ "}";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();
		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Join join = (Join) projection.getArg();
		assertThat(join.getRightArg()).isInstanceOf(ArbitraryLengthPath.class);

		ArbitraryLengthPath path = (ArbitraryLengthPath) join.getRightArg();
		assertThat(path.getObjectVar().getName()).isEqualTo("z");
		assertThat(path.getObjectVar().getValue()).isNull();
	}

	@Test
	public void testOptimize_MultipleValues() {
		String query = "select * \n"
				+ "where { values ?z { <urn:z1> <urn:z2> } \n"
				+ "        ?x <urn:pred1> ?y ; \n"
				+ "           (<urn:pred2>/<urn:pred3>)* ?z . \n"
				+ "}";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();
		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Join join = (Join) projection.getArg();
		assertThat(join.getRightArg()).isInstanceOf(ArbitraryLengthPath.class);

		ArbitraryLengthPath path = (ArbitraryLengthPath) join.getRightArg();
		assertThat(path.getObjectVar().getName()).isEqualTo("z");
		assertThat(path.getObjectVar().getValue()).isNull();
	}

	@Test
	public void testOptimize_leftJoin() {
		String query = "PREFIX : <http://example.org/> \n"
				+ "PREFIX foaf: <http://xmlns.com/foaf/0.1/> \n"
				+ "SELECT ?s ?o1 ?o2\n"
				+ "{\n"
				+ "  ?s ?p1 ?o1 \n"
				+ "  OPTIONAL { ?s foaf:knows ?o2 }\n"
				+ "} VALUES (?o2) {\n"
				+ " (:b)\n"
				+ "}";
		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();

		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Join join = (Join) projection.getArg();
		assertThat(join.getRightArg()).isInstanceOf(LeftJoin.class);
		LeftJoin optional = (LeftJoin) join.getRightArg();

		Var o2 = ((StatementPattern) optional.getRightArg()).getObjectVar();
		assertThat(o2.getName()).isEqualTo("o2");
		assertThat(o2.getValue()).isNull();
	}

	/**
	 * @see https://github.com/eclipse/rdf4j/issues/3091
	 */
	@Test
	public void testOptimize_LeftJoinWithValuesInScope() {
		String query = "SELECT ?datasetBound {\n"
				+ "  OPTIONAL {\n"
				+ "    VALUES(?dataset ?uriSpace) {\n"
				+ "       (<http://example.org/void.ttl#FOAF> \"http://xmlns.com/foaf/0.1/\")  \n"
				+ "    }\n"
				+ "    FILTER(STRSTARTS(STR(<http://example.com>), ?uriSpace))\n"
				+ "  }\n"
				+ "  BIND(BOUND(?dataset) as ?datasetBound)\n"
				+ "}";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();

		assertThat(optimizedTree).isInstanceOf(Projection.class);
		Projection projection = (Projection) optimizedTree;

		Extension extension = (Extension) projection.getArg();
		assertThat(extension.getArg()).isInstanceOf(LeftJoin.class);
		ExtensionElem elem = extension.getElements().iterator().next();
		Bound bound = (Bound) elem.getExpr();

		Var datasetVar = bound.getArg();
		assertThat(datasetVar.getName()).isEqualTo("dataset");
		assertThat(datasetVar.getValue()).isNull();

	}

	@Test
	public void testOptimize_Union_OutOfScope() {
		String query = "SELECT * WHERE { VALUES ?s1 { <urn:a> } { ?s1 ?p1 ?o1 } UNION { ?s1 ?p1 ?o2 } }";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();

		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Join join = (Join) projection.getArg();
		assertThat(join.getRightArg()).isInstanceOf(Union.class);
		Union union = (Union) join.getRightArg();

		Var s1 = ((StatementPattern) union.getLeftArg()).getSubjectVar();
		assertThat(s1.getName()).isEqualTo("s1");
		assertThat(s1.getValue()).isNull();
		s1 = ((StatementPattern) union.getRightArg()).getSubjectVar();
		assertThat(s1.getName()).isEqualTo("s1");
		assertThat(s1.getValue()).isNull();
	}

	@Test
	public void testOptimize_Union_InScope() {
		String query = "SELECT * WHERE { { VALUES ?s1 { <urn:a> } ?s1 ?p1 ?o1 } UNION { ?s1 ?p1 ?o2 } }";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();

		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Union union = (Union) projection.getArg();

		Join join = (Join) union.getLeftArg();

		Var s1 = ((StatementPattern) join.getRightArg()).getSubjectVar();
		assertThat(s1.getName()).isEqualTo("s1");
		assertThat(s1.getValue()).isEqualTo(iri("urn:a"));
		s1 = ((StatementPattern) union.getRightArg()).getSubjectVar();
		assertThat(s1.getName()).isEqualTo("s1");
		assertThat(s1.getValue()).isNull();
	}

	@Test
	public void testOptimize_FilterNotExists() {
		String query = "SELECT * WHERE { VALUES ?s1 { <urn:a> } ?s1 ?p1 ?o1 . FILTER NOT EXISTS { ?s1 ?p2 ?o2 } }";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();

		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Filter filter = (Filter) projection.getArg();
		assertThat(filter.getArg()).isInstanceOf(Join.class);
		Join join = (Join) filter.getArg();

		Var s1 = ((StatementPattern) join.getRightArg()).getSubjectVar();
		assertThat(s1.getName()).isEqualTo("s1");
		assertThat(s1.getValue()).isEqualTo(iri("urn:a"));

		Not not = (Not) filter.getCondition();
		Exists exists = (Exists) not.getArg();
		StatementPattern sp = (StatementPattern) exists.getSubQuery();
		assertThat(sp.getSubjectVar().getName()).isEqualTo("s1");
		assertThat(sp.getSubjectVar().getValue()).isNull();
	}

	@Test
	public void testOptimize_Minus() {
		String query = "SELECT * WHERE { VALUES ?s1 { <urn:a> } ?s1 ?p1 ?o1 MINUS { ?s1 ?p2 ?o2 } }";

		ParsedTupleQuery parsedQuery = QueryParserUtil.parseTupleQuery(QueryLanguage.SPARQL, query, null);

		QueryOptimizer optimizer = getOptimizer();
		optimizer.optimize(parsedQuery.getTupleExpr(), new SimpleDataset(), EmptyBindingSet.getInstance());

		TupleExpr optimizedTree = parsedQuery.getTupleExpr();

		assertThat(optimizedTree).isInstanceOf(Projection.class);

		Projection projection = (Projection) optimizedTree;

		Difference difference = (Difference) projection.getArg();

		Join join = (Join) difference.getLeftArg();
		Var s1 = ((StatementPattern) join.getRightArg()).getSubjectVar();
		assertThat(s1.getName()).isEqualTo("s1");
		assertThat(s1.getValue()).isEqualTo(iri("urn:a"));

		StatementPattern sp = (StatementPattern) difference.getRightArg();
		assertThat(sp.getSubjectVar().getName()).isEqualTo("s1");
		assertThat(sp.getSubjectVar().getValue()).isNull();
	}

	@Override
	public QueryOptimizer getOptimizer() {
		return new BindingSetAssignmentInliner();
	}
}
