// Copyright 2020 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.runtime;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.mock;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.buildtool.BuildResult;
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
import com.google.devtools.build.lib.buildtool.buildevent.MainRepoMappingComputationStartingEvent;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.testutil.ManualClock;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests for output generated by {@link UiEventHandler}. */
@RunWith(TestParameterInjector.class)
public final class UiEventHandlerStdOutAndStdErrTest {

  private static final BuildCompleteEvent BUILD_COMPLETE_EVENT =
      new BuildCompleteEvent(new BuildResult(/*startTimeMillis=*/ 0));

  @TestParameter private TestedOutput testedOutput;

  private UiEventHandler uiEventHandler;
  private FlushCollectingOutputStream output;
  private EventKind eventKind;

  private enum TestedOutput {
    STDOUT,
    STDERR;
  }

  @Before
  public void createUiEventHandler() {
    UiOptions uiOptions = new UiOptions();
    uiOptions.eventFilters = ImmutableList.of();
    createUiEventHandler(uiOptions);
  }

  private void createUiEventHandler(UiOptions uiOptions) {
    output = new FlushCollectingOutputStream();

    OutErr outErr = null;
    switch (testedOutput) {
      case STDOUT:
        outErr = OutErr.create(/* out= */ output, /* err= */ mock(OutputStream.class));
        eventKind = EventKind.STDOUT;
        break;
      case STDERR:
        outErr = OutErr.create(/* out= */ mock(OutputStream.class), /* err= */ output);
        eventKind = EventKind.STDERR;
        break;
    }

    uiEventHandler =
        new UiEventHandler(outErr, uiOptions, new ManualClock(), /* workspacePathFragment= */ null);
    uiEventHandler.mainRepoMappingComputationStarted(new MainRepoMappingComputationStartingEvent());
    uiEventHandler.buildStarted(
        BuildStartingEvent.create(
            "outputFileSystemType",
            /* usesInMemoryFileSystem= */ false,
            mock(BuildRequest.class),
            /* workspace= */ null,
            "/pwd"));
  }

  @Test
  public void buildComplete_outputsNothing() {
    uiEventHandler.buildComplete(BUILD_COMPLETE_EVENT);
    output.assertFlushed();
  }

  @Test
  public void buildComplete_flushesBufferedMessage() {
    uiEventHandler.handle(output("hello"));
    uiEventHandler.buildComplete(BUILD_COMPLETE_EVENT);

    output.assertFlushed("hello");
  }

  @Test
  public void buildComplete_emptyBuffer_outputsNothing() {
    uiEventHandler.handle(output(""));
    uiEventHandler.buildComplete(BUILD_COMPLETE_EVENT);

    output.assertFlushed();
  }

  @Test
  public void handleOutputEvent_buffersWithoutNewline() {
    uiEventHandler.handle(output("hello"));
    output.assertFlushed();
  }

  @Test
  public void handleOutputEvent_concatenatesInBuffer() {
    uiEventHandler.handle(output("hello "));
    uiEventHandler.handle(output("there"));
    uiEventHandler.buildComplete(BUILD_COMPLETE_EVENT);

    output.assertFlushed("hello there");
  }

  @Test
  public void handleOutputEvent_flushesOnNewline() {
    uiEventHandler.handle(output("hello\n"));
    output.assertFlushed("hello\n");
  }

  @Test
  public void handleOutputEvent_flushesOnlyUntilNewline() {
    uiEventHandler.handle(output("hello\nworld"));
    output.assertFlushed("hello\n");
  }

  @Test
  public void handleOutputEvent_flushesUntilLastNewline() {
    uiEventHandler.handle(output("hello\nto\neveryone"));
    output.assertFlushed("hello\nto\n");
  }

  @Test
  public void handleOutputEvent_flushesMultiLineMessageAtOnce() {
    uiEventHandler.handle(output("hello\neveryone\n"));
    output.assertFlushed("hello\neveryone\n");
  }

  @Test
  public void handleOutputEvent_concatenatesBufferBeforeFlushingOnNewline() {
    uiEventHandler.handle(output("hello"));
    uiEventHandler.handle(output(" there!\nmore text"));

    output.assertFlushed("hello there!\n");
  }

  // This test only exercises progress bar code when testing stderr output, since we don't make
  // any assertions on stderr (where the progress bar is written) when testing stdout.
  @Test
  public void noChangeOnUnflushedWrite() {
    UiOptions uiOptions = new UiOptions();
    uiOptions.showProgress = true;
    uiOptions.useCursesEnum = UiOptions.UseCurses.YES;
    uiOptions.eventFilters = ImmutableList.of();
    createUiEventHandler(uiOptions);
    if (testedOutput == TestedOutput.STDERR) {
      assertThat(output.flushed).hasSize(2);
      output.flushed.clear();
    }
    // Unterminated strings are saved in memory and not pushed out at all.
    assertThat(output.flushed).isEmpty();
    assertThat(output.writtenSinceFlush).isEmpty();
  }

  @Test
  public void buildCompleteMessageDoesntOverrideError() {
    Assume.assumeTrue(testedOutput == TestedOutput.STDERR);
    UiOptions uiOptions = new UiOptions();
    uiOptions.showProgress = true;
    uiOptions.useCursesEnum = UiOptions.UseCurses.YES;
    uiOptions.eventFilters = ImmutableList.of();
    createUiEventHandler(uiOptions);

    uiEventHandler.buildComplete(BUILD_COMPLETE_EVENT);
    uiEventHandler.handle(Event.error("Show me this!"));
    uiEventHandler.afterCommand(new AfterCommandEvent());

    assertThat(output.flushed.size()).isEqualTo(5);
    assertThat(output.flushed.get(3)).contains("Show me this!");
    assertThat(output.flushed.get(4)).doesNotContain("\033[1A\033[K");
  }

  private Event output(String message) {
    return Event.of(eventKind, message);
  }

  private static class FlushCollectingOutputStream extends OutputStream {
    private final List<String> flushed = new ArrayList<>();
    private String writtenSinceFlush = "";

    @Override
    public void write(int b) throws IOException {
      write(new byte[] {(byte) b});
    }

    @Override
    public void write(byte[] bytes, int offset, int len) {
      writtenSinceFlush += new String(Arrays.copyOfRange(bytes, offset, offset + len), UTF_8);
    }

    @Override
    public void flush() {
      // Ignore inconsequential extra flushes.
      if (!writtenSinceFlush.isEmpty()) {
        flushed.add(writtenSinceFlush);
      }
      writtenSinceFlush = "";
    }

    private void assertFlushed(String... messages) {
      assertThat(writtenSinceFlush).isEmpty();
      assertThat(flushed).containsExactlyElementsIn(messages);
    }
  }
}
