/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.opencensusshim;

import io.opencensus.trace.AttributeValue;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.samplers.Samplers;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions;
import org.assertj.core.api.AbstractBooleanAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class JavaagentInstrumentationTest {

  @RegisterExtension
  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();

  @BeforeEach
  void setup() {
    // by default in opencensus, a probability sampling is used which is not 100%;
    // we specifically set this configuration here to always sample to ensure traces are emitted for
    // our tests
    Tracing.getTraceConfig()
        .updateActiveTraceParams(
            Tracing.getTraceConfig().getActiveTraceParams().toBuilder()
                .setSampler(Samplers.alwaysSample())
                .build());
  }

  @Test
  void testInterleavedSpansOcFirst() {
    io.opencensus.trace.Tracer ocTracer = Tracing.getTracer();
    Tracer otelTracer = testing.getOpenTelemetry().getTracer("test");

    io.opencensus.trace.Span outerSpan = ocTracer.spanBuilder("outer-span").startSpan();
    Span midSpan;
    io.opencensus.trace.Span innerSpan;

    outerSpan.putAttribute("outer", AttributeValue.booleanAttributeValue(true));

    try (io.opencensus.common.Scope outerScope = ocTracer.withSpan(outerSpan)) {
      midSpan =
          otelTracer
              .spanBuilder("mid-span")
              .setSpanKind(SpanKind.INTERNAL)
              .setAttribute("middle", true)
              .startSpan();
      try (Scope midScope = midSpan.makeCurrent()) {
        innerSpan = ocTracer.spanBuilder("inner-span").startSpan();
        innerSpan.putAttribute("inner", AttributeValue.booleanAttributeValue(true));

        // make current and immediately close -- avoid empty try block
        ocTracer.withSpan(innerSpan).close();

        innerSpan.end();
      } finally {
        midSpan.end();
      }
    } finally {
      outerSpan.end();
    }

    Tracing.getExportComponent().shutdown();

    // expecting 1 trace with 3 spans
    testing.waitAndAssertTraces(
        ta ->
            // ensure each span's attributes haven't seeped into parents or children
            ta.hasSpansSatisfyingExactly(
                // outer span
                sa ->
                    sa.hasName("outer-span")
                        .hasNoParent()
                        .hasAttribute(AttributeKey.booleanKey("outer"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull)),
                // middle span
                sa ->
                    sa.hasName("mid-span")
                        .hasParent(ta.getSpan(0))
                        .hasAttribute(AttributeKey.booleanKey("middle"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)),
                // inner span
                sa ->
                    sa.hasName("inner-span")
                        .hasParent(ta.getSpan(1))
                        .hasAttribute(AttributeKey.booleanKey("inner"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull))));
  }

  @Test
  void testInterleavedSpansOtelFirst() {
    io.opencensus.trace.Tracer ocTracer = Tracing.getTracer();
    Tracer otelTracer = testing.getOpenTelemetry().getTracer("test");

    Span outerSpan =
        otelTracer
            .spanBuilder("outer-span")
            .setSpanKind(SpanKind.INTERNAL)
            .setAttribute("outer", true)
            .startSpan();
    io.opencensus.trace.Span midSpan;
    Span innerSpan;

    try (Scope outerScope = outerSpan.makeCurrent()) {
      midSpan = ocTracer.spanBuilder("mid-span").startSpan();
      midSpan.putAttribute("middle", AttributeValue.booleanAttributeValue(true));
      try (io.opencensus.common.Scope midScope = ocTracer.withSpan(midSpan)) {
        innerSpan =
            otelTracer
                .spanBuilder("inner-span")
                .setSpanKind(SpanKind.INTERNAL)
                .setAttribute("inner", true)
                .startSpan();

        // make current and immediately close -- avoid empty try block
        innerSpan.makeCurrent().close();

        innerSpan.end();
      } finally {
        midSpan.end();
      }
    } finally {
      outerSpan.end();
    }

    Tracing.getExportComponent().shutdown();

    // expecting 1 trace with 3 spans
    testing.waitAndAssertTraces(
        ta ->
            // ensure each span's attributes haven't seeped into parents or children
            ta.hasSpansSatisfyingExactly(
                // outer span
                sa ->
                    sa.hasName("outer-span")
                        .hasNoParent()
                        .hasAttribute(AttributeKey.booleanKey("outer"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull)),
                // middle span
                sa ->
                    sa.hasName("mid-span")
                        .hasParent(ta.getSpan(0))
                        .hasAttribute(AttributeKey.booleanKey("middle"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)),
                // inner span
                sa ->
                    sa.hasName("inner-span")
                        .hasParent(ta.getSpan(1))
                        .hasAttribute(AttributeKey.booleanKey("inner"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull))));
  }

  @Test
  void testStartingWithOtelSpan() {
    io.opencensus.trace.Tracer ocTracer = Tracing.getTracer();
    Tracer otelTracer = testing.getOpenTelemetry().getTracer("test");

    Span otelSpan =
        otelTracer
            .spanBuilder("otel-span")
            .setSpanKind(SpanKind.INTERNAL)
            .setAttribute("present-on-otel", true)
            .startSpan();

    io.opencensus.trace.Span ocSpan;
    try (Scope scope = otelSpan.makeCurrent()) {
      ocSpan = ocTracer.spanBuilder("oc-span").startSpan();
      try (io.opencensus.common.Scope ocScope = ocTracer.withSpan(ocSpan)) {
        ocTracer
            .getCurrentSpan()
            .putAttribute("present-on-oc", AttributeValue.booleanAttributeValue(true));
      }
      ocSpan.end();
    }
    otelSpan.end();

    Tracing.getExportComponent().shutdown();

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span ->
                    span.hasName("otel-span")
                        .hasNoParent()
                        .hasAttribute(AttributeKey.booleanKey("present-on-otel"), true),
                span ->
                    span.hasName("oc-span")
                        .hasParent(trace.getSpan(0))
                        .hasAttribute(AttributeKey.booleanKey("present-on-oc"), true)));
  }

  @Test
  void testStartingWithOpenCensusSpan() {
    io.opencensus.trace.Tracer ocTracer = Tracing.getTracer();
    Tracer otelTracer = testing.getOpenTelemetry().getTracer("test");

    io.opencensus.trace.Span ocSpan = ocTracer.spanBuilder("oc-span").startSpan();

    ocSpan.putAttribute("present-on-oc", AttributeValue.booleanAttributeValue(true));

    Span otelSpan;
    try (io.opencensus.common.Scope ocScope = ocTracer.withSpan(ocSpan)) {
      otelSpan = otelTracer.spanBuilder("otel-span").setSpanKind(SpanKind.INTERNAL).startSpan();
      try (Scope scope = otelSpan.makeCurrent()) {
        Span.current().setAttribute("present-on-otel", true);
      }
      otelSpan.end();
    }
    ocSpan.end();

    Tracing.getExportComponent().shutdown();

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span ->
                    span.hasName("oc-span")
                        .hasNoParent()
                        .hasAttribute(AttributeKey.booleanKey("present-on-oc"), true),
                span ->
                    span.hasName("otel-span")
                        .hasParent(trace.getSpan(0))
                        .hasAttribute(AttributeKey.booleanKey("present-on-otel"), true)));
  }

  @Test
  void testNestedOpenCensusSpans() {
    io.opencensus.trace.Tracer ocTracer = Tracing.getTracer();

    io.opencensus.trace.Span outerSpan = ocTracer.spanBuilder("outer-span").startSpan();
    io.opencensus.trace.Span midSpan;
    io.opencensus.trace.Span innerSpan;

    outerSpan.putAttribute("outer", AttributeValue.booleanAttributeValue(true));

    try (io.opencensus.common.Scope outerScope = ocTracer.withSpan(outerSpan)) {
      midSpan = ocTracer.spanBuilder("mid-span").startSpan();
      midSpan.putAttribute("middle", AttributeValue.booleanAttributeValue(true));
      try (io.opencensus.common.Scope midScope = ocTracer.withSpan(midSpan)) {
        innerSpan = ocTracer.spanBuilder("inner-span").startSpan();
        innerSpan.putAttribute("inner", AttributeValue.booleanAttributeValue(true));

        // make current and immediately close -- avoid empty try block
        ocTracer.withSpan(innerSpan).close();

        innerSpan.end();
      } finally {
        midSpan.end();
      }
    } finally {
      outerSpan.end();
    }

    Tracing.getExportComponent().shutdown();

    // expecting 1 trace with 3 spans
    testing.waitAndAssertTraces(
        ta ->
            // ensure each span's attributes haven't seeped into parents or children
            ta.hasSpansSatisfyingExactly(
                // outer span
                sa ->
                    sa.hasName("outer-span")
                        .hasNoParent()
                        .hasAttribute(AttributeKey.booleanKey("outer"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull)),
                // middle span
                sa ->
                    sa.hasName("mid-span")
                        .hasParent(ta.getSpan(0))
                        .hasAttribute(AttributeKey.booleanKey("middle"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("inner"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull)),
                // inner span
                sa ->
                    sa.hasName("inner-span")
                        .hasParent(ta.getSpan(1))
                        .hasAttribute(AttributeKey.booleanKey("inner"), true)
                        .hasAttributesSatisfying(
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("middle"), AbstractBooleanAssert::isNull),
                            OpenTelemetryAssertions.satisfies(
                                AttributeKey.booleanKey("outer"), AbstractBooleanAssert::isNull))));
  }
}
