/*
 * Copyright 2014 The gRPC Authors
 *
 * 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 io.grpc;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.testing.EqualsTester;
import io.grpc.internal.GrpcUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for {@link Metadata}.
 */
@RunWith(JUnit4.class)
public class MetadataTest {

  private static final Metadata.BinaryMarshaller<Fish> FISH_MARSHALLER =
      new Metadata.BinaryMarshaller<Fish>() {
        @Override
        public byte[] toBytes(Fish fish) {
          return fish.name.getBytes(UTF_8);
        }

        @Override
        public Fish parseBytes(byte[] serialized) {
          return new Fish(new String(serialized, UTF_8));
        }
      };

  private static class FishStreamMarshaller implements Metadata.BinaryStreamMarshaller<Fish> {
    @Override
    public InputStream toStream(Fish fish) {
      return new ByteArrayInputStream(FISH_MARSHALLER.toBytes(fish));
    }

    @Override
    public Fish parseStream(InputStream stream) {
      try {
        return FISH_MARSHALLER.parseBytes(ByteStreams.toByteArray(stream));
      } catch (IOException ioe) {
        throw new AssertionError();
      }
    }
  }

  private static final Metadata.BinaryStreamMarshaller<Fish> FISH_STREAM_MARSHALLER =
      new FishStreamMarshaller();

  /** A pattern commonly used to avoid unnecessary serialization of immutable objects. */
  private static final class FakeFishStream extends InputStream {
    final Fish fish;

    FakeFishStream(Fish fish) {
      this.fish = fish;
    }

    @Override
    public int read() throws IOException {
      throw new IOException("Not actually a stream");
    }
  }

  private static final Metadata.BinaryStreamMarshaller<Fish> IMMUTABLE_FISH_MARSHALLER =
      new Metadata.BinaryStreamMarshaller<Fish>() {
        @Override
        public InputStream toStream(Fish fish) {
          return new FakeFishStream(fish);
        }

        @Override
        public Fish parseStream(InputStream stream) {
          return ((FakeFishStream) stream).fish;
        }
      };

  private static final String LANCE = "lance";
  private static final byte[] LANCE_BYTES = LANCE.getBytes(US_ASCII);
  private static final Metadata.Key<Fish> KEY = Metadata.Key.of("test-bin", FISH_MARSHALLER);
  private static final Metadata.Key<Fish> KEY_STREAMED =
      Metadata.Key.of("streamed-bin", FISH_STREAM_MARSHALLER);
  private static final Metadata.Key<Fish> KEY_IMMUTABLE =
      Metadata.Key.of("immutable-bin", IMMUTABLE_FISH_MARSHALLER);

  @Test
  public void noPseudoHeaders() {
    IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
        () -> Metadata.Key.of(":test-bin", FISH_MARSHALLER));
    assertThat(e).hasMessageThat().isEqualTo("Invalid character ':' in key name ':test-bin'");
  }

  @Test
  public void testMutations() {
    Fish lance = new Fish(LANCE);
    Fish cat = new Fish("cat");
    Metadata metadata = new Metadata();

    assertNull(metadata.get(KEY));
    metadata.put(KEY, lance);
    assertEquals(Arrays.asList(lance), Lists.newArrayList(metadata.getAll(KEY)));
    assertEquals(lance, metadata.get(KEY));
    metadata.put(KEY, lance);
    assertEquals(Arrays.asList(lance, lance), Lists.newArrayList(metadata.getAll(KEY)));
    assertTrue(metadata.remove(KEY, lance));
    assertEquals(Arrays.asList(lance), Lists.newArrayList(metadata.getAll(KEY)));

    assertFalse(metadata.remove(KEY, cat));
    metadata.put(KEY, cat);
    assertEquals(cat, metadata.get(KEY));
    metadata.put(KEY, lance);
    assertEquals(Arrays.asList(lance, cat, lance), Lists.newArrayList(metadata.getAll(KEY)));
    assertEquals(lance, metadata.get(KEY));
    assertTrue(metadata.remove(KEY, lance));
    assertEquals(Arrays.asList(cat, lance), Lists.newArrayList(metadata.getAll(KEY)));
    metadata.put(KEY, lance);
    assertTrue(metadata.remove(KEY, cat));
    assertEquals(Arrays.asList(lance, lance), Lists.newArrayList(metadata.getAll(KEY)));

    assertEquals(Arrays.asList(lance, lance), Lists.newArrayList(metadata.removeAll(KEY)));
    assertNull(metadata.getAll(KEY));
    assertNull(metadata.get(KEY));
  }

  @Test
  public void discardAll() {
    Fish lance = new Fish(LANCE);
    Metadata metadata = new Metadata();

    metadata.put(KEY, lance);
    metadata.discardAll(KEY);
    assertNull(metadata.getAll(KEY));
    assertNull(metadata.get(KEY));
  }

  @Test
  public void discardAll_empty() {
    Metadata metadata = new Metadata();
    metadata.discardAll(KEY);
    assertNull(metadata.getAll(KEY));
    assertNull(metadata.get(KEY));
  }

  @Test
  public void testGetAllNoRemove() {
    Fish lance = new Fish(LANCE);
    Metadata metadata = new Metadata();
    metadata.put(KEY, lance);
    Iterator<Fish> i = metadata.getAll(KEY).iterator();
    assertEquals(lance, i.next());

    assertThrows(UnsupportedOperationException.class, i::remove);
  }

  @Test
  public void testWriteParsed() {
    Fish lance = new Fish(LANCE);
    Metadata metadata = new Metadata();
    metadata.put(KEY, lance);
    assertEquals(lance, metadata.get(KEY));
    Iterator<Fish> fishes = metadata.getAll(KEY).iterator();
    assertTrue(fishes.hasNext());
    assertEquals(fishes.next(), lance);
    assertFalse(fishes.hasNext());
    byte[][] serialized = metadata.serialize();
    assertEquals(2, serialized.length);
    assertEquals("test-bin", new String(serialized[0], US_ASCII));
    assertArrayEquals(LANCE_BYTES, serialized[1]);
    assertEquals(lance, metadata.get(KEY));
    assertEquals(serialized[0], metadata.serialize()[0]);
    assertEquals(serialized[1], metadata.serialize()[1]);
  }

  @Test
  public void testWriteRaw() {
    Metadata raw = new Metadata(KEY.asciiName(), LANCE_BYTES);
    Fish lance = raw.get(KEY);
    assertEquals(lance, new Fish(LANCE));
    // Reading again should return the same parsed instance
    assertEquals(lance, raw.get(KEY));
  }

  @Test
  public void testSerializeRaw() {
    Metadata raw = new Metadata(KEY.asciiName(), LANCE_BYTES);
    byte[][] serialized = raw.serialize();
    assertArrayEquals(serialized[0], KEY.asciiName());
    assertArrayEquals(serialized[1], LANCE_BYTES);
  }

  @Test
  public void testMergeByteConstructed() {
    Metadata raw = new Metadata(KEY.asciiName(), LANCE_BYTES);
    Metadata serializable = new Metadata();
    serializable.merge(raw);

    byte[][] serialized = serializable.serialize();
    assertArrayEquals(serialized[0], KEY.asciiName());
    assertArrayEquals(serialized[1], LANCE_BYTES);
    assertEquals(new Fish(LANCE), serializable.get(KEY));
  }

  @Test
  public void headerMergeShouldCopyValues() {
    Fish lance = new Fish(LANCE);
    Metadata h1 = new Metadata();

    Metadata h2 = new Metadata();
    h2.put(KEY, lance);

    h1.merge(h2);

    Iterator<Fish> fishes = h1.getAll(KEY).iterator();
    assertTrue(fishes.hasNext());
    assertEquals(fishes.next(), lance);
    assertFalse(fishes.hasNext());
  }

  @Test
  public void mergeExpands() {
    Fish lance = new Fish(LANCE);
    Metadata h1 = new Metadata();
    h1.put(KEY, lance);

    Metadata h2 = new Metadata();
    h2.put(KEY, lance);
    h2.put(KEY, lance);
    h2.put(KEY, lance);
    h2.put(KEY, lance);

    h1.merge(h2);
  }

  @Test
  public void shortBinaryKeyName() {
    assertThrows(IllegalArgumentException.class, () -> Metadata.Key.of("-bin", FISH_MARSHALLER));
  }

  @Test
  public void invalidSuffixBinaryKeyName() {
    IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
        () -> Metadata.Key.of("nonbinary", FISH_MARSHALLER));
    assertThat(e).hasMessageThat()
        .isEqualTo("Binary header is named nonbinary. It must end with -bin");
  }

  @Test
  public void verifyToString() {
    Metadata h = new Metadata();
    h.put(KEY, new Fish("binary"));
    h.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
    assertEquals("Metadata(test-bin=YmluYXJ5,test=ascii)", h.toString());

    Metadata t = new Metadata();
    t.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
    assertEquals("Metadata(test=ascii)", t.toString());

    t = new Metadata("test".getBytes(US_ASCII), "ascii".getBytes(US_ASCII),
        "test-bin".getBytes(US_ASCII), "binary".getBytes(US_ASCII));
    assertEquals("Metadata(test=ascii,test-bin=YmluYXJ5)", t.toString());
  }

  @Test
  public void verifyToString_usingBinary() {
    Metadata h = new Metadata();
    h.put(KEY, new Fish("binary"));
    h.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
    assertEquals("Metadata(test-bin=YmluYXJ5,test=ascii)", h.toString());

    Metadata t = new Metadata();
    t.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "ascii");
    assertEquals("Metadata(test=ascii)", t.toString());
  }

  @Test
  @SuppressWarnings("StringCaseLocaleUsage")  // System locale is exactly what we're testing.
  public void testKeyCaseHandling() {
    Locale originalLocale = Locale.getDefault();
    Locale.setDefault(new Locale("tr", "TR"));
    try {
      // In Turkish, both I and i (which are in ASCII) change into non-ASCII characters when their
      // case is changed as ı and İ, respectively.
      assertEquals("İ", "i".toUpperCase());
      assertEquals("ı", "I".toLowerCase());

      Metadata.Key<String> keyTitleCase
          = Metadata.Key.of("If-Modified-Since", Metadata.ASCII_STRING_MARSHALLER);
      Metadata.Key<String> keyLowerCase
          = Metadata.Key.of("if-modified-since", Metadata.ASCII_STRING_MARSHALLER);
      Metadata.Key<String> keyUpperCase
          = Metadata.Key.of("IF-MODIFIED-SINCE", Metadata.ASCII_STRING_MARSHALLER);

      Metadata metadata = new Metadata();
      metadata.put(keyTitleCase, "plain string");
      assertEquals("plain string", metadata.get(keyTitleCase));
      assertEquals("plain string", metadata.get(keyLowerCase));
      assertEquals("plain string", metadata.get(keyUpperCase));

      byte[][] bytes = metadata.serialize();
      assertEquals(2, bytes.length);
      assertArrayEquals("if-modified-since".getBytes(US_ASCII), bytes[0]);
      assertArrayEquals("plain string".getBytes(US_ASCII), bytes[1]);
    } finally {
      Locale.setDefault(originalLocale);
    }
  }

  @Test
  public void removeIgnoresMissingValue() {
    Metadata m = new Metadata();
    // Any key will work.
    Metadata.Key<String> key = GrpcUtil.USER_AGENT_KEY;

    boolean success = m.remove(key, "agent");
    assertFalse(success);
  }

  @Test
  public void removeAllIgnoresMissingValue() {
    Metadata m = new Metadata();
    // Any key will work.
    Metadata.Key<String> key = GrpcUtil.USER_AGENT_KEY;

    Iterable<String> removed = m.removeAll(key);
    assertNull(removed);
  }

  @Test
  public void keyEqualsHashNameWorks() {
    Metadata.Key<?> k1 = Metadata.Key.of("case", Metadata.ASCII_STRING_MARSHALLER);
    Metadata.Key<?> k2 = Metadata.Key.of("CASE", Metadata.ASCII_STRING_MARSHALLER);

    new EqualsTester()
        .addEqualityGroup(k1, k2)
        .addEqualityGroup(new Object(){})
        .testEquals();
    // Check that the casing is preserved.
    assertEquals("CASE", k2.originalName());
    assertEquals("case", k2.name());
  }

  @Test
  public void invalidKeyName() {
    try {
      Metadata.Key.of("io.grpc/key1", Metadata.ASCII_STRING_MARSHALLER);
      fail("Should have thrown");
    } catch (IllegalArgumentException e) {
      assertEquals("Invalid character '/' in key name 'io.grpc/key1'", e.getMessage());
    }
  }

  @Test
  public void streamedValue() {
    Fish salmon = new Fish("salmon");
    Metadata h = new Metadata();
    h.put(KEY_STREAMED, salmon);
    assertEquals(salmon, h.get(KEY_STREAMED));
  }

  @Test
  public void streamedValueDifferentKey() {
    Fish salmon = new Fish("salmon");
    Metadata h = new Metadata();
    h.put(KEY_STREAMED, salmon);

    // Get using a different key instance (but the same marshaller).
    Fish fish = h.get(copyKey(KEY_STREAMED, FISH_STREAM_MARSHALLER));
    assertEquals(salmon, fish);
  }

  @Test
  public void streamedValueDifferentMarshaller() {
    Fish salmon = new Fish("salmon");
    Metadata h = new Metadata();
    h.put(KEY_STREAMED, salmon);

    // Get using a different marshaller instance.
    Fish fish = h.get(copyKey(KEY_STREAMED, new FishStreamMarshaller()));
    assertEquals(salmon, fish);
  }

  @Test
  public void serializeParseMetadataWithStreams() {
    Metadata h = new Metadata();
    Fish salmon = new Fish("salmon");
    h.put(KEY_STREAMED, salmon);

    Metadata parsed = new Metadata(h.serialize());
    assertEquals(salmon, parsed.get(KEY_STREAMED));
  }

  @Test
  public void immutableMarshaller() {
    Metadata h = new Metadata(KEY.asciiName(), LANCE_BYTES);
    Fish salmon = new Fish("salmon");
    h.put(KEY_IMMUTABLE, salmon);
    assertSame(salmon, h.get(KEY_IMMUTABLE));
    // Even though the key differs, the marshaller can chose to avoid serialization.
    assertSame(salmon, h.get(copyKey(KEY_IMMUTABLE, IMMUTABLE_FISH_MARSHALLER)));
  }

  @Test
  public void partialSerialization() {
    Metadata h = new Metadata(KEY.asciiName(), LANCE_BYTES);
    Fish salmon = new Fish("salmon");
    h.put(KEY_STREAMED, salmon);
    h.put(KEY_IMMUTABLE, salmon);

    Object[] serialized = InternalMetadata.serializePartial(h);
    assertEquals(6, serialized.length);
    assertEquals("test-bin", new String((byte[]) serialized[0], US_ASCII));
    assertArrayEquals(LANCE_BYTES, (byte[]) serialized[1]);

    assertEquals("streamed-bin", new String((byte[]) serialized[2], US_ASCII));
    assertEquals(salmon, FISH_STREAM_MARSHALLER.parseStream((InputStream) serialized[3]));
    assertNotSame(salmon, FISH_STREAM_MARSHALLER.parseStream((InputStream) serialized[3]));

    assertEquals("immutable-bin", new String((byte[]) serialized[4], US_ASCII));
    assertSame(salmon, IMMUTABLE_FISH_MARSHALLER.parseStream((InputStream) serialized[5]));
  }

  @Test
  public void createFromPartial() {
    Metadata h = new Metadata(KEY.asciiName(), LANCE_BYTES);
    Fish salmon = new Fish("salmon");
    h.put(KEY_STREAMED, salmon);
    h.put(KEY_IMMUTABLE, salmon);

    Fish anotherSalmon = new Fish("salmon");

    Object[] partial = InternalMetadata.serializePartial(h);
    partial[3] = InternalMetadata.parsedValue(FISH_STREAM_MARSHALLER, anotherSalmon);
    partial[5] = InternalMetadata.parsedValue(IMMUTABLE_FISH_MARSHALLER, anotherSalmon);

    Metadata h2 = new Metadata(3, partial);
    assertEquals(new Fish(LANCE), h2.get(KEY));
    assertEquals(anotherSalmon, h2.get(KEY_STREAMED));
    assertSame(anotherSalmon, h2.get(KEY_IMMUTABLE));
  }

  private static final class Fish {
    private String name;

    private Fish(String name) {
      this.name = name;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      Fish fish = (Fish) o;
      if (name != null ? !name.equals(fish.name) : fish.name != null) {
        return false;
      }
      return true;
    }

    @Override
    public int hashCode() {
      return name.hashCode();
    }

    @Override
    public String toString() {
      return "Fish(" + name + ")";
    }
  }

  private static <T> Metadata.Key<T> copyKey(
      Metadata.Key<T> key, Metadata.BinaryStreamMarshaller<T> marshaller) {
    return Metadata.Key.of(key.originalName(), marshaller);
  }
}
