/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.search;


import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.similarities.ClassicSimilarity;
import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.search.spans.SpanTermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.NamedThreadFactory;
import org.apache.lucene.util.TestUtil;

public class TestBooleanQuery extends LuceneTestCase {

  public void testEquality() throws Exception {
    BooleanQuery.Builder bq1 = new BooleanQuery.Builder();
    bq1.add(new TermQuery(new Term("field", "value1")), BooleanClause.Occur.SHOULD);
    bq1.add(new TermQuery(new Term("field", "value2")), BooleanClause.Occur.SHOULD);
    BooleanQuery.Builder nested1 = new BooleanQuery.Builder();
    nested1.add(new TermQuery(new Term("field", "nestedvalue1")), BooleanClause.Occur.SHOULD);
    nested1.add(new TermQuery(new Term("field", "nestedvalue2")), BooleanClause.Occur.SHOULD);
    bq1.add(nested1.build(), BooleanClause.Occur.SHOULD);

    BooleanQuery.Builder bq2 = new BooleanQuery.Builder();
    bq2.add(new TermQuery(new Term("field", "value1")), BooleanClause.Occur.SHOULD);
    bq2.add(new TermQuery(new Term("field", "value2")), BooleanClause.Occur.SHOULD);
    BooleanQuery.Builder nested2 = new BooleanQuery.Builder();
    nested2.add(new TermQuery(new Term("field", "nestedvalue1")), BooleanClause.Occur.SHOULD);
    nested2.add(new TermQuery(new Term("field", "nestedvalue2")), BooleanClause.Occur.SHOULD);
    bq2.add(nested2.build(), BooleanClause.Occur.SHOULD);

    assertEquals(bq1.build(), bq2.build());
  }

  public void testEqualityDoesNotDependOnOrder() {
    TermQuery[] queries = new TermQuery[] {
        new TermQuery(new Term("foo", "bar")),
        new TermQuery(new Term("foo", "baz"))
    };
    for (int iter = 0; iter < 10; ++iter) {
      List<BooleanClause> clauses = new ArrayList<>();
      final int numClauses = random().nextInt(20);
      for (int i = 0; i < numClauses; ++i) {
        Query query = RandomPicks.randomFrom(random(), queries);
        if (random().nextBoolean()) {
          query = new BoostQuery(query, random().nextFloat());
        }
        Occur occur = RandomPicks.randomFrom(random(), Occur.values());
        clauses.add(new BooleanClause(query, occur));
      }

      final int minShouldMatch = random().nextInt(5);
      BooleanQuery.Builder bq1Builder = new BooleanQuery.Builder();
      bq1Builder.setMinimumNumberShouldMatch(minShouldMatch);
      for (BooleanClause clause : clauses) {
        bq1Builder.add(clause);
      }
      final BooleanQuery bq1 = bq1Builder.build();

      Collections.shuffle(clauses, random());
      BooleanQuery.Builder bq2Builder = new BooleanQuery.Builder();
      bq2Builder.setMinimumNumberShouldMatch(minShouldMatch);
      for (BooleanClause clause : clauses) {
        bq2Builder.add(clause);
      }
      final BooleanQuery bq2 = bq2Builder.build();

      QueryUtils.checkEqual(bq1, bq2);
    }
  }

  public void testEqualityOnDuplicateShouldClauses() {
    BooleanQuery bq1 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(random().nextInt(2))
      .add(new TermQuery(new Term("foo", "bar")), Occur.SHOULD)
      .build();
    BooleanQuery bq2 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(bq1.getMinimumNumberShouldMatch())
      .add(new TermQuery(new Term("foo", "bar")), Occur.SHOULD)
      .add(new TermQuery(new Term("foo", "bar")), Occur.SHOULD)
      .build();
    QueryUtils.checkUnequal(bq1, bq2);
  }

  public void testEqualityOnDuplicateMustClauses() {
    BooleanQuery bq1 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(random().nextInt(2))
      .add(new TermQuery(new Term("foo", "bar")), Occur.MUST)
      .build();
    BooleanQuery bq2 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(bq1.getMinimumNumberShouldMatch())
      .add(new TermQuery(new Term("foo", "bar")), Occur.MUST)
      .add(new TermQuery(new Term("foo", "bar")), Occur.MUST)
      .build();
    QueryUtils.checkUnequal(bq1, bq2);
  }

  public void testEqualityOnDuplicateFilterClauses() {
    BooleanQuery bq1 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(random().nextInt(2))
      .add(new TermQuery(new Term("foo", "bar")), Occur.FILTER)
      .build();
    BooleanQuery bq2 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(bq1.getMinimumNumberShouldMatch())
      .add(new TermQuery(new Term("foo", "bar")), Occur.FILTER)
      .add(new TermQuery(new Term("foo", "bar")), Occur.FILTER)
      .build();
    QueryUtils.checkEqual(bq1, bq2);
  }

  public void testEqualityOnDuplicateMustNotClauses() {
    BooleanQuery bq1 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(random().nextInt(2))
      .add(new MatchAllDocsQuery(), Occur.MUST)
      .add(new TermQuery(new Term("foo", "bar")), Occur.FILTER)
      .build();
    BooleanQuery bq2 = new BooleanQuery.Builder()
      .setMinimumNumberShouldMatch(bq1.getMinimumNumberShouldMatch())
      .add(new MatchAllDocsQuery(), Occur.MUST)
      .add(new TermQuery(new Term("foo", "bar")), Occur.FILTER)
      .add(new TermQuery(new Term("foo", "bar")), Occur.FILTER)
      .build();
    QueryUtils.checkEqual(bq1, bq2);
  }

  public void testHashCodeIsStable() {
    BooleanQuery bq = new BooleanQuery.Builder()
      .add(new TermQuery(new Term("foo", TestUtil.randomSimpleString(random()))), Occur.SHOULD)
      .add(new TermQuery(new Term("foo", TestUtil.randomSimpleString(random()))), Occur.SHOULD)
      .build();
    final int hashCode = bq.hashCode();
    assertEquals(hashCode, bq.hashCode());
  }

  public void testException() {
    expectThrows(IllegalArgumentException.class, () -> {
      BooleanQuery.setMaxClauseCount(0);
    });
  }

  // LUCENE-1630
  public void testNullOrSubScorer() throws Throwable {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    doc.add(newTextField("field", "a b c d", Field.Store.NO));
    w.addDocument(doc);

    IndexReader r = w.getReader();
    IndexSearcher s = newSearcher(r);
    // this test relies upon coord being the default implementation,
    // otherwise scores are different!
    s.setSimilarity(new ClassicSimilarity());

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(new TermQuery(new Term("field", "a")), BooleanClause.Occur.SHOULD);

    // PhraseQuery w/ no terms added returns a null scorer
    PhraseQuery pq = new PhraseQuery("field", new String[0]);
    q.add(pq, BooleanClause.Occur.SHOULD);
    assertEquals(1, s.search(q.build(), 10).totalHits.value);

    // A required clause which returns null scorer should return null scorer to
    // IndexSearcher.
    q = new BooleanQuery.Builder();
    pq = new PhraseQuery("field", new String[0]);
    q.add(new TermQuery(new Term("field", "a")), BooleanClause.Occur.SHOULD);
    q.add(pq, BooleanClause.Occur.MUST);
    assertEquals(0, s.search(q.build(), 10).totalHits.value);

    DisjunctionMaxQuery dmq = new DisjunctionMaxQuery(
        Arrays.asList(new TermQuery(new Term("field", "a")), pq),
        1.0f);
    assertEquals(1, s.search(dmq, 10).totalHits.value);

    r.close();
    w.close();
    dir.close();
  }

  public void testDeMorgan() throws Exception {
    Directory dir1 = newDirectory();
    RandomIndexWriter iw1 = new RandomIndexWriter(random(), dir1);
    Document doc1 = new Document();
    doc1.add(newTextField("field", "foo bar", Field.Store.NO));
    iw1.addDocument(doc1);
    IndexReader reader1 = iw1.getReader();
    iw1.close();

    Directory dir2 = newDirectory();
    RandomIndexWriter iw2 = new RandomIndexWriter(random(), dir2);
    Document doc2 = new Document();
    doc2.add(newTextField("field", "foo baz", Field.Store.NO));
    iw2.addDocument(doc2);
    IndexReader reader2 = iw2.getReader();
    iw2.close();

    BooleanQuery.Builder query = new BooleanQuery.Builder(); // Query: +foo -ba*
    query.add(new TermQuery(new Term("field", "foo")), BooleanClause.Occur.MUST);
    WildcardQuery wildcardQuery = new WildcardQuery(new Term("field", "ba*"));
    wildcardQuery.setRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_REWRITE);
    query.add(wildcardQuery, BooleanClause.Occur.MUST_NOT);

    MultiReader multireader = new MultiReader(reader1, reader2);
    IndexSearcher searcher = newSearcher(multireader);
    assertEquals(0, searcher.search(query.build(), 10).totalHits.value);

    final ExecutorService es = Executors.newCachedThreadPool(new NamedThreadFactory("NRT search threads"));
    searcher = new IndexSearcher(multireader, es);
    if (VERBOSE)
      System.out.println("rewritten form: " + searcher.rewrite(query.build()));
    assertEquals(0, searcher.search(query.build(), 10).totalHits.value);
    es.shutdown();
    es.awaitTermination(1, TimeUnit.SECONDS);

    multireader.close();
    reader1.close();
    reader2.close();
    dir1.close();
    dir2.close();
  }

  public void testBS2DisjunctionNextVsAdvance() throws Exception {
    final Directory d = newDirectory();
    final RandomIndexWriter w = new RandomIndexWriter(random(), d);
    final int numDocs = atLeast(300);
    for(int docUpto=0;docUpto<numDocs;docUpto++) {
      String contents = "a";
      if (random().nextInt(20) <= 16) {
        contents += " b";
      }
      if (random().nextInt(20) <= 8) {
        contents += " c";
      }
      if (random().nextInt(20) <= 4) {
        contents += " d";
      }
      if (random().nextInt(20) <= 2) {
        contents += " e";
      }
      if (random().nextInt(20) <= 1) {
        contents += " f";
      }
      Document doc = new Document();
      doc.add(new TextField("field", contents, Field.Store.NO));
      w.addDocument(doc);
    }
    w.forceMerge(1);
    final IndexReader r = w.getReader();
    final IndexSearcher s = newSearcher(r);
    w.close();

    for(int iter=0;iter<10*RANDOM_MULTIPLIER;iter++) {
      if (VERBOSE) {
        System.out.println("iter=" + iter);
      }
      final List<String> terms = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e", "f"));
      final int numTerms = TestUtil.nextInt(random(), 1, terms.size());
      while(terms.size() > numTerms) {
        terms.remove(random().nextInt(terms.size()));
      }

      if (VERBOSE) {
        System.out.println("  terms=" + terms);
      }

      final BooleanQuery.Builder q = new BooleanQuery.Builder();
      for(String term : terms) {
        q.add(new BooleanClause(new TermQuery(new Term("field", term)), BooleanClause.Occur.SHOULD));
      }

      Weight weight = s.createWeight(s.rewrite(q.build()), ScoreMode.COMPLETE, 1);

      Scorer scorer = weight.scorer(s.leafContexts.get(0));

      // First pass: just use .nextDoc() to gather all hits
      final List<ScoreDoc> hits = new ArrayList<>();
      while(scorer.iterator().nextDoc() != DocIdSetIterator.NO_MORE_DOCS) {
        hits.add(new ScoreDoc(scorer.docID(), scorer.score()));
      }

      if (VERBOSE) {
        System.out.println("  " + hits.size() + " hits");
      }

      // Now, randomly next/advance through the list and
      // verify exact match:
      for(int iter2=0;iter2<10;iter2++) {

        weight = s.createWeight(s.rewrite(q.build()), ScoreMode.COMPLETE, 1);
        scorer = weight.scorer(s.leafContexts.get(0));

        if (VERBOSE) {
          System.out.println("  iter2=" + iter2);
        }

        int upto = -1;
        while(upto < hits.size()) {
          final int nextUpto;
          final int nextDoc;
          final int left = hits.size() - upto;
          if (left == 1 || random().nextBoolean()) {
            // next
            nextUpto = 1+upto;
            nextDoc = scorer.iterator().nextDoc();
          } else {
            // advance
            int inc = TestUtil.nextInt(random(), 1, left - 1);
            nextUpto = inc + upto;
            nextDoc = scorer.iterator().advance(hits.get(nextUpto).doc);
          }

          if (nextUpto == hits.size()) {
            assertEquals(DocIdSetIterator.NO_MORE_DOCS, nextDoc);
          } else {
            final ScoreDoc hit = hits.get(nextUpto);
            assertEquals(hit.doc, nextDoc);
            // Test for precise float equality:
            assertTrue("doc " + hit.doc + " has wrong score: expected=" + hit.score + " actual=" + scorer.score(), hit.score == scorer.score());
          }
          upto = nextUpto;
        }
      }
    }

    r.close();
    d.close();
  }

  // LUCENE-4477 / LUCENE-4401:
  public void testBooleanSpanQuery() throws Exception {
    boolean failed = false;
    int hits = 0;
    Directory directory = newDirectory();
    Analyzer indexerAnalyzer = new MockAnalyzer(random());

    IndexWriterConfig config = new IndexWriterConfig(indexerAnalyzer);
    IndexWriter writer = new IndexWriter(directory, config);
    String FIELD = "content";
    Document d = new Document();
    d.add(new TextField(FIELD, "clockwork orange", Field.Store.YES));
    writer.addDocument(d);
    writer.close();

    IndexReader indexReader = DirectoryReader.open(directory);
    IndexSearcher searcher = newSearcher(indexReader);

    BooleanQuery.Builder query = new BooleanQuery.Builder();
    SpanQuery sq1 = new SpanTermQuery(new Term(FIELD, "clockwork"));
    SpanQuery sq2 = new SpanTermQuery(new Term(FIELD, "clckwork"));
    query.add(sq1, BooleanClause.Occur.SHOULD);
    query.add(sq2, BooleanClause.Occur.SHOULD);
    TopScoreDocCollector collector = TopScoreDocCollector.create(1000, Integer.MAX_VALUE);
    searcher.search(query.build(), collector);
    hits = collector.topDocs().scoreDocs.length;
    for (ScoreDoc scoreDoc : collector.topDocs().scoreDocs){
      System.out.println(scoreDoc.doc);
    }
    indexReader.close();
    assertEquals("Bug in boolean query composed of span queries", failed, false);
    assertEquals("Bug in boolean query composed of span queries", hits, 1);
    directory.close();
  }

  public void testMinShouldMatchLeniency() throws Exception {
    Directory dir = newDirectory();
    IndexWriter w = new IndexWriter(dir, newIndexWriterConfig(new MockAnalyzer(random())));
    Document doc = new Document();
    doc.add(newTextField("field", "a b c d", Field.Store.NO));
    w.addDocument(doc);
    IndexReader r = DirectoryReader.open(w);
    IndexSearcher s = newSearcher(r);
    BooleanQuery.Builder bq = new BooleanQuery.Builder();
    bq.add(new TermQuery(new Term("field", "a")), BooleanClause.Occur.SHOULD);
    bq.add(new TermQuery(new Term("field", "b")), BooleanClause.Occur.SHOULD);

    // No doc can match: BQ has only 2 clauses and we are asking for minShouldMatch=4
    bq.setMinimumNumberShouldMatch(4);
    assertEquals(0, s.search(bq.build(), 1).totalHits.value);
    r.close();
    w.close();
    dir.close();
  }

  private static BitSet getMatches(IndexSearcher searcher, Query query) throws IOException {
    BitSet set = new BitSet();
    searcher.search(query, new SimpleCollector() {
      int docBase = 0;
      @Override
      public ScoreMode scoreMode() {
        return ScoreMode.COMPLETE_NO_SCORES;
      }
      @Override
      protected void doSetNextReader(LeafReaderContext context)
          throws IOException {
        super.doSetNextReader(context);
        docBase = context.docBase;
      }
      @Override
      public void collect(int doc) throws IOException {
        set.set(docBase + doc);
      }
    });
    return set;
  }

  public void testFILTERClauseBehavesLikeMUST() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c d", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    f.setStringValue("b d");
    w.addDocument(doc);
    f.setStringValue("d");
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    final IndexSearcher searcher = new IndexSearcher(reader);

    for (List<String> requiredTerms : Arrays.<List<String>>asList(
        Arrays.asList("a", "d"),
        Arrays.asList("a", "b", "d"),
        Arrays.asList("d"),
        Arrays.asList("e"),
        Arrays.asList())) {
      final BooleanQuery.Builder bq1 = new BooleanQuery.Builder();
      final BooleanQuery.Builder bq2 = new BooleanQuery.Builder();
      for (String term : requiredTerms) {
        final Query q = new TermQuery(new Term("field", term));
        bq1.add(q, Occur.MUST);
        bq2.add(q, Occur.FILTER);
      }

      final BitSet matches1 = getMatches(searcher, bq1.build());
      final BitSet matches2 = getMatches(searcher, bq2.build());
      assertEquals(matches1, matches2);
    }

    reader.close();
    w.close();
    dir.close();
  }

  private void assertSameScoresWithoutFilters(IndexSearcher searcher, BooleanQuery bq) throws IOException {
    final BooleanQuery.Builder bq2Builder = new BooleanQuery.Builder();
    for (BooleanClause c : bq) {
      if (c.getOccur() != Occur.FILTER) {
        bq2Builder.add(c);
      }
    }
    bq2Builder.setMinimumNumberShouldMatch(bq.getMinimumNumberShouldMatch());
    BooleanQuery bq2 = bq2Builder.build();

    final AtomicBoolean matched = new AtomicBoolean();
    searcher.search(bq, new SimpleCollector() {
      int docBase;
      Scorable scorer;

      @Override
      protected void doSetNextReader(LeafReaderContext context)
          throws IOException {
        super.doSetNextReader(context);
        docBase = context.docBase;
      }

      @Override
      public ScoreMode scoreMode() {
        return ScoreMode.COMPLETE;
      }

      @Override
      public void setScorer(Scorable scorer) throws IOException {
        this.scorer = scorer;
      }

      @Override
      public void collect(int doc) throws IOException {
        final float actualScore = scorer.score();
        final float expectedScore = searcher.explain(bq2, docBase + doc).getValue().floatValue();
        assertEquals(expectedScore, actualScore, 10e-5);
        matched.set(true);
      }
    });
    assertTrue(matched.get());
  }

  public void testFilterClauseDoesNotImpactScore() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c d", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    f.setStringValue("b d");
    w.addDocument(doc);
    f.setStringValue("a d");
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    final IndexSearcher searcher = new IndexSearcher(reader);

    BooleanQuery.Builder qBuilder = new BooleanQuery.Builder();
    BooleanQuery q = qBuilder.build();
    qBuilder.add(new TermQuery(new Term("field", "a")), Occur.FILTER);

    // With a single clause, we will rewrite to the underlying
    // query. Make sure that it returns null scores
    assertSameScoresWithoutFilters(searcher, qBuilder.build());

    // Now with two clauses, we will get a conjunction scorer
    // Make sure it returns null scores
    qBuilder.add(new TermQuery(new Term("field", "b")), Occur.FILTER);
    q = qBuilder.build();
    assertSameScoresWithoutFilters(searcher, q);

    // Now with a scoring clause, we need to make sure that
    // the boolean scores are the same as those from the term
    // query
    qBuilder.add(new TermQuery(new Term("field", "c")), Occur.SHOULD);
    q = qBuilder.build();
    assertSameScoresWithoutFilters(searcher, q);

    // FILTER and empty SHOULD
    qBuilder = new BooleanQuery.Builder();
    qBuilder.add(new TermQuery(new Term("field", "a")), Occur.FILTER);
    qBuilder.add(new TermQuery(new Term("field", "e")), Occur.SHOULD);
    q = qBuilder.build();
    assertSameScoresWithoutFilters(searcher, q);

    // mix of FILTER and MUST
    qBuilder = new BooleanQuery.Builder();
    qBuilder.add(new TermQuery(new Term("field", "a")), Occur.FILTER);
    qBuilder.add(new TermQuery(new Term("field", "d")), Occur.MUST);
    q = qBuilder.build();
    assertSameScoresWithoutFilters(searcher, q);

    // FILTER + minShouldMatch
    qBuilder = new BooleanQuery.Builder();
    qBuilder.add(new TermQuery(new Term("field", "b")), Occur.FILTER);
    qBuilder.add(new TermQuery(new Term("field", "a")), Occur.SHOULD);
    qBuilder.add(new TermQuery(new Term("field", "d")), Occur.SHOULD);
    qBuilder.setMinimumNumberShouldMatch(1);
    q = qBuilder.build();
    assertSameScoresWithoutFilters(searcher, q);

    reader.close();
    w.close();
    dir.close();
  }

  public void testConjunctionPropagatesApproximations() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    // not LuceneTestCase.newSearcher to not have the asserting wrappers
    // and do instanceof checks
    final IndexSearcher searcher = new IndexSearcher(reader);
    searcher.setQueryCache(null); // to still have approximations

    PhraseQuery pq = new PhraseQuery("field", "a", "b");

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(pq, Occur.MUST);
    q.add(new TermQuery(new Term("field", "c")), Occur.FILTER);

    final Weight weight = searcher.createWeight(searcher.rewrite(q.build()), ScoreMode.COMPLETE, 1);
    final Scorer scorer = weight.scorer(searcher.getIndexReader().leaves().get(0));
    assertTrue(scorer instanceof ConjunctionScorer);
    assertNotNull(scorer.twoPhaseIterator());

    reader.close();
    w.close();
    dir.close();
  }

  public void testDisjunctionPropagatesApproximations() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    final IndexSearcher searcher = new IndexSearcher(reader);
    searcher.setQueryCache(null); // to still have approximations

    PhraseQuery pq = new PhraseQuery("field", "a", "b");

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(pq, Occur.SHOULD);
    q.add(new TermQuery(new Term("field", "c")), Occur.SHOULD);

    final Weight weight = searcher.createWeight(searcher.rewrite(q.build()), ScoreMode.COMPLETE, 1);
    final Scorer scorer = weight.scorer(reader.leaves().get(0));
    assertTrue(scorer instanceof DisjunctionScorer);
    assertNotNull(scorer.twoPhaseIterator());

    reader.close();
    w.close();
    dir.close();
  }

  public void testBoostedScorerPropagatesApproximations() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    // not LuceneTestCase.newSearcher to not have the asserting wrappers
    // and do instanceof checks
    final IndexSearcher searcher = new IndexSearcher(reader);
    searcher.setQueryCache(null); // to still have approximations

    PhraseQuery pq = new PhraseQuery("field", "a", "b");

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(pq, Occur.SHOULD);
    q.add(new TermQuery(new Term("field", "d")), Occur.SHOULD);

    final Weight weight = searcher.createWeight(searcher.rewrite(q.build()), ScoreMode.COMPLETE, 1);
    final Scorer scorer = weight.scorer(searcher.getIndexReader().leaves().get(0));
    assertTrue(scorer instanceof PhraseScorer);
    assertNotNull(scorer.twoPhaseIterator());

    reader.close();
    w.close();
    dir.close();
  }

  public void testExclusionPropagatesApproximations() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    final IndexSearcher searcher = new IndexSearcher(reader);
    searcher.setQueryCache(null); // to still have approximations

    PhraseQuery pq = new PhraseQuery("field", "a", "b");

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(pq, Occur.SHOULD);
    q.add(new TermQuery(new Term("field", "c")), Occur.MUST_NOT);

    final Weight weight = searcher.createWeight(searcher.rewrite(q.build()), ScoreMode.COMPLETE, 1);
    final Scorer scorer = weight.scorer(reader.leaves().get(0));
    assertTrue(scorer instanceof ReqExclScorer);
    assertNotNull(scorer.twoPhaseIterator());

    reader.close();
    w.close();
    dir.close();
  }

  public void testReqOptPropagatesApproximations() throws IOException {
    Directory dir = newDirectory();
    RandomIndexWriter w = new RandomIndexWriter(random(), dir);
    Document doc = new Document();
    Field f = newTextField("field", "a b c", Field.Store.NO);
    doc.add(f);
    w.addDocument(doc);
    w.commit();

    DirectoryReader reader = w.getReader();
    final IndexSearcher searcher = new IndexSearcher(reader);
    searcher.setQueryCache(null); // to still have approximations

    PhraseQuery pq = new PhraseQuery("field", "a", "b");

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(pq, Occur.MUST);
    q.add(new TermQuery(new Term("field", "c")), Occur.SHOULD);

    final Weight weight = searcher.createWeight(searcher.rewrite(q.build()), ScoreMode.COMPLETE, 1);
    final Scorer scorer = weight.scorer(reader.leaves().get(0));
    assertTrue(scorer instanceof ReqOptSumScorer);
    assertNotNull(scorer.twoPhaseIterator());

    reader.close();
    w.close();
    dir.close();
  }
  
  public void testToString() {
    BooleanQuery.Builder bq = new BooleanQuery.Builder();
    bq.add(new TermQuery(new Term("field", "a")), Occur.SHOULD);
    bq.add(new TermQuery(new Term("field", "b")), Occur.MUST);
    bq.add(new TermQuery(new Term("field", "c")), Occur.MUST_NOT);
    bq.add(new TermQuery(new Term("field", "d")), Occur.FILTER);
    assertEquals("a +b -c #d", bq.build().toString("field"));
  }



  public void testQueryVisitor() throws IOException {
    Term a = new Term("f", "a");
    Term b = new Term("f", "b");
    Term c = new Term("f", "c");
    Term d = new Term("f", "d");
    BooleanQuery.Builder bqBuilder = new BooleanQuery.Builder();
    bqBuilder.add(new TermQuery(a), Occur.SHOULD);
    bqBuilder.add(new TermQuery(b), Occur.MUST);
    bqBuilder.add(new TermQuery(c), Occur.FILTER);
    bqBuilder.add(new TermQuery(d), Occur.MUST_NOT);
    BooleanQuery bq = bqBuilder.build();

    bq.visit(new QueryVisitor() {

      Term expected;

      @Override
      public QueryVisitor getSubVisitor(Occur occur, Query parent) {
        switch (occur) {
          case SHOULD:
            expected = a;
            break;
          case MUST:
            expected = b;
            break;
          case FILTER:
            expected = c;
            break;
          case MUST_NOT:
            expected = d;
            break;
          default:
            throw new IllegalStateException();
        }
        return this;
      }

      @Override
      public void consumeTerms(Query query, Term... terms) {
        assertEquals(expected, terms[0]);
      }
    });
  }
}
