package org.apache.lucene.index;

import java.io.IOException;
import java.util.Iterator;

import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.VirtualMethod;
import org.apache.lucene.util.automaton.CompiledAutomaton;

/*
 * 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.
 */

/**
 * A {@link FilterLeafReader} that can be used to apply
 * additional checks for tests.
 */
public class AssertingLeafReader extends FilterLeafReader {

  private static void assertThread(String object, Thread creationThread) {
    if (creationThread != Thread.currentThread()) {
      throw new AssertionError(object + " are only supposed to be consumed in "
          + "the thread in which they have been acquired. But was acquired in "
          + creationThread + " and consumed in " + Thread.currentThread() + ".");
    }
  }

  public AssertingLeafReader(LeafReader in) {
    super(in);
    // check some basic reader sanity
    assert in.maxDoc() >= 0;
    assert in.numDocs() <= in.maxDoc();
    assert in.numDeletedDocs() + in.numDocs() == in.maxDoc();
    assert !in.hasDeletions() || in.numDeletedDocs() > 0 && in.numDocs() < in.maxDoc();

    addCoreClosedListener(new CoreClosedListener() {
      @Override
      public void onClose(Object ownerCoreCacheKey) throws IOException {
        final Object expectedKey = getCoreCacheKey();
        assert expectedKey == ownerCoreCacheKey
            : "Core closed listener called on a different key " + expectedKey + " <> " + ownerCoreCacheKey;
      }
    });
  }

  @Override
  public Fields fields() throws IOException {
    return new AssertingFields(super.fields());
  }
  
  @Override
  public Fields getTermVectors(int docID) throws IOException {
    Fields fields = super.getTermVectors(docID);
    return fields == null ? null : new AssertingFields(fields);
  }

  /**
   * Wraps a Fields but with additional asserts
   */
  public static class AssertingFields extends FilterFields {
    public AssertingFields(Fields in) {
      super(in);
    }

    @Override
    public Iterator<String> iterator() {
      Iterator<String> iterator = super.iterator();
      assert iterator != null;
      return iterator;
    }

    @Override
    public Terms terms(String field) throws IOException {
      Terms terms = super.terms(field);
      return terms == null ? null : new AssertingTerms(terms);
    }
  }
  
  /**
   * Wraps a Terms but with additional asserts
   */
  public static class AssertingTerms extends FilterTerms {
    public AssertingTerms(Terms in) {
      super(in);
    }

    @Override
    public TermsEnum intersect(CompiledAutomaton automaton, BytesRef bytes) throws IOException {
      TermsEnum termsEnum = in.intersect(automaton, bytes);
      assert termsEnum != null;
      assert bytes == null || bytes.isValid();
      return new AssertingTermsEnum(termsEnum);
    }

    @Override
    public BytesRef getMin() throws IOException {
      BytesRef v = in.getMin();
      assert v == null || v.isValid();
      return v;
    }

    @Override
    public BytesRef getMax() throws IOException {
      BytesRef v = in.getMax();
      assert v == null || v.isValid();
      return v;
    }

    @Override
    public TermsEnum iterator() throws IOException {
      TermsEnum termsEnum = super.iterator();
      assert termsEnum != null;
      return new AssertingTermsEnum(termsEnum);
    }
  }
  
  static final VirtualMethod<TermsEnum> SEEK_EXACT = new VirtualMethod<>(TermsEnum.class, "seekExact", BytesRef.class);

  static class AssertingTermsEnum extends FilterTermsEnum {
    private final Thread creationThread = Thread.currentThread();
    private enum State {INITIAL, POSITIONED, UNPOSITIONED};
    private State state = State.INITIAL;
    private final boolean delegateOverridesSeekExact;

    public AssertingTermsEnum(TermsEnum in) {
      super(in);
      delegateOverridesSeekExact = SEEK_EXACT.isOverriddenAsOf(in.getClass());
    }

    @Override
    public PostingsEnum postings(PostingsEnum reuse, int flags) throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.POSITIONED: "docs(...) called on unpositioned TermsEnum";

      // reuse if the codec reused
      final PostingsEnum actualReuse;
      if (reuse instanceof AssertingPostingsEnum) {
        actualReuse = ((AssertingPostingsEnum) reuse).in;
      } else {
        actualReuse = null;
      }
      PostingsEnum docs = super.postings(actualReuse, flags);
      if (docs == null) {
        assert PostingsEnum.featureRequested(flags, DocsAndPositionsEnum.OLD_NULL_SEMANTICS);
        return null;
      }
      if (docs == actualReuse) {
        // codec reused, reset asserting state
        ((AssertingPostingsEnum)reuse).reset();
        return reuse;
      } else {
        return new AssertingPostingsEnum(docs);
      }
    }

    // TODO: we should separately track if we are 'at the end' ?
    // someone should not call next() after it returns null!!!!
    @Override
    public BytesRef next() throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.INITIAL || state == State.POSITIONED: "next() called on unpositioned TermsEnum";
      BytesRef result = super.next();
      if (result == null) {
        state = State.UNPOSITIONED;
      } else {
        assert result.isValid();
        state = State.POSITIONED;
      }
      return result;
    }

    @Override
    public long ord() throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.POSITIONED : "ord() called on unpositioned TermsEnum";
      return super.ord();
    }

    @Override
    public int docFreq() throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.POSITIONED : "docFreq() called on unpositioned TermsEnum";
      return super.docFreq();
    }

    @Override
    public long totalTermFreq() throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.POSITIONED : "totalTermFreq() called on unpositioned TermsEnum";
      return super.totalTermFreq();
    }

    @Override
    public BytesRef term() throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.POSITIONED : "term() called on unpositioned TermsEnum";
      BytesRef ret = super.term();
      assert ret == null || ret.isValid();
      return ret;
    }

    @Override
    public void seekExact(long ord) throws IOException {
      assertThread("Terms enums", creationThread);
      super.seekExact(ord);
      state = State.POSITIONED;
    }

    @Override
    public SeekStatus seekCeil(BytesRef term) throws IOException {
      assertThread("Terms enums", creationThread);
      assert term.isValid();
      SeekStatus result = super.seekCeil(term);
      if (result == SeekStatus.END) {
        state = State.UNPOSITIONED;
      } else {
        state = State.POSITIONED;
      }
      return result;
    }

    @Override
    public boolean seekExact(BytesRef text) throws IOException {
      assertThread("Terms enums", creationThread);
      assert text.isValid();
      boolean result;
      if (delegateOverridesSeekExact) {
        result = in.seekExact(text);
      } else {
        result = super.seekExact(text);
      }
      if (result) {
        state = State.POSITIONED;
      } else {
        state = State.UNPOSITIONED;
      }
      return result;
    }

    @Override
    public TermState termState() throws IOException {
      assertThread("Terms enums", creationThread);
      assert state == State.POSITIONED : "termState() called on unpositioned TermsEnum";
      return in.termState();
    }

    @Override
    public void seekExact(BytesRef term, TermState state) throws IOException {
      assertThread("Terms enums", creationThread);
      assert term.isValid();
      in.seekExact(term, state);
      this.state = State.POSITIONED;
    }

    @Override
    public String toString() {
      return "AssertingTermsEnum(" + in + ")";
    }
    
    void reset() {
      state = State.INITIAL;
    }
  }
  
  static enum DocsEnumState { START, ITERATING, FINISHED };

  /** Wraps a docsenum with additional checks */
  public static class AssertingPostingsEnum extends FilterPostingsEnum {
    private final Thread creationThread = Thread.currentThread();
    private DocsEnumState state = DocsEnumState.START;
    int positionCount = 0;
    int positionMax = 0;
    private int doc;

    public AssertingPostingsEnum(PostingsEnum in) {
      super(in);
      this.doc = in.docID();
    }

    @Override
    public int nextDoc() throws IOException {
      assertThread("Docs enums", creationThread);
      assert state != DocsEnumState.FINISHED : "nextDoc() called after NO_MORE_DOCS";
      int nextDoc = super.nextDoc();
      assert nextDoc > doc : "backwards nextDoc from " + doc + " to " + nextDoc + " " + in;
      if (nextDoc == DocIdSetIterator.NO_MORE_DOCS) {
        state = DocsEnumState.FINISHED;
        positionMax = 0;
      } else {
        state = DocsEnumState.ITERATING;
        positionMax = super.freq();
      }
      positionCount = 0;
      assert super.docID() == nextDoc;
      return doc = nextDoc;
    }

    @Override
    public int advance(int target) throws IOException {
      assertThread("Docs enums", creationThread);
      assert state != DocsEnumState.FINISHED : "advance() called after NO_MORE_DOCS";
      assert target > doc : "target must be > docID(), got " + target + " <= " + doc;
      int advanced = super.advance(target);
      assert advanced >= target : "backwards advance from: " + target + " to: " + advanced;
      if (advanced == DocIdSetIterator.NO_MORE_DOCS) {
        state = DocsEnumState.FINISHED;
        positionMax = 0;
      } else {
        state = DocsEnumState.ITERATING;
        positionMax = super.freq();
      }
      positionCount = 0;
      assert super.docID() == advanced;
      return doc = advanced;
    }

    @Override
    public int docID() {
      assertThread("Docs enums", creationThread);
      assert doc == super.docID() : " invalid docID() in " + in.getClass() + " " + super.docID() + " instead of " + doc;
      return doc;
    }

    @Override
    public int freq() throws IOException {
      assertThread("Docs enums", creationThread);
      assert state != DocsEnumState.START : "freq() called before nextDoc()/advance()";
      assert state != DocsEnumState.FINISHED : "freq() called after NO_MORE_DOCS";
      int freq = super.freq();
      assert freq > 0;
      return freq;
    }

    @Override
    public int nextPosition() throws IOException {
      assert state != DocsEnumState.START : "nextPosition() called before nextDoc()/advance()";
      assert state != DocsEnumState.FINISHED : "nextPosition() called after NO_MORE_DOCS";
      assert positionCount < positionMax : "nextPosition() called more than freq() times!";
      int position = super.nextPosition();
      assert position >= 0 || position == -1 : "invalid position: " + position;
      positionCount++;
      return position;
    }

    @Override
    public int startOffset() throws IOException {
      assert state != DocsEnumState.START : "startOffset() called before nextDoc()/advance()";
      assert state != DocsEnumState.FINISHED : "startOffset() called after NO_MORE_DOCS";
      assert positionCount > 0 : "startOffset() called before nextPosition()!";
      return super.startOffset();
    }

    @Override
    public int endOffset() throws IOException {
      assert state != DocsEnumState.START : "endOffset() called before nextDoc()/advance()";
      assert state != DocsEnumState.FINISHED : "endOffset() called after NO_MORE_DOCS";
      assert positionCount > 0 : "endOffset() called before nextPosition()!";
      return super.endOffset();
    }

    @Override
    public BytesRef getPayload() throws IOException {
      assert state != DocsEnumState.START : "getPayload() called before nextDoc()/advance()";
      assert state != DocsEnumState.FINISHED : "getPayload() called after NO_MORE_DOCS";
      assert positionCount > 0 : "getPayload() called before nextPosition()!";
      BytesRef payload = super.getPayload();
      assert payload == null || payload.length > 0 : "getPayload() returned payload with invalid length!";
      return payload;
    }
    
    void reset() {
      state = DocsEnumState.START;
      doc = in.docID();
      positionCount = positionMax = 0;
    }
  }

  /** Wraps a NumericDocValues but with additional asserts */
  public static class AssertingNumericDocValues extends NumericDocValues {
    private final Thread creationThread = Thread.currentThread();
    private final NumericDocValues in;
    private final int maxDoc;
    
    public AssertingNumericDocValues(NumericDocValues in, int maxDoc) {
      this.in = in;
      this.maxDoc = maxDoc;
    }

    @Override
    public long get(int docID) {
      assertThread("Numeric doc values", creationThread);
      assert docID >= 0 && docID < maxDoc;
      return in.get(docID);
    }    
  }
  
  /** Wraps a BinaryDocValues but with additional asserts */
  public static class AssertingBinaryDocValues extends BinaryDocValues {
    private final Thread creationThread = Thread.currentThread();
    private final BinaryDocValues in;
    private final int maxDoc;
    
    public AssertingBinaryDocValues(BinaryDocValues in, int maxDoc) {
      this.in = in;
      this.maxDoc = maxDoc;
    }

    @Override
    public BytesRef get(int docID) {
      assertThread("Binary doc values", creationThread);
      assert docID >= 0 && docID < maxDoc;
      final BytesRef result = in.get(docID);
      assert result.isValid();
      return result;
    }
  }
  
  /** Wraps a SortedDocValues but with additional asserts */
  public static class AssertingSortedDocValues extends SortedDocValues {
    private final Thread creationThread = Thread.currentThread();
    private final SortedDocValues in;
    private final int maxDoc;
    private final int valueCount;
    
    public AssertingSortedDocValues(SortedDocValues in, int maxDoc) {
      this.in = in;
      this.maxDoc = maxDoc;
      this.valueCount = in.getValueCount();
      assert valueCount >= 0 && valueCount <= maxDoc;
    }

    @Override
    public int getOrd(int docID) {
      assertThread("Sorted doc values", creationThread);
      assert docID >= 0 && docID < maxDoc;
      int ord = in.getOrd(docID);
      assert ord >= -1 && ord < valueCount;
      return ord;
    }

    @Override
    public BytesRef lookupOrd(int ord) {
      assertThread("Sorted doc values", creationThread);
      assert ord >= 0 && ord < valueCount;
      final BytesRef result = in.lookupOrd(ord);
      assert result.isValid();
      return result;
    }

    @Override
    public int getValueCount() {
      assertThread("Sorted doc values", creationThread);
      int valueCount = in.getValueCount();
      assert valueCount == this.valueCount; // should not change
      return valueCount;
    }

    @Override
    public BytesRef get(int docID) {
      assertThread("Sorted doc values", creationThread);
      assert docID >= 0 && docID < maxDoc;
      final BytesRef result = in.get(docID);
      assert result.isValid();
      return result;
    }

    @Override
    public int lookupTerm(BytesRef key) {
      assertThread("Sorted doc values", creationThread);
      assert key.isValid();
      int result = in.lookupTerm(key);
      assert result < valueCount;
      assert key.isValid();
      return result;
    }
  }
  
  /** Wraps a SortedSetDocValues but with additional asserts */
  public static class AssertingSortedNumericDocValues extends SortedNumericDocValues {
    private final Thread creationThread = Thread.currentThread();
    private final SortedNumericDocValues in;
    private final int maxDoc;
    
    public AssertingSortedNumericDocValues(SortedNumericDocValues in, int maxDoc) {
      this.in = in;
      this.maxDoc = maxDoc;
    }

    @Override
    public void setDocument(int doc) {
      assertThread("Sorted numeric doc values", creationThread);
      assert doc >= 0 && doc < maxDoc;
      in.setDocument(doc);
      // check the values are actually sorted
      long previous = Long.MIN_VALUE;
      for (int i = 0; i < in.count(); i++) {
        long v = in.valueAt(i);
        assert v >= previous;
        previous = v;
      }
    }

    @Override
    public long valueAt(int index) {
      assertThread("Sorted numeric doc values", creationThread);
      assert index < in.count();
      return in.valueAt(index);
    }

    @Override
    public int count() {
      assertThread("Sorted numeric doc values", creationThread);
      return in.count();
    } 
  }
  
  /** Wraps a RandomAccessOrds but with additional asserts */
  public static class AssertingRandomAccessOrds extends RandomAccessOrds {
    private final Thread creationThread = Thread.currentThread();
    private final RandomAccessOrds in;
    private final int maxDoc;
    private final long valueCount;
    long lastOrd = NO_MORE_ORDS;
    
    public AssertingRandomAccessOrds(RandomAccessOrds in, int maxDoc) {
      this.in = in;
      this.maxDoc = maxDoc;
      this.valueCount = in.getValueCount();
      assert valueCount >= 0;
    }
    
    @Override
    public long nextOrd() {
      assertThread("Sorted numeric doc values", creationThread);
      assert lastOrd != NO_MORE_ORDS;
      long ord = in.nextOrd();
      assert ord < valueCount;
      assert ord == NO_MORE_ORDS || ord > lastOrd;
      lastOrd = ord;
      return ord;
    }

    @Override
    public void setDocument(int docID) {
      assertThread("Sorted numeric doc values", creationThread);
      assert docID >= 0 && docID < maxDoc : "docid=" + docID + ",maxDoc=" + maxDoc;
      in.setDocument(docID);
      lastOrd = -2;
    }

    @Override
    public BytesRef lookupOrd(long ord) {
      assertThread("Sorted numeric doc values", creationThread);
      assert ord >= 0 && ord < valueCount;
      final BytesRef result = in.lookupOrd(ord);
      assert result.isValid();
      return result;
    }

    @Override
    public long getValueCount() {
      assertThread("Sorted numeric doc values", creationThread);
      long valueCount = in.getValueCount();
      assert valueCount == this.valueCount; // should not change
      return valueCount;
    }

    @Override
    public long lookupTerm(BytesRef key) {
      assertThread("Sorted numeric doc values", creationThread);
      assert key.isValid();
      long result = in.lookupTerm(key);
      assert result < valueCount;
      assert key.isValid();
      return result;
    }

    @Override
    public long ordAt(int index) {
      assertThread("Sorted numeric doc values", creationThread);
      assert index < in.cardinality();
      long ord = in.ordAt(index);
      assert ord >= 0 && ord < valueCount;
      return ord;
    }

    @Override
    public int cardinality() {
      assertThread("Sorted numeric doc values", creationThread);
      int cardinality = in.cardinality();
      assert cardinality >= 0;
      return cardinality;
    }
  }
  
  /** Wraps a SortedSetDocValues but with additional asserts */
  public static class AssertingSortedSetDocValues extends SortedSetDocValues {
    private final Thread creationThread = Thread.currentThread();
    private final SortedSetDocValues in;
    private final int maxDoc;
    private final long valueCount;
    long lastOrd = NO_MORE_ORDS;
    
    public AssertingSortedSetDocValues(SortedSetDocValues in, int maxDoc) {
      this.in = in;
      this.maxDoc = maxDoc;
      this.valueCount = in.getValueCount();
      assert valueCount >= 0;
    }
    
    @Override
    public long nextOrd() {
      assertThread("Sorted numeric doc values", creationThread);
      assert lastOrd != NO_MORE_ORDS;
      long ord = in.nextOrd();
      assert ord < valueCount;
      assert ord == NO_MORE_ORDS || ord > lastOrd;
      lastOrd = ord;
      return ord;
    }

    @Override
    public void setDocument(int docID) {
      assertThread("Sorted numeric doc values", creationThread);
      assert docID >= 0 && docID < maxDoc : "docid=" + docID + ",maxDoc=" + maxDoc;
      in.setDocument(docID);
      lastOrd = -2;
    }

    @Override
    public BytesRef lookupOrd(long ord) {
      assertThread("Sorted numeric doc values", creationThread);
      assert ord >= 0 && ord < valueCount;
      final BytesRef result = in.lookupOrd(ord);
      assert result.isValid();
      return result;
    }

    @Override
    public long getValueCount() {
      assertThread("Sorted numeric doc values", creationThread);
      long valueCount = in.getValueCount();
      assert valueCount == this.valueCount; // should not change
      return valueCount;
    }

    @Override
    public long lookupTerm(BytesRef key) {
      assertThread("Sorted numeric doc values", creationThread);
      assert key.isValid();
      long result = in.lookupTerm(key);
      assert result < valueCount;
      assert key.isValid();
      return result;
    }
  }
  
  @Override
  public NumericDocValues getNumericDocValues(String field) throws IOException {
    NumericDocValues dv = super.getNumericDocValues(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (dv != null) {
      assert fi != null;
      assert fi.getDocValuesType() == DocValuesType.NUMERIC;
      return new AssertingNumericDocValues(dv, maxDoc());
    } else {
      assert fi == null || fi.getDocValuesType() != DocValuesType.NUMERIC;
      return null;
    }
  }

  @Override
  public BinaryDocValues getBinaryDocValues(String field) throws IOException {
    BinaryDocValues dv = super.getBinaryDocValues(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (dv != null) {
      assert fi != null;
      assert fi.getDocValuesType() == DocValuesType.BINARY;
      return new AssertingBinaryDocValues(dv, maxDoc());
    } else {
      assert fi == null || fi.getDocValuesType() != DocValuesType.BINARY;
      return null;
    }
  }

  @Override
  public SortedDocValues getSortedDocValues(String field) throws IOException {
    SortedDocValues dv = super.getSortedDocValues(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (dv != null) {
      assert fi != null;
      assert fi.getDocValuesType() == DocValuesType.SORTED;
      return new AssertingSortedDocValues(dv, maxDoc());
    } else {
      assert fi == null || fi.getDocValuesType() != DocValuesType.SORTED;
      return null;
    }
  }
  
  @Override
  public SortedNumericDocValues getSortedNumericDocValues(String field) throws IOException {
    SortedNumericDocValues dv = super.getSortedNumericDocValues(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (dv != null) {
      assert fi != null;
      assert fi.getDocValuesType() == DocValuesType.SORTED_NUMERIC;
      return new AssertingSortedNumericDocValues(dv, maxDoc());
    } else {
      assert fi == null || fi.getDocValuesType() != DocValuesType.SORTED_NUMERIC;
      return null;
    }
  }

  @Override
  public SortedSetDocValues getSortedSetDocValues(String field) throws IOException {
    SortedSetDocValues dv = super.getSortedSetDocValues(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (dv != null) {
      assert fi != null;
      assert fi.getDocValuesType() == DocValuesType.SORTED_SET;
      if (dv instanceof RandomAccessOrds) {
        return new AssertingRandomAccessOrds((RandomAccessOrds) dv, maxDoc());
      } else {
        return new AssertingSortedSetDocValues(dv, maxDoc());
      }
    } else {
      assert fi == null || fi.getDocValuesType() != DocValuesType.SORTED_SET;
      return null;
    }
  }

  @Override
  public NumericDocValues getNormValues(String field) throws IOException {
    NumericDocValues dv = super.getNormValues(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (dv != null) {
      assert fi != null;
      assert fi.hasNorms();
      return new AssertingNumericDocValues(dv, maxDoc());
    } else {
      assert fi == null || fi.hasNorms() == false;
      return null;
    }
  }
  
  /** Wraps a Bits but with additional asserts */
  public static class AssertingBits implements Bits {
    private final Thread creationThread = Thread.currentThread();
    final Bits in;
    
    public AssertingBits(Bits in) {
      this.in = in;
    }
    
    @Override
    public boolean get(int index) {
      assertThread("Bits", creationThread);
      assert index >= 0 && index < length();
      return in.get(index);
    }

    @Override
    public int length() {
      assertThread("Bits", creationThread);
      return in.length();
    }
  }

  @Override
  public Bits getLiveDocs() {
    Bits liveDocs = super.getLiveDocs();
    if (liveDocs != null) {
      assert maxDoc() == liveDocs.length();
      liveDocs = new AssertingBits(liveDocs);
    } else {
      assert maxDoc() == numDocs();
      assert !hasDeletions();
    }
    return liveDocs;
  }

  @Override
  public Bits getDocsWithField(String field) throws IOException {
    Bits docsWithField = super.getDocsWithField(field);
    FieldInfo fi = getFieldInfos().fieldInfo(field);
    if (docsWithField != null) {
      assert fi != null;
      assert fi.getDocValuesType() != DocValuesType.NONE;
      assert maxDoc() == docsWithField.length();
      docsWithField = new AssertingBits(docsWithField);
    } else {
      assert fi == null || fi.getDocValuesType() == DocValuesType.NONE;
    }
    return docsWithField;
  }

  // we don't change behavior of the reader: just validate the API.

  @Override
  public Object getCoreCacheKey() {
    return in.getCoreCacheKey();
  }

  @Override
  public Object getCombinedCoreAndDeletesKey() {
    return in.getCombinedCoreAndDeletesKey();
  }
}
