/*
 * 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.Arrays;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.FilterLeafReader;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.ReaderUtil;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanOrQuery;
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.BytesRef;
import org.apache.lucene.util.LuceneTestCase;

public class TestMatchesIterator extends LuceneTestCase {

  protected IndexSearcher searcher;
  protected Directory directory;
  protected IndexReader reader = null;

  private static final String FIELD_WITH_OFFSETS = "field_offsets";
  private static final String FIELD_NO_OFFSETS = "field_no_offsets";
  private static final String FIELD_DOCS_ONLY = "field_docs_only";
  private static final String FIELD_FREQS = "field_freqs";
  private static final String FIELD_POINT = "field_point";

  private static final FieldType OFFSETS = new FieldType(TextField.TYPE_STORED);
  static {
    OFFSETS.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
  }

  private static final FieldType DOCS = new FieldType(TextField.TYPE_STORED);
  static {
    DOCS.setIndexOptions(IndexOptions.DOCS);
  }

  private static final FieldType DOCS_AND_FREQS = new FieldType(TextField.TYPE_STORED);
  static {
    DOCS_AND_FREQS.setIndexOptions(IndexOptions.DOCS_AND_FREQS);
  }

  @Override
  public void tearDown() throws Exception {
    reader.close();
    directory.close();
    super.tearDown();
  }

  @Override
  public void setUp() throws Exception {
    super.setUp();
    directory = newDirectory();
    RandomIndexWriter writer = new RandomIndexWriter(random(), directory,
        newIndexWriterConfig(new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy()));
    for (int i = 0; i < docFields.length; i++) {
      Document doc = new Document();
      doc.add(newField(FIELD_WITH_OFFSETS, docFields[i], OFFSETS));
      doc.add(newField(FIELD_NO_OFFSETS, docFields[i], TextField.TYPE_STORED));
      doc.add(newField(FIELD_DOCS_ONLY, docFields[i], DOCS));
      doc.add(newField(FIELD_FREQS, docFields[i], DOCS_AND_FREQS));
      doc.add(new IntPoint(FIELD_POINT, 10));
      doc.add(new NumericDocValuesField(FIELD_POINT, 10));
      doc.add(new NumericDocValuesField("id", i));
      doc.add(newField("id", Integer.toString(i), TextField.TYPE_STORED));
      writer.addDocument(doc);
    }
    writer.forceMerge(1);
    reader = writer.getReader();
    writer.close();
    searcher = newSearcher(getOnlyLeafReader(reader));
  }

  protected String[] docFields = {
      "w1 w2 w3 w4 w5",
      "w1 w3 w2 w3 zz",
      "w1 xx w2 yy w4",
      "w1 w2 w1 w4 w2 w3",
      "a phrase sentence with many phrase sentence iterations of a phrase sentence",
      "nothing matches this document"
  };

  private void checkMatches(Query q, String field, int[][] expected) throws IOException {
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);
    for (int i = 0; i < expected.length; i++) {
      LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(expected[i][0], searcher.leafContexts));
      int doc = expected[i][0] - ctx.docBase;
      Matches matches = w.matches(ctx, doc);
      if (matches == null) {
        assertEquals(expected[i].length, 1);
        continue;
      }
      MatchesIterator it = matches.getMatches(field);
      if (expected[i].length == 1) {
        assertNull(it);
        continue;
      }
      checkFieldMatches(it, expected[i]);
      checkFieldMatches(matches.getMatches(field), expected[i]);  // test multiple calls
    }
  }

  private void checkLabelCount(Query q, String field, int[] expected) throws IOException {
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);
    for (int i = 0; i < expected.length; i++) {
      LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(i, searcher.leafContexts));
      int doc = i - ctx.docBase;
      Matches matches = w.matches(ctx, doc);
      if (matches == null) {
        assertEquals("Expected to get matches on document " + i, 0, expected[i]);
        continue;
      }
      MatchesIterator it = matches.getMatches(field);
      if (expected[i] == 0) {
        assertNull(it);
        continue;
      }
      else {
        assertNotNull(it);
      }
      IdentityHashMap<Query, Integer> labels = new IdentityHashMap<>();
      while (it.next()) {
        labels.put(it.getQuery(), 1);
      }
      assertEquals(expected[i], labels.size());
    }
  }

  private void checkFieldMatches(MatchesIterator it, int[] expected) throws IOException {
    int pos = 1;
    while (it.next()) {
      //System.out.println(expected[i][pos] + "->" + expected[i][pos + 1] + "[" + expected[i][pos + 2] + "->" + expected[i][pos + 3] + "]");
      assertEquals("Wrong start position", expected[pos], it.startPosition());
      assertEquals("Wrong end position", expected[pos + 1], it.endPosition());
      assertEquals("Wrong start offset", expected[pos + 2], it.startOffset());
      assertEquals("Wrong end offset", expected[pos + 3], it.endOffset());
      pos += 4;
    }
    assertEquals(expected.length, pos);
  }

  private void checkNoPositionsMatches(Query q, String field, boolean[] expected) throws IOException {
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);
    for (int i = 0; i < expected.length; i++) {
      LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(i, searcher.leafContexts));
      int doc = i - ctx.docBase;
      Matches matches = w.matches(ctx, doc);
      if (expected[i]) {
        MatchesIterator mi = matches.getMatches(field);
        assertNull(mi);
      }
      else {
        assertNull(matches);
      }
    }
  }

  private void checkSubMatches(Query q, String[][] expectedNames) throws IOException {
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE_NO_SCORES, 1);
    for (int i = 0; i < expectedNames.length; i++) {
      LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(i, searcher.leafContexts));
      int doc = i - ctx.docBase;
      Matches matches = w.matches(ctx, doc);
      if (matches == null) {
        assertEquals("Expected to get no matches on document " + i, 0, expectedNames[i].length);
        continue;
      }
      Set<String> expectedQueries = new HashSet<>(Arrays.asList(expectedNames[i]));
      Set<String> actualQueries = NamedMatches.findNamedMatches(matches)
          .stream().map(NamedMatches::getName).collect(Collectors.toSet());

      Set<String> unexpected = new HashSet<>(actualQueries);
      unexpected.removeAll(expectedQueries);
      assertEquals("Unexpected matching leaf queries: " + unexpected, 0, unexpected.size());
      Set<String> missing = new HashSet<>(expectedQueries);
      missing.removeAll(actualQueries);
      assertEquals("Missing matching leaf queries: " + missing, 0, missing.size());
    }
  }

  private void assertIsLeafMatch(Query q, String field) throws IOException {
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);
    for (int i = 0; i < searcher.reader.maxDoc(); i++) {
      LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(i, searcher.leafContexts));
      int doc = i - ctx.docBase;
      Matches matches = w.matches(ctx, doc);
      if (matches == null) {
        return;
      }
      MatchesIterator mi = matches.getMatches(field);
      if (mi == null) {
        return;
      }
      while (mi.next()) {
        assertNull(mi.getSubMatches());
      }
    }
  }

  private void checkTermMatches(Query q, String field, TermMatch[][][] expected) throws IOException {
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);
    for (int i = 0; i < expected.length; i++) {
      LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(i, searcher.leafContexts));
      int doc = i - ctx.docBase;
      Matches matches = w.matches(ctx, doc);
      if (matches == null) {
        assertEquals(expected[i].length, 0);
        continue;
      }
      MatchesIterator it = matches.getMatches(field);
      if (expected[i].length == 0) {
        assertNull(it);
        continue;
      }
      checkTerms(expected[i], it);
    }
  }

  private void checkTerms(TermMatch[][] expected, MatchesIterator it) throws IOException {
    int upTo = 0;
    while (it.next()) {
      Set<TermMatch> expectedMatches = new HashSet<>(Arrays.asList(expected[upTo]));
      MatchesIterator submatches = it.getSubMatches();
      while (submatches.next()) {
        TermMatch tm = new TermMatch(submatches.startPosition(), submatches.startOffset(), submatches.endOffset());
        if (expectedMatches.remove(tm) == false) {
          fail("Unexpected term match: " + tm);
        }
      }
      if (expectedMatches.size() != 0) {
        fail("Missing term matches: " + expectedMatches.stream().map(Object::toString).collect(Collectors.joining(", ")));
      }
      upTo++;
    }
    if (upTo < expected.length - 1) {
      fail("Missing expected match");
    }
  }

  static class TermMatch {

    public final int position;

    public final int startOffset;

    public final int endOffset;

    public TermMatch(PostingsEnum pe, int position) throws IOException {
      this.position = position;
      this.startOffset = pe.startOffset();
      this.endOffset = pe.endOffset();
    }

    public TermMatch(int position, int startOffset, int endOffset) {
      this.position = position;
      this.startOffset = startOffset;
      this.endOffset = endOffset;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      TermMatch termMatch = (TermMatch) o;
      return position == termMatch.position &&
          startOffset == termMatch.startOffset &&
          endOffset == termMatch.endOffset;
    }

    @Override
    public int hashCode() {
      return Objects.hash(position, startOffset, endOffset);
    }

    @Override
    public String toString() {
      return position + "[" + startOffset + "->" + endOffset + "]";
    }
  }

  public void testTermQuery() throws IOException {
    Term t = new Term(FIELD_WITH_OFFSETS, "w1");
    Query q = NamedMatches.wrapQuery("q", new TermQuery(t));
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2 },
        { 1, 0, 0, 0, 2 },
        { 2, 0, 0, 0, 2 },
        { 3, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 4 }
    });
    checkLabelCount(q, FIELD_WITH_OFFSETS, new int[]{ 1, 1, 1, 1, 0, 0 });
    assertIsLeafMatch(q, FIELD_WITH_OFFSETS);
    checkSubMatches(q, new String[][]{ {"q"}, {"q"}, {"q"}, {"q"}, {}, {}});
  }

  public void testTermQueryNoStoredOffsets() throws IOException {
    Query q = new TermQuery(new Term(FIELD_NO_OFFSETS, "w1"));
    checkMatches(q, FIELD_NO_OFFSETS, new int[][]{
        { 0, 0, 0, -1, -1 },
        { 1, 0, 0, -1, -1 },
        { 2, 0, 0, -1, -1 },
        { 3, 0, 0, -1, -1, 2, 2, -1, -1 },
        { 4 }
    });
  }

  public void testTermQueryNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_DOCS_ONLY, FIELD_FREQS }) {
      Query q = new TermQuery(new Term(field, "w1"));
      checkNoPositionsMatches(q, field, new boolean[]{ true, true, true, true, false });
    }
  }

  public void testDisjunction() throws IOException {
    Query w1 = NamedMatches.wrapQuery("w1", new TermQuery(new Term(FIELD_WITH_OFFSETS, "w1")));
    Query w3 = NamedMatches.wrapQuery("w3", new TermQuery(new Term(FIELD_WITH_OFFSETS, "w3")));
    Query q = new BooleanQuery.Builder()
        .add(w1, BooleanClause.Occur.SHOULD)
        .add(w3, BooleanClause.Occur.SHOULD)
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 1, 0, 0, 0, 2, 1, 1, 3, 5, 3, 3, 9, 11 },
        { 2, 0, 0, 0, 2 },
        { 3, 0, 0, 0, 2, 2, 2, 6, 8, 5, 5, 15, 17 },
        { 4 }
    });
    checkLabelCount(q, FIELD_WITH_OFFSETS, new int[]{ 2, 2, 1, 2, 0, 0 });
    assertIsLeafMatch(q, FIELD_WITH_OFFSETS);
    checkSubMatches(q, new String[][]{ {"w1", "w3"}, {"w1", "w3"}, {"w1"}, {"w1", "w3"}, {}, {}});
  }

  public void testDisjunctionNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_DOCS_ONLY, FIELD_FREQS }) {
      Query q = new BooleanQuery.Builder()
          .add(new TermQuery(new Term(field, "w1")), BooleanClause.Occur.SHOULD)
          .add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.SHOULD)
          .build();
      checkNoPositionsMatches(q, field, new boolean[]{ true, true, true, true, false });
    }
  }

  public void testReqOpt() throws IOException {
    Query q = new BooleanQuery.Builder()
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "w1")), BooleanClause.Occur.SHOULD)
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "w3")), BooleanClause.Occur.MUST)
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 1, 0, 0, 0, 2, 1, 1, 3, 5, 3, 3, 9, 11 },
        { 2 },
        { 3, 0, 0, 0, 2, 2, 2, 6, 8, 5, 5, 15, 17 },
        { 4 }
    });
    checkLabelCount(q, FIELD_WITH_OFFSETS, new int[]{ 2, 2, 0, 2, 0, 0 });
  }

  public void testReqOptNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_DOCS_ONLY, FIELD_FREQS }) {
      Query q = new BooleanQuery.Builder()
          .add(new TermQuery(new Term(field, "w1")), BooleanClause.Occur.SHOULD)
          .add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST)
          .build();
      checkNoPositionsMatches(q, field, new boolean[]{ true, true, false, true, false });
    }
  }

  public void testMinShouldMatch() throws IOException {
    Query w1 = NamedMatches.wrapQuery("w1", new TermQuery(new Term(FIELD_WITH_OFFSETS, "w1")));
    Query w3 = NamedMatches.wrapQuery("w3", new TermQuery(new Term(FIELD_WITH_OFFSETS, "w3")));
    Query w4 = new TermQuery(new Term(FIELD_WITH_OFFSETS, "w4"));
    Query xx = NamedMatches.wrapQuery("xx", new TermQuery(new Term(FIELD_WITH_OFFSETS, "xx")));
    Query q = new BooleanQuery.Builder()
        .add(w3, BooleanClause.Occur.SHOULD)
        .add(new BooleanQuery.Builder()
            .add(w1, BooleanClause.Occur.SHOULD)
            .add(w4, BooleanClause.Occur.SHOULD)
            .add(xx, BooleanClause.Occur.SHOULD)
            .setMinimumNumberShouldMatch(2)
            .build(), BooleanClause.Occur.SHOULD)
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2, 2, 2, 6, 8, 3, 3, 9, 11 },
        { 1, 1, 1, 3, 5, 3, 3, 9, 11 },
        { 2, 0, 0, 0, 2, 1, 1, 3, 5, 4, 4, 12, 14 },
        { 3, 0, 0, 0, 2, 2, 2, 6, 8, 3, 3, 9, 11, 5, 5, 15, 17 },
        { 4 }
    });
    checkLabelCount(q, FIELD_WITH_OFFSETS, new int[]{ 3, 1, 3, 3, 0, 0 });
    assertIsLeafMatch(q, FIELD_WITH_OFFSETS);
    checkSubMatches(q, new String[][]{ {"w1", "w3"}, {"w3"}, {"w1", "xx"}, {"w1", "w3"}, {}, {}});
  }

  public void testMinShouldMatchNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_FREQS, FIELD_DOCS_ONLY }) {
      Query q = new BooleanQuery.Builder()
          .add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.SHOULD)
          .add(new BooleanQuery.Builder()
              .add(new TermQuery(new Term(field, "w1")), BooleanClause.Occur.SHOULD)
              .add(new TermQuery(new Term(field, "w4")), BooleanClause.Occur.SHOULD)
              .add(new TermQuery(new Term(field, "xx")), BooleanClause.Occur.SHOULD)
              .setMinimumNumberShouldMatch(2)
              .build(), BooleanClause.Occur.SHOULD)
          .build();
      checkNoPositionsMatches(q, field, new boolean[]{ true, true, true, true, false });
    }
  }

  public void testExclusion() throws IOException {
    Query q = new BooleanQuery.Builder()
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "w3")), BooleanClause.Occur.SHOULD)
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "zz")), BooleanClause.Occur.MUST_NOT)
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 2, 2, 6, 8 },
        { 1 },
        { 2 },
        { 3, 5, 5, 15, 17 },
        { 4 }
    });
  }

  public void testExclusionNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_FREQS, FIELD_DOCS_ONLY }) {
      Query q = new BooleanQuery.Builder()
          .add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.SHOULD)
          .add(new TermQuery(new Term(field, "zz")), BooleanClause.Occur.MUST_NOT)
          .build();
      checkNoPositionsMatches(q, field, new boolean[]{ true, false, false, true, false });
    }
  }

  public void testConjunction() throws IOException {
    Query q = new BooleanQuery.Builder()
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "w3")), BooleanClause.Occur.MUST)
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "w4")), BooleanClause.Occur.MUST)
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 2, 2, 6, 8, 3, 3, 9, 11 },
        { 1 },
        { 2 },
        { 3, 3, 3, 9, 11, 5, 5, 15, 17 },
        { 4 }
    });
  }

  public void testConjunctionNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_FREQS, FIELD_DOCS_ONLY }) {
      Query q = new BooleanQuery.Builder()
          .add(new TermQuery(new Term(field, "w3")), BooleanClause.Occur.MUST)
          .add(new TermQuery(new Term(field, "w4")), BooleanClause.Occur.MUST)
          .build();
      checkNoPositionsMatches(q, field, new boolean[]{ true, false, false, true, false });
    }
  }

  public void testWildcards() throws IOException {
    Query q = new PrefixQuery(new Term(FIELD_WITH_OFFSETS, "x"));
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0 },
        { 1 },
        { 2, 1, 1, 3, 5 },
        { 3 },
        { 4 }
    });

    Query rq = new RegexpQuery(new Term(FIELD_WITH_OFFSETS, "w[1-2]"));
    checkMatches(rq, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2, 1, 1, 3, 5 },
        { 1, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 2, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 3, 0, 0, 0, 2, 1, 1, 3, 5, 2, 2, 6, 8, 4, 4, 12, 14 },
        { 4 }
    });
    checkLabelCount(rq, FIELD_WITH_OFFSETS, new int[]{ 1, 1, 1, 1, 0 });
    assertIsLeafMatch(rq, FIELD_WITH_OFFSETS);

  }

  public void testNoMatchWildcards() throws IOException {
    Query nomatch = new PrefixQuery(new Term(FIELD_WITH_OFFSETS, "wibble"));
    Matches matches = searcher.createWeight(searcher.rewrite(nomatch), ScoreMode.COMPLETE_NO_SCORES, 1)
        .matches(searcher.leafContexts.get(0), 0);
    assertNull(matches);
  }

  public void testWildcardsNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_FREQS, FIELD_DOCS_ONLY }) {
      Query q = new PrefixQuery(new Term(field, "x"));
      checkNoPositionsMatches(q, field, new boolean[]{ false, false, true, false, false });
    }
  }

  public void testSynonymQuery() throws IOException {
    Query q = new SynonymQuery.Builder(FIELD_WITH_OFFSETS)
        .addTerm(new Term(FIELD_WITH_OFFSETS, "w1")).addTerm(new Term(FIELD_WITH_OFFSETS, "w2"))
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2, 1, 1, 3, 5 },
        { 1, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 2, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 3, 0, 0, 0, 2, 1, 1, 3, 5, 2, 2, 6, 8, 4, 4, 12, 14 },
        { 4 }
    });
    assertIsLeafMatch(q, FIELD_WITH_OFFSETS);
  }

  public void testSynonymQueryNoPositions() throws IOException {
    for (String field : new String[]{ FIELD_FREQS, FIELD_DOCS_ONLY }) {
      Query q = new SynonymQuery.Builder(field).addTerm(new Term(field, "w1")).addTerm(new Term(field, "w2")).build();
      checkNoPositionsMatches(q, field, new boolean[]{ true, true, true, true, false });
    }
  }

  public void testMultipleFields() throws IOException {
    Query q = new BooleanQuery.Builder()
        .add(new TermQuery(new Term("id", "1")), BooleanClause.Occur.SHOULD)
        .add(new TermQuery(new Term(FIELD_WITH_OFFSETS, "w3")), BooleanClause.Occur.MUST)
        .build();
    Weight w = searcher.createWeight(searcher.rewrite(q), ScoreMode.COMPLETE, 1);

    LeafReaderContext ctx = searcher.leafContexts.get(ReaderUtil.subIndex(1, searcher.leafContexts));
    Matches m = w.matches(ctx, 1 - ctx.docBase);
    assertNotNull(m);
    checkFieldMatches(m.getMatches("id"), new int[]{ -1, 0, 0, -1, -1 });
    checkFieldMatches(m.getMatches(FIELD_WITH_OFFSETS), new int[]{ -1, 1, 1, 3, 5, 3, 3, 9, 11 });
    assertNull(m.getMatches("bogus"));

    Set<String> fields = new HashSet<>();
    for (String field : m) {
      fields.add(field);
    }
    assertEquals(2, fields.size());
    assertTrue(fields.contains(FIELD_WITH_OFFSETS));
    assertTrue(fields.contains("id"));

    assertEquals(2, AssertingMatches.unWrap(m).getSubMatches().size());
  }

  //  0         1         2         3         4         5         6         7
  // "a phrase sentence with many phrase sentence iterations of a phrase sentence",

  public void testSloppyPhraseQueryWithRepeats() throws IOException {
    Term p = new Term(FIELD_WITH_OFFSETS, "phrase");
    Term s = new Term(FIELD_WITH_OFFSETS, "sentence");
    PhraseQuery pq = new PhraseQuery(10, FIELD_WITH_OFFSETS, "phrase", "sentence", "sentence");
    checkMatches(pq, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 1, 6, 2, 43, 2, 11, 9, 75, 5, 11, 28, 75, 6, 11, 35, 75 }
    });
    checkLabelCount(pq, FIELD_WITH_OFFSETS, new int[]{ 0, 0, 0, 0, 1 });
    assertIsLeafMatch(pq, FIELD_WITH_OFFSETS);
  }

  public void testSloppyPhraseQuery() throws IOException {
    PhraseQuery pq = new PhraseQuery(4, FIELD_WITH_OFFSETS, "a", "sentence");
    checkMatches(pq, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 0, 2, 0, 17, 6, 9, 35, 59, 9, 11, 58, 75 }
    });
    assertIsLeafMatch(pq, FIELD_WITH_OFFSETS);
  }

  public void testExactPhraseQuery() throws IOException {
    PhraseQuery pq = new PhraseQuery(FIELD_WITH_OFFSETS, "phrase", "sentence");
    checkMatches(pq, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 1, 2, 2, 17, 5, 6, 28, 43, 10, 11, 60, 75 }
    });

    Term a = new Term(FIELD_WITH_OFFSETS, "a");
    Term s = new Term(FIELD_WITH_OFFSETS, "sentence");
    PhraseQuery pq2 = new PhraseQuery.Builder()
        .add(a)
        .add(s, 2)
        .build();
    checkMatches(pq2, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 0, 2, 0, 17, 9, 11, 58, 75 }
    });
    assertIsLeafMatch(pq2, FIELD_WITH_OFFSETS);
  }

  //  0         1         2         3         4         5         6         7
  // "a phrase sentence with many phrase sentence iterations of a phrase sentence",

  public void testSloppyMultiPhraseQuery() throws IOException {
    Term p = new Term(FIELD_WITH_OFFSETS, "phrase");
    Term s = new Term(FIELD_WITH_OFFSETS, "sentence");
    Term i = new Term(FIELD_WITH_OFFSETS, "iterations");
    MultiPhraseQuery mpq = new MultiPhraseQuery.Builder()
        .add(p)
        .add(new Term[]{ s, i })
        .setSlop(4)
        .build();
    checkMatches(mpq, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 1, 2, 2, 17, 5, 6, 28, 43, 5, 7, 28, 54, 10, 11, 60, 75 }
    });
    assertIsLeafMatch(mpq, FIELD_WITH_OFFSETS);
  }

  public void testExactMultiPhraseQuery() throws IOException {
    MultiPhraseQuery mpq = new MultiPhraseQuery.Builder()
        .add(new Term(FIELD_WITH_OFFSETS, "sentence"))
        .add(new Term[]{ new Term(FIELD_WITH_OFFSETS, "with"), new Term(FIELD_WITH_OFFSETS, "iterations") })
        .build();
    checkMatches(mpq, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 2, 3, 9, 22, 6, 7, 35, 54 }
    });

    MultiPhraseQuery mpq2 = new MultiPhraseQuery.Builder()
        .add(new Term[]{ new Term(FIELD_WITH_OFFSETS, "a"), new Term(FIELD_WITH_OFFSETS, "many")})
        .add(new Term(FIELD_WITH_OFFSETS, "phrase"))
        .build();
    checkMatches(mpq2, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 0, 1, 0, 8, 4, 5, 23, 34, 9, 10, 58, 66 }
    });
    assertIsLeafMatch(mpq2, FIELD_WITH_OFFSETS);
  }

  //  0         1         2         3         4         5         6         7
  // "a phrase sentence with many phrase sentence iterations of a phrase sentence",

  public void testSpanQuery() throws IOException {
    SpanQuery subq = SpanNearQuery.newOrderedNearQuery(FIELD_WITH_OFFSETS)
        .addClause(new SpanTermQuery(new Term(FIELD_WITH_OFFSETS, "with")))
        .addClause(new SpanTermQuery(new Term(FIELD_WITH_OFFSETS, "many")))
        .build();
    Query q = SpanNearQuery.newOrderedNearQuery(FIELD_WITH_OFFSETS)
        .addClause(new SpanTermQuery(new Term(FIELD_WITH_OFFSETS, "sentence")))
        .addClause(new SpanOrQuery(subq, new SpanTermQuery(new Term(FIELD_WITH_OFFSETS, "iterations"))))
        .build();
    checkMatches(q, FIELD_WITH_OFFSETS, new int[][]{
        { 0 }, { 1 }, { 2 }, { 3 },
        { 4, 2, 4, 9, 27, 6, 7, 35, 54 }
    });
    checkLabelCount(q, FIELD_WITH_OFFSETS, new int[]{ 0, 0, 0, 0, 1 });
    checkTermMatches(q, FIELD_WITH_OFFSETS, new TermMatch[][][]{
        {}, {}, {}, {},
        {
            {
                new TermMatch(2, 9, 17),
                new TermMatch(3, 18, 22),
                new TermMatch(4, 23, 27)
            }, {
              new TermMatch(6, 35, 43), new TermMatch(7, 44, 54)
        }
        }
    });
  }

  public void testPointQuery() throws IOException {
    IndexOrDocValuesQuery pointQuery = new IndexOrDocValuesQuery(
        IntPoint.newExactQuery(FIELD_POINT, 10),
        NumericDocValuesField.newSlowExactQuery(FIELD_POINT, 10)
    );
    Term t = new Term(FIELD_WITH_OFFSETS, "w1");
    Query query = new BooleanQuery.Builder()
        .add(new TermQuery(t), BooleanClause.Occur.MUST)
        .add(pointQuery, BooleanClause.Occur.MUST)
        .build();

    checkMatches(pointQuery, FIELD_WITH_OFFSETS, new int[][]{});

    checkMatches(query, FIELD_WITH_OFFSETS, new int[][]{
        { 0, 0, 0, 0, 2 },
        { 1, 0, 0, 0, 2 },
        { 2, 0, 0, 0, 2 },
        { 3, 0, 0, 0, 2, 2, 2, 6, 8 },
        { 4 }
    });

    pointQuery = new IndexOrDocValuesQuery(
        IntPoint.newExactQuery(FIELD_POINT, 11),
        NumericDocValuesField.newSlowExactQuery(FIELD_POINT, 11)
    );

    query = new BooleanQuery.Builder()
        .add(new TermQuery(t), BooleanClause.Occur.MUST)
        .add(pointQuery, BooleanClause.Occur.MUST)
        .build();
    checkMatches(query, FIELD_WITH_OFFSETS, new int[][]{});

    query = new BooleanQuery.Builder()
        .add(new TermQuery(t), BooleanClause.Occur.MUST)
        .add(pointQuery, BooleanClause.Occur.SHOULD)
        .build();
    checkMatches(query, FIELD_WITH_OFFSETS, new int[][]{
        {0, 0, 0, 0, 2},
        {1, 0, 0, 0, 2},
        {2, 0, 0, 0, 2},
        {3, 0, 0, 0, 2, 2, 2, 6, 8},
        {4}
    });
  }

  public void testMinimalSeekingWithWildcards() throws IOException {
    SeekCountingLeafReader reader = new SeekCountingLeafReader(getOnlyLeafReader(this.reader));
    this.searcher = new IndexSearcher(reader);
    Query query = new PrefixQuery(new Term(FIELD_WITH_OFFSETS, "w"));
    Weight w = searcher.createWeight(query.rewrite(reader), ScoreMode.COMPLETE, 1);

    // docs 0-3 match several different terms here, but we only seek to the first term and
    // then short-cut return; other terms are ignored until we try and iterate over matches
    int[] expectedSeeks = new int[]{ 1, 1, 1, 1, 6, 6 };
    int i = 0;
    for (LeafReaderContext ctx : reader.leaves()) {
      for (int doc = 0; doc < ctx.reader().maxDoc(); doc++) {
        reader.seeks = 0;
        w.matches(ctx, doc);
        assertEquals("Unexpected seek count on doc " + doc, expectedSeeks[i], reader.seeks);
        i++;
      }
    }
  }

  private static class SeekCountingLeafReader extends FilterLeafReader {

    int seeks = 0;

    public SeekCountingLeafReader(LeafReader in) {
      super(in);
    }

    @Override
    public Terms terms(String field) throws IOException {
      Terms terms = super.terms(field);
      if (terms == null) {
        return null;
      }
      return new FilterTerms(terms) {
        @Override
        public TermsEnum iterator() throws IOException {
          return new FilterTermsEnum(super.iterator()) {
            @Override
            public boolean seekExact(BytesRef text) throws IOException {
              seeks++;
              return super.seekExact(text);
            }
          };
        }
      };
    }

    @Override
    public CacheHelper getCoreCacheHelper() {
      return null;
    }

    @Override
    public CacheHelper getReaderCacheHelper() {
      return null;
    }
  }

}