/*
 * 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 java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.search.Sort;
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.tests.mockfile.ExtrasFS;
import org.apache.lucene.tests.store.BaseDirectoryWrapper;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.StringHelper;
import org.apache.lucene.util.Version;

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,
            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,
            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,
            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,
            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,
            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,
            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,
            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,
              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,
            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,
            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 (
            @SuppressWarnings("unused")
            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();
  }

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

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

    // adds an additional key/value pair
    si =
        new SegmentInfo(
            dir,
            Version.LATEST,
            Version.LATEST,
            "TEST",
            10000,
            false,
            false,
            codec,
            diagnostics,
            StringHelper.randomId(),
            new HashMap<>(),
            Sort.INDEXORDER);
    si.addDiagnostics(Map.of("key3", "value3"));
    assertEquals(Map.of("key1", "value1", "key2", "value2", "key3", "value3"), si.getDiagnostics());

    // modifies an existing key/value pair
    si =
        new SegmentInfo(
            dir,
            Version.LATEST,
            Version.LATEST,
            "TEST",
            10000,
            false,
            false,
            codec,
            diagnostics,
            StringHelper.randomId(),
            new HashMap<>(),
            Sort.INDEXORDER);
    si.addDiagnostics(Map.of("key2", "foo"));
    assertEquals(Map.of("key1", "value1", "key2", "foo"), si.getDiagnostics());

    dir.close();
  }
}
