/*
 * Copyright 2015 the original author or authors.
 *
 * Licensed 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 io.atomix.copycat.server.storage;

import io.atomix.catalyst.buffer.Buffer;
import io.atomix.catalyst.buffer.DirectBuffer;
import io.atomix.catalyst.serializer.Serializer;
import io.atomix.copycat.server.storage.compaction.Compaction;
import org.testng.annotations.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.testng.Assert.*;

/**
 * Abstract log test.
 * 
 * @author Jonathan Halterman
 */
@Test
public abstract class AbstractLogTest {
  protected int entryPadding;
  protected int entriesPerSegment = 3;
  protected Log log;
  String logId;

  protected abstract Log createLog();

  /**
   * Returns a set of tests to run for varied log configurations.
   */
  protected Object[] testsFor(Class<? extends LogTest> testClass) throws Throwable {
    List<Object> tests = new ArrayList<>();
    for (int i = 1; i < 30; i++) {
      LogTest test = testClass.newInstance();
      test.entriesPerSegment = i;
      test.entryPadding = i / 3;
      tests.add(test);
    }

    return tests.toArray(new Object[tests.size()]);
  }

  protected int entrySize() {
    Serializer serializer = new Serializer();
    serializer.register(TestEntry.class);
    Buffer buffer = DirectBuffer.allocate(1000);
    TestEntry entry = new TestEntry();
    entry.setPadding(entryPadding);
    serializer.writeObject(entry, buffer);
    return (int) buffer.position() + Short.BYTES + Long.BYTES;
  }

  @BeforeMethod
  void setLog() throws Exception {
    logId = UUID.randomUUID().toString();
    log = createLog();
    assertTrue(log.isOpen());
    assertFalse(log.isClosed());
    assertTrue(log.isEmpty());
  }

  @AfterMethod
  protected void deleteLog() {
    try {
      if (log.isOpen())
        log.close();
    } catch (Exception ignore) {
    } finally {
      assertFalse(log.isOpen());
      assertTrue(log.isClosed());
      log.delete();
    }
  }

  /**
   * Returns the size of a full segment given the entrySize, entriesPerSegment, and SegmentDescriptor.
   */
  int fullSegmentSize() {
    return entriesPerSegment * entrySize() + SegmentDescriptor.BYTES;
  }

  protected Storage.Builder tempStorageBuilder() {
    return Storage.builder().withDirectory(new File(String.format("target/test-logs/%s", logId)));
  }

  @BeforeTest
  @AfterTest
  protected void cleanLogDir() throws IOException {
    Path directory = Paths.get("target/test-logs/");
    if (Files.exists(directory)) {
      Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
          Files.delete(file);
          return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
          Files.delete(dir);
          return FileVisitResult.CONTINUE;
        }
      });
    }
  }

  /**
   * Appends {@code numEntries} increasingly numbered ByteBuffer wrapped entries to the log.
   */
  protected List<Long> appendEntries(int numEntries) {
    return appendEntries(numEntries, (int) log.length() + 1, Compaction.Mode.QUORUM);
  }

  /**
   * Appends {@code numEntries} increasingly numbered ByteBuffer wrapped entries to the log.
   */
  protected List<Long> appendEntries(int numEntries, Compaction.Mode mode) {
    return appendEntries(numEntries, (int) log.length() + 1, mode);
  }

  /**
   * Appends {@code numEntries} increasingly numbered ByteBuffer wrapped entries to the log, starting at the
   * {@code startingId}.
   */
  protected List<Long> appendEntries(int numEntries, int startingId, Compaction.Mode mode) {
    List<Integer> entryIds = IntStream.range(startingId, startingId + numEntries).boxed().collect(Collectors.toList());
    return entryIds.stream().map(entryId -> {
      try (TestEntry entry = log.create(TestEntry.class)) {
        entry.setTerm(1).setCompactionMode(mode).setPadding(entryPadding);
        return log.append(entry);
      }
    }).collect(Collectors.toList());
  }

  protected static void assertIndexes(List<Long> indexes, int start, int end) {
    for (int i = 0, j = start; j <= end; i++, j++)
      assertEquals(indexes.get(i).longValue(), j);
  }

  protected void cleanAndCompact(int startIndex, int endIndex) {
    for (int i = startIndex; i <= endIndex; i++) {
      log.clean(i);
    }

    log.compactor().compact(Compaction.MAJOR).join();
  }

  protected void assertCompacted(int startIndex, int endIndex) {
    for (int i = startIndex; i <= endIndex; i++) {
      assertNull(log.get(i));
    }
  }
  
  protected void printLog() {
    for (int i = 1; i < log.length(); i++) {
      System.out.println(log.get(i).toString());
    }
  }
}

@@ -1675,17 +1675,17 @@
  1; i %3C 
-3
+1
 0; i++) 

Decrease log test iterations for Travis.