/*
 * Copyright (C) 2013 Square, Inc.
 *
 * 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.squareup.okhttp.internal.framed;

import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.ByteString;
import okio.GzipSink;
import okio.Okio;
import org.junit.Test;

import static com.squareup.okhttp.TestUtil.headerEntries;
import static com.squareup.okhttp.internal.framed.Http2.FLAG_COMPRESSED;
import static com.squareup.okhttp.internal.framed.Http2.FLAG_END_HEADERS;
import static com.squareup.okhttp.internal.framed.Http2.FLAG_END_STREAM;
import static com.squareup.okhttp.internal.framed.Http2.FLAG_NONE;
import static com.squareup.okhttp.internal.framed.Http2.FLAG_PADDED;
import static com.squareup.okhttp.internal.framed.Http2.FLAG_PRIORITY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class Http2Test {
  final Buffer frame = new Buffer();
  final FrameReader fr = new Http2.Reader(frame, 4096, false);
  final int expectedStreamId = 15;

  @Test public void unknownFrameTypeSkipped() throws IOException {
    writeMedium(frame, 4); // has a 4-byte field
    frame.writeByte(99); // type 99
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(expectedStreamId);
    frame.writeInt(111111111); // custom data

    fr.nextFrame(new BaseTestHandler()); // Should not callback.
  }

  @Test public void onlyOneLiteralHeadersFrame() throws IOException {
    final List<Header> sentHeaders = headerEntries("name", "value");

    Buffer headerBytes = literalHeaders(sentHeaders);
    writeMedium(frame, (int) headerBytes.size());
    frame.writeByte(Http2.TYPE_HEADERS);
    frame.writeByte(FLAG_END_HEADERS | FLAG_END_STREAM);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeAll(headerBytes);

    assertEquals(frame, sendHeaderFrames(true, sentHeaders)); // Check writer sends the same bytes.

    fr.nextFrame(new BaseTestHandler() {
      @Override
      public void headers(boolean outFinished, boolean inFinished, int streamId,
          int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
        assertFalse(outFinished);
        assertTrue(inFinished);
        assertEquals(expectedStreamId, streamId);
        assertEquals(-1, associatedStreamId);
        assertEquals(sentHeaders, headerBlock);
        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
      }
    });
  }

  @Test public void headersWithPriority() throws IOException {
    final List<Header> sentHeaders = headerEntries("name", "value");

    Buffer headerBytes = literalHeaders(sentHeaders);
    writeMedium(frame, (int) (headerBytes.size() + 5));
    frame.writeByte(Http2.TYPE_HEADERS);
    frame.writeByte(FLAG_END_HEADERS | FLAG_PRIORITY);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeInt(0); // Independent stream.
    frame.writeByte(255); // Heaviest weight, zero-indexed.
    frame.writeAll(headerBytes);

    fr.nextFrame(new BaseTestHandler() {
      @Override public void priority(int streamId, int streamDependency, int weight,
          boolean exclusive) {
        assertEquals(0, streamDependency);
        assertEquals(256, weight);
        assertFalse(exclusive);
      }

      @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
          int associatedStreamId, List<Header> nameValueBlock,
          HeadersMode headersMode) {
        assertFalse(outFinished);
        assertFalse(inFinished);
        assertEquals(expectedStreamId, streamId);
        assertEquals(-1, associatedStreamId);
        assertEquals(sentHeaders, nameValueBlock);
        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
      }
    });
  }

  /** Headers are compressed, then framed. */
  @Test public void headersFrameThenContinuation() throws IOException {
    final List<Header> sentHeaders = largeHeaders();

    Buffer headerBlock = literalHeaders(sentHeaders);

    // Write the first headers frame.
    writeMedium(frame, Http2.INITIAL_MAX_FRAME_SIZE);
    frame.writeByte(Http2.TYPE_HEADERS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.write(headerBlock, Http2.INITIAL_MAX_FRAME_SIZE);

    // Write the continuation frame, specifying no more frames are expected.
    writeMedium(frame, (int) headerBlock.size());
    frame.writeByte(Http2.TYPE_CONTINUATION);
    frame.writeByte(FLAG_END_HEADERS);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeAll(headerBlock);

    assertEquals(frame, sendHeaderFrames(false, sentHeaders)); // Check writer sends the same bytes.

    // Reading the above frames should result in a concatenated headerBlock.
    fr.nextFrame(new BaseTestHandler() {
      @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
          int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
        assertFalse(outFinished);
        assertFalse(inFinished);
        assertEquals(expectedStreamId, streamId);
        assertEquals(-1, associatedStreamId);
        assertEquals(sentHeaders, headerBlock);
        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
      }
    });
  }

  @Test public void pushPromise() throws IOException {
    final int expectedPromisedStreamId = 11;

    final List<Header> pushPromise = Arrays.asList(
        new Header(Header.TARGET_METHOD, "GET"),
        new Header(Header.TARGET_SCHEME, "https"),
        new Header(Header.TARGET_AUTHORITY, "squareup.com"),
        new Header(Header.TARGET_PATH, "/")
    );

    // Write the push promise frame, specifying the associated stream ID.
    Buffer headerBytes = literalHeaders(pushPromise);
    writeMedium(frame, (int) (headerBytes.size() + 4));
    frame.writeByte(Http2.TYPE_PUSH_PROMISE);
    frame.writeByte(Http2.FLAG_END_PUSH_PROMISE);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
    frame.writeAll(headerBytes);

    assertEquals(frame, sendPushPromiseFrames(expectedPromisedStreamId, pushPromise));

    fr.nextFrame(new BaseTestHandler() {
      @Override
      public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
        assertEquals(expectedStreamId, streamId);
        assertEquals(expectedPromisedStreamId, promisedStreamId);
        assertEquals(pushPromise, headerBlock);
      }
    });
  }

  /** Headers are compressed, then framed. */
  @Test public void pushPromiseThenContinuation() throws IOException {
    final int expectedPromisedStreamId = 11;
    final List<Header> pushPromise = largeHeaders();

    // Decoding the first header will cross frame boundaries.
    Buffer headerBlock = literalHeaders(pushPromise);

    // Write the first headers frame.
    writeMedium(frame, Http2.INITIAL_MAX_FRAME_SIZE);
    frame.writeByte(Http2.TYPE_PUSH_PROMISE);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
    frame.write(headerBlock, Http2.INITIAL_MAX_FRAME_SIZE - 4);

    // Write the continuation frame, specifying no more frames are expected.
    writeMedium(frame, (int) headerBlock.size());
    frame.writeByte(Http2.TYPE_CONTINUATION);
    frame.writeByte(FLAG_END_HEADERS);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeAll(headerBlock);

    assertEquals(frame, sendPushPromiseFrames(expectedPromisedStreamId, pushPromise));

    // Reading the above frames should result in a concatenated headerBlock.
    fr.nextFrame(new BaseTestHandler() {
      @Override
      public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
        assertEquals(expectedStreamId, streamId);
        assertEquals(expectedPromisedStreamId, promisedStreamId);
        assertEquals(pushPromise, headerBlock);
      }
    });
  }

  @Test public void readRstStreamFrame() throws IOException {
    writeMedium(frame, 4);
    frame.writeByte(Http2.TYPE_RST_STREAM);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode);

    fr.nextFrame(new BaseTestHandler() {
      @Override public void rstStream(int streamId, ErrorCode errorCode) {
        assertEquals(expectedStreamId, streamId);
        assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode);
      }
    });
  }

  @Test public void readSettingsFrame() throws IOException {
    final int reducedTableSizeBytes = 16;

    writeMedium(frame, 12); // 2 settings * 6 bytes (2 for the code and 4 for the value).
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(1); // SETTINGS_HEADER_TABLE_SIZE
    frame.writeInt(reducedTableSizeBytes);
    frame.writeShort(2); // SETTINGS_ENABLE_PUSH
    frame.writeInt(0);

    fr.nextFrame(new BaseTestHandler() {
      @Override public void settings(boolean clearPrevious, Settings settings) {
        assertFalse(clearPrevious); // No clearPrevious in HTTP/2.
        assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize());
        assertEquals(false, settings.getEnablePush(true));
      }
    });
  }

  @Test public void readSettingsFrameInvalidPushValue() throws IOException {
    writeMedium(frame, 6); // 2 for the code and 4 for the value
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(2);
    frame.writeInt(2);

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1", e.getMessage());
    }
  }

  @Test public void readSettingsFrameInvalidSettingId() throws IOException {
    writeMedium(frame, 6); // 2 for the code and 4 for the value
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(7); // old number for SETTINGS_INITIAL_WINDOW_SIZE
    frame.writeInt(1);

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR invalid settings id: 7", e.getMessage());
    }
  }

  @Test public void readSettingsFrameNegativeWindowSize() throws IOException {
    writeMedium(frame, 6); // 2 for the code and 4 for the value
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(4); // SETTINGS_INITIAL_WINDOW_SIZE
    frame.writeInt(Integer.MIN_VALUE);

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1", e.getMessage());
    }
  }

  @Test public void readSettingsFrameNegativeFrameLength() throws IOException {
    writeMedium(frame, 6); // 2 for the code and 4 for the value
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(5); // SETTINGS_MAX_FRAME_SIZE
    frame.writeInt(Integer.MIN_VALUE);

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: -2147483648", e.getMessage());
    }
  }

  @Test public void readSettingsFrameTooShortFrameLength() throws IOException {
    writeMedium(frame, 6); // 2 for the code and 4 for the value
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(5); // SETTINGS_MAX_FRAME_SIZE
    frame.writeInt((int) Math.pow(2, 14) - 1);

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: 16383", e.getMessage());
    }
  }

  @Test public void readSettingsFrameTooLongFrameLength() throws IOException {
    writeMedium(frame, 6); // 2 for the code and 4 for the value
    frame.writeByte(Http2.TYPE_SETTINGS);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // Settings are always on the connection stream 0.
    frame.writeShort(5); // SETTINGS_MAX_FRAME_SIZE
    frame.writeInt((int) Math.pow(2, 24));

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: 16777216", e.getMessage());
    }
  }

  @Test public void pingRoundTrip() throws IOException {
    final int expectedPayload1 = 7;
    final int expectedPayload2 = 8;

    writeMedium(frame, 8); // length
    frame.writeByte(Http2.TYPE_PING);
    frame.writeByte(Http2.FLAG_ACK);
    frame.writeInt(0); // connection-level
    frame.writeInt(expectedPayload1);
    frame.writeInt(expectedPayload2);

    // Check writer sends the same bytes.
    assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2));

    fr.nextFrame(new BaseTestHandler() {
      @Override public void ping(boolean ack, int payload1, int payload2) {
        assertTrue(ack);
        assertEquals(expectedPayload1, payload1);
        assertEquals(expectedPayload2, payload2);
      }
    });
  }

  @Test public void maxLengthDataFrame() throws IOException {
    final byte[] expectedData = new byte[Http2.INITIAL_MAX_FRAME_SIZE];
    Arrays.fill(expectedData, (byte) 2);

    writeMedium(frame, expectedData.length);
    frame.writeByte(Http2.TYPE_DATA);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.write(expectedData);

    // Check writer sends the same bytes.
    assertEquals(frame, sendDataFrame(new Buffer().write(expectedData)));

    fr.nextFrame(new BaseTestHandler() {
      @Override public void data(boolean inFinished, int streamId, BufferedSource source,
          int length) throws IOException {
        assertFalse(inFinished);
        assertEquals(expectedStreamId, streamId);
        assertEquals(Http2.INITIAL_MAX_FRAME_SIZE, length);
        ByteString data = source.readByteString(length);
        for (byte b : data.toByteArray()) {
          assertEquals(2, b);
        }
      }
    });
  }

  /** We do not send SETTINGS_COMPRESS_DATA = 1, nor want to. Let's make sure we error. */
  @Test public void compressedDataFrameWhenSettingDisabled() throws IOException {
    byte[] expectedData = new byte[Http2.INITIAL_MAX_FRAME_SIZE];
    Arrays.fill(expectedData, (byte) 2);
    Buffer zipped = gzip(expectedData);
    int zippedSize = (int) zipped.size();

    writeMedium(frame, zippedSize);
    frame.writeByte(Http2.TYPE_DATA);
    frame.writeByte(FLAG_COMPRESSED);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    zipped.readAll(frame);

    try {
      fr.nextFrame(new BaseTestHandler());
      fail();
    } catch (IOException e) {
      assertEquals("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA",
          e.getMessage());
    }
  }

  @Test public void readPaddedDataFrame() throws IOException {
    int dataLength = 1123;
    byte[] expectedData = new byte[dataLength];
    Arrays.fill(expectedData, (byte) 2);

    int paddingLength = 254;
    byte[] padding = new byte[paddingLength];
    Arrays.fill(padding, (byte) 0);

    writeMedium(frame, dataLength + paddingLength + 1);
    frame.writeByte(Http2.TYPE_DATA);
    frame.writeByte(FLAG_PADDED);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeByte(paddingLength);
    frame.write(expectedData);
    frame.write(padding);

    fr.nextFrame(assertData());
    assertTrue(frame.exhausted()); // Padding was skipped.
  }

  @Test public void readPaddedDataFrameZeroPadding() throws IOException {
    int dataLength = 1123;
    byte[] expectedData = new byte[dataLength];
    Arrays.fill(expectedData, (byte) 2);

    writeMedium(frame, dataLength + 1);
    frame.writeByte(Http2.TYPE_DATA);
    frame.writeByte(FLAG_PADDED);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeByte(0);
    frame.write(expectedData);

    fr.nextFrame(assertData());
  }

  @Test public void readPaddedHeadersFrame() throws IOException {
    int paddingLength = 254;
    byte[] padding = new byte[paddingLength];
    Arrays.fill(padding, (byte) 0);

    Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
    writeMedium(frame, (int) headerBlock.size() + paddingLength + 1);
    frame.writeByte(Http2.TYPE_HEADERS);
    frame.writeByte(FLAG_END_HEADERS | FLAG_PADDED);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeByte(paddingLength);
    frame.writeAll(headerBlock);
    frame.write(padding);

    fr.nextFrame(assertHeaderBlock());
    assertTrue(frame.exhausted()); // Padding was skipped.
  }

  @Test public void readPaddedHeadersFrameZeroPadding() throws IOException {
    Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
    writeMedium(frame, (int) headerBlock.size() + 1);
    frame.writeByte(Http2.TYPE_HEADERS);
    frame.writeByte(FLAG_END_HEADERS | FLAG_PADDED);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeByte(0);
    frame.writeAll(headerBlock);

    fr.nextFrame(assertHeaderBlock());
  }

  /** Headers are compressed, then framed. */
  @Test public void readPaddedHeadersFrameThenContinuation() throws IOException {
    int paddingLength = 254;
    byte[] padding = new byte[paddingLength];
    Arrays.fill(padding, (byte) 0);

    // Decoding the first header will cross frame boundaries.
    Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));

    // Write the first headers frame.
    writeMedium(frame, (int) (headerBlock.size() / 2) + paddingLength + 1);
    frame.writeByte(Http2.TYPE_HEADERS);
    frame.writeByte(FLAG_PADDED);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeByte(paddingLength);
    frame.write(headerBlock, headerBlock.size() / 2);
    frame.write(padding);

    // Write the continuation frame, specifying no more frames are expected.
    writeMedium(frame, (int) headerBlock.size());
    frame.writeByte(Http2.TYPE_CONTINUATION);
    frame.writeByte(FLAG_END_HEADERS);
    frame.writeInt(expectedStreamId & 0x7fffffff);
    frame.writeAll(headerBlock);

    fr.nextFrame(assertHeaderBlock());
    assertTrue(frame.exhausted());
  }

  @Test public void tooLargeDataFrame() throws IOException {
    try {
      sendDataFrame(new Buffer().write(new byte[0x1000000]));
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("FRAME_SIZE_ERROR length > 16384: 16777216", e.getMessage());
    }
  }

  @Test public void windowUpdateRoundTrip() throws IOException {
    final long expectedWindowSizeIncrement = 0x7fffffff;

    writeMedium(frame, 4); // length
    frame.writeByte(Http2.TYPE_WINDOW_UPDATE);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(expectedStreamId);
    frame.writeInt((int) expectedWindowSizeIncrement);

    // Check writer sends the same bytes.
    assertEquals(frame, windowUpdate(expectedWindowSizeIncrement));

    fr.nextFrame(new BaseTestHandler() {
      @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
        assertEquals(expectedStreamId, streamId);
        assertEquals(expectedWindowSizeIncrement, windowSizeIncrement);
      }
    });
  }

  @Test public void badWindowSizeIncrement() throws IOException {
    try {
      windowUpdate(0);
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0",
          e.getMessage());
    }
    try {
      windowUpdate(0x80000000L);
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648",
          e.getMessage());
    }
  }

  @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException {
    final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;

    writeMedium(frame, 8); // Without debug data there's only 2 32-bit fields.
    frame.writeByte(Http2.TYPE_GOAWAY);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // connection-scope
    frame.writeInt(expectedStreamId); // last good stream.
    frame.writeInt(expectedError.httpCode);

    // Check writer sends the same bytes.
    assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY));

    fr.nextFrame(new BaseTestHandler() {
      @Override public void goAway(
          int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
        assertEquals(expectedStreamId, lastGoodStreamId);
        assertEquals(expectedError, errorCode);
        assertEquals(0, debugData.size());
      }
    });
  }

  @Test public void goAwayWithDebugDataRoundTrip() throws IOException {
    final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
    final ByteString expectedData = ByteString.encodeUtf8("abcdefgh");

    // Compose the expected GOAWAY frame without debug data.
    writeMedium(frame, 8 + expectedData.size());
    frame.writeByte(Http2.TYPE_GOAWAY);
    frame.writeByte(Http2.FLAG_NONE);
    frame.writeInt(0); // connection-scope
    frame.writeInt(0); // never read any stream!
    frame.writeInt(expectedError.httpCode);
    frame.write(expectedData.toByteArray());

    // Check writer sends the same bytes.
    assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray()));

    fr.nextFrame(new BaseTestHandler() {
      @Override public void goAway(
          int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
        assertEquals(0, lastGoodStreamId);
        assertEquals(expectedError, errorCode);
        assertEquals(expectedData, debugData);
      }
    });
  }

  @Test public void frameSizeError() throws IOException {
    Http2.Writer writer = new Http2.Writer(new Buffer(), true);

    try {
      writer.frameHeader(0, 16777216, Http2.TYPE_DATA, FLAG_NONE);
      fail();
    } catch (IllegalArgumentException e) {
      // TODO: real max is based on settings between 16384 and 16777215
      assertEquals("FRAME_SIZE_ERROR length > 16384: 16777216", e.getMessage());
    }
  }

  @Test public void ackSettingsAppliesMaxFrameSize() throws IOException {
    int newMaxFrameSize = 16777215;

    Http2.Writer writer = new Http2.Writer(new Buffer(), true);

    writer.ackSettings(new Settings().set(Settings.MAX_FRAME_SIZE, 0, newMaxFrameSize));

    assertEquals(newMaxFrameSize, writer.maxDataLength());
    writer.frameHeader(0, newMaxFrameSize, Http2.TYPE_DATA, FLAG_NONE);
  }

  @Test public void streamIdHasReservedBit() throws IOException {
    Http2.Writer writer = new Http2.Writer(new Buffer(), true);

    try {
      int streamId = 3;
      streamId |= 1L << 31; // set reserved bit
      writer.frameHeader(streamId, Http2.INITIAL_MAX_FRAME_SIZE, Http2.TYPE_DATA, FLAG_NONE);
      fail();
    } catch (IllegalArgumentException e) {
      assertEquals("reserved bit set: -2147483645", e.getMessage());
    }
  }

  private Buffer literalHeaders(List<Header> sentHeaders) throws IOException {
    Buffer out = new Buffer();
    new Hpack.Writer(out).writeHeaders(sentHeaders);
    return out;
  }

  private Buffer sendHeaderFrames(boolean outFinished, List<Header> headers) throws IOException {
    Buffer out = new Buffer();
    new Http2.Writer(out, true).headers(outFinished, expectedStreamId, headers);
    return out;
  }

  private Buffer sendPushPromiseFrames(int streamId, List<Header> headers) throws IOException {
    Buffer out = new Buffer();
    new Http2.Writer(out, true).pushPromise(expectedStreamId, streamId, headers);
    return out;
  }

  private Buffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException {
    Buffer out = new Buffer();
    new Http2.Writer(out, true).ping(ack, payload1, payload2);
    return out;
  }

  private Buffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
      throws IOException {
    Buffer out = new Buffer();
    new Http2.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData);
    return out;
  }

  private Buffer sendDataFrame(Buffer data) throws IOException {
    Buffer out = new Buffer();
    new Http2.Writer(out, true).dataFrame(expectedStreamId, FLAG_NONE, data,
        (int) data.size());
    return out;
  }

  private Buffer windowUpdate(long windowSizeIncrement) throws IOException {
    Buffer out = new Buffer();
    new Http2.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement);
    return out;
  }

  private FrameReader.Handler assertHeaderBlock() {
    return new BaseTestHandler() {
      @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
          int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
        assertFalse(outFinished);
        assertFalse(inFinished);
        assertEquals(expectedStreamId, streamId);
        assertEquals(-1, associatedStreamId);
        assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock);
        assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
      }
    };
  }

  private FrameReader.Handler assertData() {
    return new BaseTestHandler() {
      @Override public void data(boolean inFinished, int streamId, BufferedSource source,
          int length) throws IOException {
        assertFalse(inFinished);
        assertEquals(expectedStreamId, streamId);
        assertEquals(1123, length);
        ByteString data = source.readByteString(length);
        for (byte b : data.toByteArray()) {
          assertEquals(2, b);
        }
      }
    };
  }

  private static Buffer gzip(byte[] data) throws IOException {
    Buffer buffer = new Buffer();
    Okio.buffer(new GzipSink(buffer)).write(data).close();
    return buffer;
  }

  /** Create a sufficiently large header set to overflow Http20Draft12.INITIAL_MAX_FRAME_SIZE bytes. */
  private static List<Header> largeHeaders() {
    String[] nameValues = new String[32];
    char[] chars = new char[512];
    for (int i = 0; i < nameValues.length;) {
      Arrays.fill(chars, (char) i);
      nameValues[i++] = nameValues[i++] = String.valueOf(chars);
    }
    return headerEntries(nameValues);
  }

  private static void writeMedium(BufferedSink sink, int i) throws IOException {
    sink.writeByte((i >>> 16) & 0xff);
    sink.writeByte((i >>>  8) & 0xff);
    sink.writeByte( i         & 0xff);
  }
}
