// Copyright 2014 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.util.io;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

import com.google.common.io.ByteStreams;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;

/**
 * Test for {@link StreamMultiplexer}.
 */
@RunWith(JUnit4.class)
public class StreamMultiplexerTest {

  private ByteArrayOutputStream multiplexed;
  private OutputStream out;
  private OutputStream err;
  private OutputStream ctl;

  @Before
  public final void createOutputStreams() throws Exception  {
    multiplexed = new ByteArrayOutputStream();
    StreamMultiplexer multiplexer = new StreamMultiplexer(multiplexed);
    out = multiplexer.createStdout();
    err = multiplexer.createStderr();
    ctl = multiplexer.createControl();
  }

  @Test
  public void testEmptyWire() throws IOException {
    out.flush();
    err.flush();
    ctl.flush();
    assertEquals(0, multiplexed.toByteArray().length);
  }

  private static byte[] getLatin(String string)
      throws UnsupportedEncodingException {
    return string.getBytes("ISO-8859-1");
  }

  @Test
  public void testHelloWorldOnStdOut() throws Exception {
    out.write(getLatin("Hello, world."));
    out.flush();
    assertMessage(multiplexed.toByteArray(), 0, "Hello, world.");
  }

  @Test
  public void testInterleavedStdoutStderrControl() throws Exception {
    int start = 0;
    out.write(getLatin("Hello, stdout."));
    out.flush();
    assertMessage(multiplexed.toByteArray(), start, "Hello, stdout.");
    start = multiplexed.toByteArray().length;

    err.write(getLatin("Hello, stderr."));
    err.flush();
    assertMessage(multiplexed.toByteArray(), start, "Hello, stderr.");
    start = multiplexed.toByteArray().length;

    ctl.write(getLatin("Hello, control."));
    ctl.flush();
    assertMessage(multiplexed.toByteArray(), start, "Hello, control.");
    start = multiplexed.toByteArray().length;

    out.write(getLatin("... and back!"));
    out.flush();
    assertMessage(multiplexed.toByteArray(), start, "... and back!");
  }

  @Test
  public void testWillNotCommitToUnderlyingStreamUnlessFlushOrNewline()
      throws Exception {
    out.write(getLatin("There are no newline characters in here, so it won't" +
        " get written just yet."));
    assertArrayEquals(multiplexed.toByteArray(), new byte[0]);
  }

  @Test
  public void testNewlineTriggersFlush() throws Exception {
    out.write(getLatin("No newline just yet, so no flushing. "));
    assertArrayEquals(multiplexed.toByteArray(), new byte[0]);
    out.write(getLatin("OK, here we go:\nAnd more to come."));
    assertMessage(
        multiplexed.toByteArray(), 0, "No newline just yet, so no flushing. OK, here we go:\n");
    int firstMessageLength = multiplexed.toByteArray().length;
    out.write((byte) '\n');
    assertMessage(multiplexed.toByteArray(), firstMessageLength, "And more to come.\n");
  }

  @Test
  public void testFlush() throws Exception {
    out.write(getLatin("Don't forget to flush!"));
    assertArrayEquals(new byte[0], multiplexed.toByteArray());
    out.flush(); // now the output will appear in multiplexed.
    assertStartsWith(multiplexed.toByteArray(), 1, 0, 0, 0);
    assertMessage(multiplexed.toByteArray(), 0, "Don't forget to flush!");
  }

  @Test
  public void testByteEncoding() throws IOException {
    OutputStream devNull = ByteStreams.nullOutputStream();
    StreamDemultiplexer demux = new StreamDemultiplexer((byte) 1, devNull);
    StreamMultiplexer mux = new StreamMultiplexer(demux);
    OutputStream out = mux.createStdout();

    // When we cast 266 to a byte, we get 10. So basically, we ended up
    // comparing 266 with 10 as an integer (because out.write takes an int),
    // and then later cast it to 10. This way we'd end up with a control
    // character \n in the middle of the payload which would then screw things
    // up when the real control character arrived. The fixed version of the
    // StreamMultiplexer avoids this problem by always casting to a byte before
    // carrying out any comparisons.

    out.write(266);
    out.write(10);
  }

  private static void assertStartsWith(byte[] actual, int... expectedPrefix){
    for (int i = 0; i < expectedPrefix.length; i++) {
      assertThat(actual[i]).isEqualTo(expectedPrefix[i]);
    }
  }

  private static void assertMessage(byte[] actual, int start, String expected) throws Exception {
    assertThat(Arrays.copyOfRange(actual, start + 5, actual.length)).isEqualTo(getLatin(expected));
  }
}
