/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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 com.intellij.vcs.log.data;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.progress.impl.ProgressManagerImpl;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcs.log.Hash;
import com.intellij.vcs.log.TimedVcsCommit;
import com.intellij.vcs.log.VcsLogProvider;
import com.intellij.vcs.log.graph.GraphCommit;
import com.intellij.vcs.log.impl.*;
import com.intellij.vcs.test.VcsPlatformTest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.concurrent.*;

import static com.intellij.vcs.log.TimedCommitParser.log;

public class VcsLogRefresherTest extends VcsPlatformTest {

  private static final Logger LOG = Logger.getInstance(VcsLogRefresherTest.class);

  private static final int RECENT_COMMITS_COUNT = 2;
  private static final Consumer<Exception> FAILING_EXCEPTION_HANDLER = e -> {
    throw new AssertionError(e);
  };
  private TestVcsLogProvider myLogProvider;
  private VcsLogData myLogData;
  private Map<VirtualFile, VcsLogProvider> myLogProviders;

  private DataWaiter myDataWaiter;
  private VcsLogRefresher myLoader;
  private final List<Future<?>> myStartedTasks = Collections.synchronizedList(new ArrayList<>());

  private List<String> myCommits;

  @Override
  public void setUp() throws Exception {
    super.setUp();

    myLogProvider = new TestVcsLogProvider(myProjectRoot);
    myLogProviders = Collections.singletonMap(myProjectRoot, myLogProvider);

    myCommits = Arrays.asList("3|-a2|-a1", "2|-a1|-a", "1|-a|-");
    myLogProvider.appendHistory(log(myCommits));
    myLogProvider.addRef(createBranchRef("master", "a2"));

    myDataWaiter = new DataWaiter();
    myLoader = createLoader(myDataWaiter);
  }

  @Override
  public void tearDown() throws Exception {
    try {
      assertNoMoreResultsArrive();
      myDataWaiter.tearDown();
      if (myDataWaiter.failed()) {
        fail("Only one refresh should have happened, an error happened instead: " + myDataWaiter.getExceptionText());
      }
    }
    finally {
      super.tearDown();
    }
  }

  @NotNull
  @Override
  protected Collection<String> getDebugLogCategories() {
    return Arrays.asList("#" + SingleTaskController.class.getName(), "#" + VcsLogRefresherImpl.class.getName(),
                         "#" + VcsLogRefresherTest.class.getName(), "#" + TestVcsLogProvider.class.getName());
  }

  public void test_initialize_shows_short_history() throws InterruptedException, ExecutionException, TimeoutException {
    myLogProvider.blockFullLog();
    DataPack result = myLoader.readFirstBlock();
    myLogProvider.unblockFullLog();
    assertNotNull(result);
    assertDataPack(log(myCommits.subList(0, 2)), result.getPermanentGraph().getAllCommits());
    waitForBackgroundTasksToComplete();
    myDataWaiter.get();
  }

  public void test_first_refresh_reports_full_history() throws InterruptedException, ExecutionException, TimeoutException {
    myLoader.readFirstBlock();

    DataPack result = myDataWaiter.get();
    assertDataPack(log(myCommits), result.getPermanentGraph().getAllCommits());
  }

  public void test_first_refresh_waits_for_full_log() throws InterruptedException, ExecutionException, TimeoutException {
    myLogProvider.blockFullLog();
    myLoader.readFirstBlock();
    assertTimeout("Refresh waiter should have failed on the timeout");
    myLogProvider.unblockFullLog();

    DataPack result = myDataWaiter.get();
    assertDataPack(log(myCommits), result.getPermanentGraph().getAllCommits());
  }

  public void test_refresh_captures_new_commits() throws InterruptedException, ExecutionException, TimeoutException {
    initAndWaitForFirstRefresh();

    String newCommit = "4|-a3|-a2";
    myLogProvider.appendHistory(log(newCommit));
    myLoader.refresh(Collections.singletonList(myProjectRoot));
    DataPack result = myDataWaiter.get();

    List<String> allCommits = ContainerUtil.newArrayList();
    allCommits.add(newCommit);
    allCommits.addAll(myCommits);
    assertDataPack(log(allCommits), result.getPermanentGraph().getAllCommits());
  }

  public void test_single_refresh_causes_single_data_read() throws InterruptedException, ExecutionException, TimeoutException {
    initAndWaitForFirstRefresh();

    myLogProvider.resetReadFirstBlockCounter();
    myLoader.refresh(Collections.singletonList(myProjectRoot));
    myDataWaiter.get();
    assertEquals("Unexpected first block read count", 1, myLogProvider.getReadFirstBlockCounter());
  }

  public void test_reinitialize_makes_refresh_cancelled() throws InterruptedException, ExecutionException, TimeoutException {
    initAndWaitForFirstRefresh();

    // initiate the refresh and make it hang
    myLogProvider.blockRefresh();
    myLoader.refresh(Collections.singletonList(myProjectRoot));

    // initiate reinitialize; the full log will await because the Task is busy waiting for the refresh
    myLoader.readFirstBlock();

    // the task queue now contains (1) blocked ongoing refresh request; (2) queued complete refresh request
    // we want to make sure only one data pack is reported
    myLogProvider.unblockRefresh();
    myDataWaiter.get();
    assertNoMoreResultsArrive();
  }

  private void assertNoMoreResultsArrive() throws InterruptedException, ExecutionException, TimeoutException {
    waitForBackgroundTasksToComplete();
    assertTrue(myDataWaiter.myQueue.isEmpty());
  }

  private void waitForBackgroundTasksToComplete() throws InterruptedException, ExecutionException, TimeoutException {
    for (Future<?> task : new ArrayList<>(myStartedTasks)) {
      task.get(1, TimeUnit.SECONDS);
    }
  }

  public void test_two_immediately_consecutive_refreshes_causes_only_one_data_pack_update()
    throws InterruptedException, ExecutionException, TimeoutException {
    initAndWaitForFirstRefresh();
    myLogProvider.blockRefresh();
    myLoader.refresh(Collections.singletonList(myProjectRoot)); // this refresh hangs in VcsLogProvider.readFirstBlock()
    myLoader.refresh(Collections.singletonList(myProjectRoot)); // this refresh is queued
    myLogProvider.unblockRefresh(); // this will make the first one complete, and then perform the second as well

    myDataWaiter.get();
    assertTimeout("Second refresh shouldn't cause the data pack update"); // it may also fail in beforehand in set().
  }

  private void initAndWaitForFirstRefresh() throws InterruptedException, ExecutionException, TimeoutException {
    // wait for the first block and the whole log to complete
    myLoader.readFirstBlock();
    DataPack fullDataPack = myDataWaiter.get();
    assertTrue(fullDataPack.isFull());
    assertNoMoreResultsArrive();
  }

  private void assertTimeout(@NotNull String message) throws InterruptedException, ExecutionException {
    assertNull(message, myDataWaiter.myQueue.poll(500, TimeUnit.MILLISECONDS));
  }

  private VcsLogRefresherImpl createLoader(Consumer<DataPack> dataPackConsumer) {
    myLogData = new VcsLogData(myProject, myLogProviders, new FatalErrorHandler() {
      @Override
      public void consume(@Nullable Object source, @NotNull Exception exception) {
        LOG.error(exception);
      }

      @Override
      public void displayFatalErrorMessage(@NotNull String message) {
        LOG.error(message);
      }
    });
    Disposer.register(myProject, myLogData);
    return new VcsLogRefresherImpl(myProject, myLogData.getStorage(), myLogProviders, myLogData.getUserRegistry(), myLogData.getIndex(),
                                   new VcsLogProgress(),
                                   myLogData.getTopCommitsCache(), dataPackConsumer, FAILING_EXCEPTION_HANDLER, RECENT_COMMITS_COUNT
    ) {
      @Override
      protected void startNewBackgroundTask(@NotNull final Task.Backgroundable refreshTask) {
        LOG.debug("Starting a background task...");
        myStartedTasks.add(((ProgressManagerImpl)ProgressManager.getInstance()).runProcessWithProgressAsynchronously(refreshTask));
        LOG.debug(myStartedTasks.size() + " started tasks");
      }
    };
  }

  private void assertDataPack(@NotNull List<TimedVcsCommit> expectedLog, @NotNull List<GraphCommit<Integer>> actualLog) {
    List<TimedVcsCommit> convertedActualLog = convert(actualLog);
    assertOrderedEquals(convertedActualLog, expectedLog);
  }

  @NotNull
  private List<TimedVcsCommit> convert(@NotNull List<GraphCommit<Integer>> actualLog) {
    return ContainerUtil.map(actualLog, commit -> {
      Function<Integer, Hash> convertor = integer -> myLogData.getCommitId(integer).getHash();
      return new TimedVcsCommitImpl(convertor.fun(commit.getId()), ContainerUtil.map(commit.getParents(), convertor),
                                    commit.getTimestamp());
    });
  }

  @NotNull
  private VcsRefImpl createBranchRef(@NotNull String name, @NotNull String commit) {
    return new VcsRefImpl(HashImpl.build(commit), name, TestVcsLogProvider.BRANCH_TYPE, myProjectRoot);
  }

  private static class DataWaiter implements Consumer<DataPack> {
    private volatile BlockingQueue<DataPack> myQueue = new ArrayBlockingQueue<>(10);
    private volatile Exception myException;

    @Override
    public void consume(DataPack t) {
      try {
        myQueue.add(t);
      }
      catch (Exception e) {
        myException = e;
        throw new AssertionError(e);
      }
    }

    @NotNull
    public DataPack get(long timeout, @NotNull TimeUnit timeUnit) throws InterruptedException {
      return ObjectUtils.assertNotNull(myQueue.poll(timeout, timeUnit));
    }

    public boolean failed() {
      return myException != null;
    }

    String getExceptionText() {
      return ExceptionUtil.getThrowableText(myException);
    }

    @NotNull
    public DataPack get() throws InterruptedException {
      return get(1, TimeUnit.SECONDS);
    }

    public void tearDown() {
      myQueue = null;
    }
  }
}
