/*
 * Copyright 2016-2020 The OpenZipkin 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 brave.opentracing;

import brave.Span.Kind;
import brave.Tracing;
import brave.baggage.BaggageField;
import brave.baggage.BaggagePropagation;
import brave.baggage.BaggagePropagationConfig.SingleBaggageField;
import brave.handler.MutableSpan;
import brave.propagation.B3Propagation;
import brave.propagation.StrictCurrentTraceContext;
import brave.sampler.Sampler;
import brave.test.TestSpanHandler;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.propagation.TextMapAdapter;
import io.opentracing.tag.Tag;
import io.opentracing.tag.Tags;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static io.opentracing.propagation.Format.Builtin.TEXT_MAP;
import static io.opentracing.tag.Tags.SAMPLING_PRIORITY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

@RunWith(DataProviderRunner.class)
public class OpenTracing0_33_BraveSpanTest {
  StrictCurrentTraceContext currentTraceContext = StrictCurrentTraceContext.create();
  TestSpanHandler spans = new TestSpanHandler();
  Tracing brave;
  BraveTracer tracer;

  @Before public void init() {
    init(Tracing.newBuilder());
  }

  void init(Tracing.Builder tracingBuilder) {
    if (brave != null) brave.close();
    brave = tracingBuilder
        .localServiceName("tracer")
        .currentTraceContext(currentTraceContext)
        .addSpanHandler(spans)
        .propagationFactory(BaggagePropagation.newFactoryBuilder(B3Propagation.FACTORY)
            .add(SingleBaggageField.remote(BaggageField.create("client-id")))
            .build()).build();
    tracer = BraveTracer.create(brave);
  }

  @After public void clear() {
    brave.close();
    currentTraceContext.close();
  }

  @DataProvider
  public static Object[][] dataProviderKind() {
    return new Object[][] {
        {Tags.SPAN_KIND_CLIENT, Kind.CLIENT},
        {Tags.SPAN_KIND_SERVER, Kind.SERVER},
        {Tags.SPAN_KIND_PRODUCER, Kind.PRODUCER},
        {Tags.SPAN_KIND_CONSUMER, Kind.CONSUMER}
    };
  }

  @Test @UseDataProvider("dataProviderKind")
  public void spanKind_beforeStart(String tagValue, Kind kind) {
    tracer.buildSpan("foo")
        .withTag(Tags.SPAN_KIND.getKey(), tagValue)
        .start().finish();

    MutableSpan span = spans.get(0);
    assertThat(span.kind())
        .isEqualTo(kind);

    assertThat(span.tags())
        .isEmpty();
  }

  @Test public void spanKind_beforeStart_mismatch() {
    tracer.buildSpan("foo")
        .withTag(Tags.SPAN_KIND.getKey(), "antelope")
        .start().finish();

    MutableSpan span = spans.get(0);
    assertThat(span.kind())
        .isNull();

    assertThat(span.tags())
        .containsEntry("span.kind", "antelope");
  }

  @Test @UseDataProvider("dataProviderKind")
  public void spanKind_afterStart(String tagValue, Kind kind) {
    tracer.buildSpan("foo")
        .start()
        .setTag(Tags.SPAN_KIND.getKey(), tagValue)
        .finish();

    MutableSpan span = spans.get(0);
    assertThat(span.kind())
        .isEqualTo(kind);

    assertThat(span.tags())
        .isEmpty();
  }

  @Test public void spanKind_afterStart_mismatch() {
    tracer.buildSpan("foo")
        .start()
        .setTag(Tags.SPAN_KIND.getKey(), "antelope")
        .finish();

    MutableSpan span = spans.get(0);
    assertThat(span.kind())
        .isNull();

    assertThat(span.tags())
        .containsEntry("span.kind", "antelope");
  }

  /** Tags end up as string binary annotations */
  @Test public void startedSpan_setTag() {
    Span span = tracer.buildSpan("foo").start();
    span.setTag("hello", "monster");
    span.finish();

    assertThat(spans)
        .flatExtracting(s -> s.tags().entrySet())
        .containsOnly(entry("hello", "monster"));
  }

  @Test public void afterFinish_dataIgnored() {
    Span span = tracer.buildSpan("foo").start();
    span.finish();
    spans.clear();

    span.setOperationName("bar");
    span.setTag("hello", "monster");
    span.log("alarming");
    span.finish();

    assertThat(spans)
        .isEmpty();
  }

  @Test public void childSpanWhenParentIsExtracted() {
    Span spanClient = tracer.buildSpan("foo")
        .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
        .start();

    Map<String, String> carrier = new LinkedHashMap<>();
    tracer.inject(spanClient.context(), TEXT_MAP, new TextMapAdapter(carrier));

    Tracing brave2 = Tracing.newBuilder().localServiceName("tracer2").addSpanHandler(spans).build();
    BraveTracer tracer2 = BraveTracer.create(brave2);

    SpanContext extractedContext = tracer2.extract(TEXT_MAP, new TextMapAdapter(carrier));

    Span spanServer = tracer2.buildSpan("foo")
        .asChildOf(extractedContext)
        .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
        .start();

    tracer2.buildSpan("bar")
        .asChildOf(spanServer)
        .start()
        .finish();

    spanServer.finish();
    spanClient.finish();

    assertThat(spans).hasSize(3);
    assertThat(spans.get(0).traceId())
        .isEqualTo(spans.get(1).traceId())
        .isEqualTo(spans.get(2).traceId());
    assertThat(spans.get(1).id()).isEqualTo(spans.get(2).id()); // supportsJoin is default
    assertThat(spans.get(0).id()).isNotEqualTo(spans.get(1).id());

    // child first
    assertThat(spans.get(0).localServiceName()).isEqualTo("tracer2");
    assertThat(spans.get(1).localServiceName()).isEqualTo("tracer2");
    assertThat(spans.get(2).localServiceName()).isEqualTo("tracer");

    brave2.close();
  }

  @Test public void extractDoesntDropBaggage() {
    Map<String, String> carrier = new LinkedHashMap<>();
    carrier.put("client-id", "aloha");

    SpanContext extractedContext =
        tracer.extract(TEXT_MAP, new TextMapAdapter(carrier));

    assertThat(extractedContext.baggageItems())
        .contains(entry("client-id", "aloha"));

    Span serverSpan = tracer.buildSpan("foo")
        .asChildOf(extractedContext)
        .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
        .start();

    assertThat(serverSpan.getBaggageItem("client-id"))
        .isEqualTo("aloha");

    serverSpan.finish();
  }

  @Test public void samplingPriority_sampledWhenAtStart() {
    init(Tracing.newBuilder().sampler(Sampler.NEVER_SAMPLE));

    BraveSpan span = tracer.buildSpan("foo")
        .withTag(SAMPLING_PRIORITY.getKey(), 1)
        .start();

    assertThat(span.context().unwrap().sampled())
        .isTrue();

    span.finish();
    assertThat(spans).hasSize(1);
  }

  @Test public void samplingPriority_unsampledWhenAtStart() {
    BraveSpan span = tracer.buildSpan("foo")
        .withTag(SAMPLING_PRIORITY.getKey(), 0)
        .start();

    assertThat(span.context().unwrap().sampled())
        .isFalse();

    span.finish();
    assertThat(spans).isEmpty();
  }

  @Test public void samplingPriority_abandonsAndUnsampledAfterStart() {
    BraveSpan span = tracer.buildSpan("foo")
        .start();

    assertThat(span.context().unwrap().sampled())
        .isTrue();

    // this is a known race-condition as sampling decision could have been propagated downstream!
    SAMPLING_PRIORITY.set(span, 0);
    assertThat(span.context().unwrap().sampled())
        .isFalse();

    assertThat(span.delegate.isNoop())
        .isTrue();

    span.finish();
    assertThat(spans).isEmpty();
  }

  @Test public void setPeerTags_beforeStart() {
    tracer.buildSpan("encode")
        .withTag(Tags.PEER_SERVICE.getKey(), "jupiter")
        .withTag(Tags.PEER_HOST_IPV4.getKey(), "1.2.3.4")
        .withTag(Tags.PEER_HOST_IPV6.getKey(), "2001:db8::c001")
        .withTag(Tags.PEER_PORT.getKey(), 8080)
        .start().finish();

    assertThat(spans.get(0).remoteServiceName()).isEqualTo("jupiter");
    assertThat(spans.get(0).remoteIp()).isEqualTo("2001:db8::c001");
    assertThat(spans.get(0).remotePort()).isEqualTo(8080);
  }

  @Test public void setPeerTags_afterStart() {
    tracer.buildSpan("encode")
        .start()
        .setTag(Tags.PEER_SERVICE.getKey(), "jupiter")
        .setTag(Tags.PEER_HOST_IPV4.getKey(), "1.2.3.4")
        .setTag(Tags.PEER_HOST_IPV6.getKey(), "2001:db8::c001")
        .setTag(Tags.PEER_PORT.getKey(), 8080)
        .finish();

    assertThat(spans.get(0).remoteServiceName()).isEqualTo("jupiter");
    assertThat(spans.get(0).remoteIp()).isEqualTo("2001:db8::c001");
    assertThat(spans.get(0).remotePort()).isEqualTo(8080);
  }

  @Test public void withTag() {
    tracer.buildSpan("encode")
        .withTag(Tags.HTTP_METHOD.getKey(), "GET")
        .withTag(Tags.ERROR.getKey(), true)
        .withTag(Tags.HTTP_STATUS.getKey(), 404)
        .start().finish();

    assertContainsTags();
  }

  @Test public void withTag_object() {
    tracer.buildSpan("encode")
        .withTag(Tags.HTTP_METHOD, "GET")
        .withTag(Tags.ERROR, true)
        .withTag(Tags.HTTP_STATUS, 404)
        .start().finish();

    assertContainsTags();
  }

  @Test public void setTag() {
    tracer.buildSpan("encode").start()
        .setTag(Tags.HTTP_METHOD.getKey(), "GET")
        .setTag(Tags.ERROR.getKey(), true)
        .setTag(Tags.HTTP_STATUS.getKey(), 404)
        .finish();

    assertContainsTags();
  }

  @Test public void setTag_object() {
    tracer.buildSpan("encode").start()
        .setTag(Tags.HTTP_METHOD, "GET")
        .setTag(Tags.ERROR, true)
        .setTag(Tags.HTTP_STATUS, 404)
        .finish();

    assertContainsTags();
  }

  void assertContainsTags() {
    assertThat(spans.get(0).tags())
        .containsEntry("http.method", "GET")
        .containsEntry("error", "true")
        .containsEntry("http.status_code", "404");
  }

  Tag<Exception> exceptionTag = new Tag<Exception>() {
    @Override public String getKey() {
      return "exception";
    }

    @Override public void set(Span span, Exception value) {
      span.setTag(getKey(), value.getClass().getSimpleName());
    }
  };

  @Test public void setTag_custom() {
    tracer.buildSpan("encode").start()
        .setTag(exceptionTag, new RuntimeException("ice cream")).finish();

    assertThat(spans.get(0).tags())
        .containsEntry("exception", "RuntimeException");
  }

  /** There is no javadoc, but we were told only string, bool or number? */
  @Test(expected = IllegalArgumentException.class)
  public void withTag_custom_unsupported() {
    tracer.buildSpan("encode")
        .withTag(exceptionTag, new RuntimeException("ice cream"));
  }
}