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


import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.mockfile.ExtrasFS;
import org.apache.lucene.search.Sort;
import org.apache.lucene.store.BaseDirectoryWrapper;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.StringHelper;
import org.apache.lucene.util.TestUtil;
import org.apache.lucene.util.Version;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class TestSegmentInfos extends LuceneTestCase {

  public void testIllegalCreatedVersion() {
    IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new SegmentInfos(5));
    assertEquals("indexCreatedVersionMajor must be >= 6, got: 5", e.getMessage());
    e = expectThrows(IllegalArgumentException.class, () -> new SegmentInfos(Version.LATEST.major + 1));
    assertEquals("indexCreatedVersionMajor is in the future: " + (Version.LATEST.major + 1), e.getMessage());
  }

  // LUCENE-5954
  public void testVersionsNoSegments() throws IOException {
    SegmentInfos sis = new SegmentInfos(Version.LATEST.major);
    BaseDirectoryWrapper dir = newDirectory();
    dir.setCheckIndexOnClose(false);
    sis.commit(dir);
    sis = SegmentInfos.readLatestCommit(dir);
    assertNull(sis.getMinSegmentLuceneVersion());
    assertEquals(Version.LATEST, sis.getCommitLuceneVersion());
    dir.close();
  }

  // LUCENE-5954
  public void testVersionsOneSegment() throws IOException {
    BaseDirectoryWrapper dir = newDirectory();
    dir.setCheckIndexOnClose(false);
    byte id[] = StringHelper.randomId();
    Codec codec = Codec.getDefault();

    SegmentInfos sis = new SegmentInfos(Version.LATEST.major);
    SegmentInfo info = new SegmentInfo(dir, Version.LUCENE_9_0_0, Version.LUCENE_9_0_0, "_0", 1, false, Codec.getDefault(),
                                       Collections.<String,String>emptyMap(), id, Collections.<String,String>emptyMap(), null);
    info.setFiles(Collections.<String>emptySet());
    codec.segmentInfoFormat().write(dir, info, IOContext.DEFAULT);
    SegmentCommitInfo commitInfo = new SegmentCommitInfo(info, 0, 0, -1, -1, -1, StringHelper.randomId());

    sis.add(commitInfo);
    sis.commit(dir);
    sis = SegmentInfos.readLatestCommit(dir);
    assertEquals(Version.LUCENE_9_0_0, sis.getMinSegmentLuceneVersion());
    assertEquals(Version.LATEST, sis.getCommitLuceneVersion());
    dir.close();
  }

  // LUCENE-5954
  public void testVersionsTwoSegments() throws IOException {
    BaseDirectoryWrapper dir = newDirectory();
    dir.setCheckIndexOnClose(false);
    byte id[] = StringHelper.randomId();
    Codec codec = Codec.getDefault();

    SegmentInfos sis = new SegmentInfos(Version.LATEST.major);
    SegmentInfo info = new SegmentInfo(dir, Version.LUCENE_9_0_0, Version.LUCENE_9_0_0, "_0", 1, false, Codec.getDefault(),
                                       Collections.<String,String>emptyMap(), id, Collections.<String,String>emptyMap(), null);
    info.setFiles(Collections.<String>emptySet());
    codec.segmentInfoFormat().write(dir, info, IOContext.DEFAULT);
    SegmentCommitInfo commitInfo = new SegmentCommitInfo(info, 0, 0, -1, -1, -1, StringHelper.randomId());
    sis.add(commitInfo);

    info = new SegmentInfo(dir, Version.LUCENE_9_0_0, Version.LUCENE_9_0_0, "_1", 1, false, Codec.getDefault(),
                           Collections.<String,String>emptyMap(), id, Collections.<String,String>emptyMap(), null);
    info.setFiles(Collections.<String>emptySet());
    codec.segmentInfoFormat().write(dir, info, IOContext.DEFAULT);
    commitInfo = new SegmentCommitInfo(info, 0, 0,-1, -1, -1, StringHelper.randomId());
    sis.add(commitInfo);

    sis.commit(dir);
    byte[] commitInfoId0 = sis.info(0).getId();
    byte[] commitInfoId1 = sis.info(1).getId();
    sis = SegmentInfos.readLatestCommit(dir);
    assertEquals(Version.LUCENE_9_0_0, sis.getMinSegmentLuceneVersion());
    assertEquals(Version.LATEST, sis.getCommitLuceneVersion());
    assertEquals(StringHelper.idToString(commitInfoId0), StringHelper.idToString(sis.info(0).getId()));
    assertEquals(StringHelper.idToString(commitInfoId1), StringHelper.idToString(sis.info(1).getId()));
    dir.close();
  }

  /** Test toString method */
  public void testToString() throws Throwable{
    SegmentInfo si;
    final Directory dir = newDirectory();
    Codec codec = Codec.getDefault();

    // diagnostics map
    Map<String, String> diagnostics = Map.of("key1", "value1", "key2", "value2");

    // attributes map
    Map<String,String> attributes =  Map.of("akey1", "value1", "akey2", "value2");

    // diagnostics X, attributes X
    si = new SegmentInfo(dir, Version.LATEST, Version.LATEST, "TEST", 10000, false, codec, Collections.emptyMap(), StringHelper.randomId(), new HashMap<>(), Sort.INDEXORDER);
    assertEquals("TEST(" + Version.LATEST.toString() + ")" +
        ":C10000" +
        ":[indexSort=<doc>]", si.toString());

    // diagnostics O, attributes X
    si = new SegmentInfo(dir, Version.LATEST, Version.LATEST, "TEST", 10000, false, codec, diagnostics, StringHelper.randomId(), new HashMap<>(), Sort.INDEXORDER);
    assertEquals("TEST(" + Version.LATEST.toString() + ")" +
        ":C10000" +
        ":[indexSort=<doc>]" +
        ":[diagnostics=" + diagnostics + "]", si.toString());

    // diagnostics X, attributes O
    si = new SegmentInfo(dir, Version.LATEST, Version.LATEST, "TEST", 10000, false, codec, Collections.emptyMap(), StringHelper.randomId(), attributes, Sort.INDEXORDER);
    assertEquals("TEST(" + Version.LATEST.toString() + ")" +
        ":C10000" +
        ":[indexSort=<doc>]" +
        ":[attributes=" + attributes + "]", si.toString());

    // diagnostics O, attributes O
    si = new SegmentInfo(dir, Version.LATEST, Version.LATEST, "TEST", 10000, false, codec, diagnostics, StringHelper.randomId(), attributes, Sort.INDEXORDER);
    assertEquals("TEST(" + Version.LATEST.toString() + ")" +
        ":C10000" +
        ":[indexSort=<doc>]" +
        ":[diagnostics=" + diagnostics + "]" +
        ":[attributes=" + attributes + "]", si.toString());

    dir.close();
  }

  public void testIDChangesOnAdvance() throws IOException {
    try (BaseDirectoryWrapper dir = newDirectory()) {
      dir.setCheckIndexOnClose(false);
      byte id[] = StringHelper.randomId();
      SegmentInfo info = new SegmentInfo(dir, Version.LUCENE_9_0_0, Version.LUCENE_9_0_0, "_0", 1, false, Codec.getDefault(),
          Collections.<String, String>emptyMap(), StringHelper.randomId(), Collections.<String, String>emptyMap(), null);
      SegmentCommitInfo commitInfo = new SegmentCommitInfo(info, 0, 0, -1, -1, -1, id);
      assertEquals(StringHelper.idToString(id), StringHelper.idToString(commitInfo.getId()));
      commitInfo.advanceDelGen();
      assertNotEquals(StringHelper.idToString(id), StringHelper.idToString(commitInfo.getId()));

      id = commitInfo.getId();
      commitInfo.advanceDocValuesGen();
      assertNotEquals(StringHelper.idToString(id), StringHelper.idToString(commitInfo.getId()));

      id = commitInfo.getId();
      commitInfo.advanceFieldInfosGen();
      assertNotEquals(StringHelper.idToString(id), StringHelper.idToString(commitInfo.getId()));
      SegmentCommitInfo clone = commitInfo.clone();
      id = commitInfo.getId();
      assertEquals(StringHelper.idToString(id), StringHelper.idToString(commitInfo.getId()));
      assertEquals(StringHelper.idToString(id), StringHelper.idToString(clone.getId()));

      commitInfo.advanceFieldInfosGen();
      assertNotEquals(StringHelper.idToString(id), StringHelper.idToString(commitInfo.getId()));
      assertEquals("clone changed but shouldn't", StringHelper.idToString(id), StringHelper.idToString(clone.getId()));
    }
  }

  public void testBitFlippedTriggersCorruptIndexException() throws IOException {
    BaseDirectoryWrapper dir = newDirectory();
    dir.setCheckIndexOnClose(false);
    byte id[] = StringHelper.randomId();
    Codec codec = Codec.getDefault();

    SegmentInfos sis = new SegmentInfos(Version.LATEST.major);
    SegmentInfo info = new SegmentInfo(dir, Version.LATEST, Version.LATEST, "_0", 1, false, Codec.getDefault(),
                                       Collections.<String,String>emptyMap(), id, Collections.<String,String>emptyMap(), null);
    info.setFiles(Collections.<String>emptySet());
    codec.segmentInfoFormat().write(dir, info, IOContext.DEFAULT);
    SegmentCommitInfo commitInfo = new SegmentCommitInfo(info, 0, 0, -1, -1, -1, StringHelper.randomId());
    sis.add(commitInfo);

    info = new SegmentInfo(dir, Version.LATEST, Version.LATEST, "_1", 1, false, Codec.getDefault(),
                           Collections.<String,String>emptyMap(), id, Collections.<String,String>emptyMap(), null);
    info.setFiles(Collections.<String>emptySet());
    codec.segmentInfoFormat().write(dir, info, IOContext.DEFAULT);
    commitInfo = new SegmentCommitInfo(info, 0, 0,-1, -1, -1, StringHelper.randomId());
    sis.add(commitInfo);

    sis.commit(dir);

    BaseDirectoryWrapper corruptDir = newDirectory();
    corruptDir.setCheckIndexOnClose(false);
    boolean corrupt = false;
    for (String file : dir.listAll()) {
      if (file.startsWith(IndexFileNames.SEGMENTS)) {
        try (IndexInput in = dir.openInput(file, IOContext.DEFAULT);
            IndexOutput out = corruptDir.createOutput(file, IOContext.DEFAULT)) {
          final long corruptIndex = TestUtil.nextLong(random(), 0, in.length() - 1);
          out.copyBytes(in, corruptIndex);
          final int b = Byte.toUnsignedInt(in.readByte()) + TestUtil.nextInt(random(), 0x01, 0xff);
          out.writeByte((byte) b);
          out.copyBytes(in, in.length() - in.getFilePointer());
        }
        try (IndexInput in = corruptDir.openInput(file, IOContext.DEFAULT)) {
          CodecUtil.checksumEntireFile(in);
          if (VERBOSE) {
            System.out.println("TEST: Altering the file did not update the checksum, aborting...");
          }
          return;
        } catch (CorruptIndexException e) {
          // ok
        }
        corrupt = true;
      } else if (ExtrasFS.isExtra(file) == false) {
        corruptDir.copyFrom(dir, file, file, IOContext.DEFAULT);
      }
    }
    assertTrue("No segments file found", corrupt);

    expectThrowsAnyOf(
        Arrays.asList(CorruptIndexException.class, IndexFormatTooOldException.class, IndexFormatTooNewException.class),
        () -> SegmentInfos.readLatestCommit(corruptDir));
    dir.close();
    corruptDir.close();
  }
}