// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.execution.impl;

import com.intellij.execution.process.ProcessOutputType;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.Disposer;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

public class ProcessStreamsSynchronizerTest extends LightPlatformTestCase {

  private static final long AWAIT_SAME_STREAM_TEXT_MILLIS = TimeUnit.NANOSECONDS.toMillis(ProcessStreamsSynchronizer.AWAIT_SAME_STREAM_TEXT_NANO);

  private MockProcessStreamsSynchronizer mySynchronizer;
  
  public void testBasic() {
    mySynchronizer = new MockProcessStreamsSynchronizer(Disposer.newDisposable());
    mySynchronizer.doWhenStreamsSynchronized("Hello stdout 1\n", ProcessOutputType.STDOUT, 0);
    mySynchronizer.doWhenStreamsSynchronized("Hello stdout 2\n", ProcessOutputType.STDOUT, 1);
    mySynchronizer.doWhenStreamsSynchronized("Hello stderr 1\n", ProcessOutputType.STDERR, 2);
    assertFlushedChunks(new FlushedChunk("Hello stdout 1\n", ProcessOutputType.STDOUT, 0),
                        new FlushedChunk("Hello stdout 2\n", ProcessOutputType.STDOUT, 1));
    mySynchronizer.advanceTimeTo(1 + AWAIT_SAME_STREAM_TEXT_MILLIS);
    assertFlushedChunks(new FlushedChunk("Hello stderr 1\n", ProcessOutputType.STDERR, 1 + AWAIT_SAME_STREAM_TEXT_MILLIS));
    assertNoPendingChunks();
  }

  public void testSimpleRun() {
    mySynchronizer = new MockProcessStreamsSynchronizer(Disposer.newDisposable());
    mySynchronizer.doWhenStreamsSynchronized("java HelloWorld\n", ProcessOutputType.SYSTEM, 10);

    mySynchronizer.doWhenStreamsSynchronized("info:", ProcessOutputType.STDOUT, 10);
    mySynchronizer.doWhenStreamsSynchronized("warn:", ProcessOutputType.STDERR, 14);
    mySynchronizer.doWhenStreamsSynchronized("Error\n", ProcessOutputType.STDERR, 14);
    mySynchronizer.doWhenStreamsSynchronized("Hello from stdout", ProcessOutputType.STDOUT, 15);
    mySynchronizer.doWhenStreamsSynchronized("\n", ProcessOutputType.STDOUT, 16);

    assertFlushedChunks(new FlushedChunk("java HelloWorld\n", ProcessOutputType.SYSTEM, 10),
                        new FlushedChunk("info:", ProcessOutputType.STDOUT, 10),
                        new FlushedChunk("Hello from stdout", ProcessOutputType.STDOUT, 15),
                        new FlushedChunk("\n", ProcessOutputType.STDOUT, 16));
    
    mySynchronizer.advanceTimeTo(10 + AWAIT_SAME_STREAM_TEXT_MILLIS);
    assertFlushedChunks(new FlushedChunk("warn:", ProcessOutputType.STDERR, 10 + AWAIT_SAME_STREAM_TEXT_MILLIS),
                        new FlushedChunk("Error\n", ProcessOutputType.STDERR, 10 + AWAIT_SAME_STREAM_TEXT_MILLIS));

    mySynchronizer.doWhenStreamsSynchronized("info:", ProcessOutputType.STDOUT, 21);
    mySynchronizer.doWhenStreamsSynchronized("2\n", ProcessOutputType.STDOUT, 21);

    mySynchronizer.doWhenStreamsSynchronized("Another error", ProcessOutputType.STDERR, 22);
    assertFlushedChunks(new FlushedChunk("Another error", ProcessOutputType.STDERR, 22));
    mySynchronizer.doWhenStreamsSynchronized("\n", ProcessOutputType.STDERR, 22);
    assertFlushedChunks(new FlushedChunk("\n", ProcessOutputType.STDERR, 22));
    mySynchronizer.doWhenStreamsSynchronized("Process exited", ProcessOutputType.SYSTEM, 22);

    mySynchronizer.advanceTimeTo(14 + AWAIT_SAME_STREAM_TEXT_MILLIS);
    assertFlushedChunks(new FlushedChunk("info:", ProcessOutputType.STDOUT, 14 + AWAIT_SAME_STREAM_TEXT_MILLIS),
                        new FlushedChunk("2\n", ProcessOutputType.STDOUT, 14 + AWAIT_SAME_STREAM_TEXT_MILLIS),
                        new FlushedChunk("Process exited", ProcessOutputType.SYSTEM, 14 + AWAIT_SAME_STREAM_TEXT_MILLIS));
    assertNoPendingChunks();
  }

  private void assertFlushedChunks(@NotNull FlushedChunk... expectedFlushedChunks) {
    Assert.assertEquals(ContainerUtil.newArrayList(expectedFlushedChunks), mySynchronizer.getFlushedChunksAndClear());
  }

  private void assertNoPendingChunks() {
    Assert.assertEquals(0, mySynchronizer.myFlushedChunkCount - mySynchronizer.myRequestedChunkCount);
    Assert.assertEquals(-1, mySynchronizer.myNextScheduledProcessingTimeNano);
  }

  private static class MockProcessStreamsSynchronizer extends ProcessStreamsSynchronizer {

    private long myNowTimeNano;
    private long myNextScheduledProcessingTimeNano = -1;
    private final List<FlushedChunk> myFlushedChunks = new ArrayList<>();
    private int myRequestedChunkCount = 0;
    private int myFlushedChunkCount = 0;

    MockProcessStreamsSynchronizer(@NotNull Disposable parentDisposable) {
      super(parentDisposable);
    }

    void doWhenStreamsSynchronized(@NotNull String text, @NotNull ProcessOutputType outputType, long nowTimeMillis) {
      myRequestedChunkCount++;
      advanceTimeTo(nowTimeMillis);
      super.doWhenStreamsSynchronized(text, outputType, () -> {
        myFlushedChunkCount++;
        myFlushedChunks.add(new FlushedChunk(text, outputType, TimeUnit.NANOSECONDS.toMillis(getNanoTime())));
      });
    }

    @Override
    long getNanoTime() {
      return myNowTimeNano;
    }

    @Override
    void scheduleProcessPendingChunks(long delayNano) {
      myNextScheduledProcessingTimeNano = myNowTimeNano + delayNano;
    }

    private void advanceTimeTo(long timeMillis) {
      long nowTimeNano = TimeUnit.MILLISECONDS.toNanos(timeMillis);
      assert nowTimeNano >= myNowTimeNano;
      tryAdvanceTimeToNextScheduledProcessing(nowTimeNano);
      myNowTimeNano = nowTimeNano;
    }

    private void tryAdvanceTimeToNextScheduledProcessing(long nextDataTimeNano) {
      if (myNextScheduledProcessingTimeNano != -1 && nextDataTimeNano >= myNextScheduledProcessingTimeNano) {
        myNowTimeNano = myNextScheduledProcessingTimeNano;
        myNextScheduledProcessingTimeNano = -1;
        super.processPendingChunks(myNowTimeNano);
      }
    }

    @NotNull
    List<FlushedChunk> getFlushedChunksAndClear() {
      List<FlushedChunk> chunks = new ArrayList<>(myFlushedChunks);
      myFlushedChunks.clear();
      return chunks;
    }
  }

  private static class FlushedChunk {
    private final String myText;
    private final ProcessOutputType myOutputType;
    private final long myFlushTimeMillis;

    private FlushedChunk(@NotNull String text, @NotNull ProcessOutputType outputType, long flushTimeMillis) {
      myText = text;
      myOutputType = outputType;
      myFlushTimeMillis = flushTimeMillis;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      FlushedChunk chunk = (FlushedChunk)o;
      return myFlushTimeMillis == chunk.myFlushTimeMillis &&
             myText.equals(chunk.myText) &&
             myOutputType.equals(chunk.myOutputType);
    }

    @Override
    public int hashCode() {
      return Objects.hash(myText, myOutputType, myFlushTimeMillis);
    }

    @Override
    public String toString() {
      return "(text='" + myText + '\'' + ", outputType=" + myOutputType + ", flushTimeMillis=" + myFlushTimeMillis + ")";
    }
  }
}

@@ -273,51 +273,8 @@
 le;%0A
-import com.intellij.openapi.util.Disposer;%0A
 impo
@@ -926,36 +926,35 @@
 ynchronizer(
-Disposer.new
+getTestRoot
 Disposable()
@@ -1743,20 +1743,19 @@
 zer(
-Disposer.new
+getTestRoot
 Disp

fix memory leak in tests (IDEA-CR-53382)