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

package io.opentelemetry.javaagent.instrumentation.jul;

import static io.opentelemetry.sdk.testing.assertj.LogAssertions.assertThat;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;

import io.opentelemetry.api.logs.Severity;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class JavaUtilLoggingTest {

  private static final Logger logger = Logger.getLogger("abc");

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

  private static Stream<Arguments> provideParameters() {
    return Stream.of(
        Arguments.of(false, false, false),
        Arguments.of(false, false, true),
        Arguments.of(false, true, false),
        Arguments.of(false, true, true),
        Arguments.of(true, false, false),
        Arguments.of(true, false, true),
        Arguments.of(true, true, false),
        Arguments.of(true, true, true));
  }

  @ParameterizedTest
  @MethodSource("provideParameters")
  public void test(boolean withParam, boolean logException, boolean withParent)
      throws InterruptedException {
    test(Level.FINE, Logger::fine, withParam, logException, withParent, null, null, null);
    testing.clearData();
    test(
        Level.INFO,
        Logger::info,
        withParam,
        logException,
        withParent,
        "abc",
        Severity.INFO,
        "INFO");
    testing.clearData();
    test(
        Level.WARNING,
        Logger::warning,
        withParam,
        logException,
        withParent,
        "abc",
        Severity.WARN,
        "WARNING");
    testing.clearData();
    test(
        Level.SEVERE,
        Logger::severe,
        withParam,
        logException,
        withParent,
        "abc",
        Severity.ERROR,
        "SEVERE");
    testing.clearData();
  }

  private static void test(
      Level level,
      LoggerMethod loggerMethod,
      boolean withParam,
      boolean logException,
      boolean withParent,
      String expectedLoggerName,
      Severity expectedSeverity,
      String expectedSeverityText)
      throws InterruptedException {

    // when
    if (withParent) {
      testing.runWithSpan(
          "parent", () -> performLogging(level, loggerMethod, withParam, logException));
    } else {
      performLogging(level, loggerMethod, withParam, logException);
    }

    // then
    if (withParent) {
      testing.waitForTraces(1);
    }

    if (expectedSeverity != null) {
      LogRecordData log = testing.waitForLogRecords(1).get(0);
      assertThat(log)
          .hasBody(withParam ? "xyz: 123" : "xyz")
          .hasInstrumentationScope(InstrumentationScopeInfo.builder(expectedLoggerName).build())
          .hasSeverity(expectedSeverity)
          .hasSeverityText(expectedSeverityText);
      if (logException) {
        assertThat(log)
            .hasAttributesSatisfyingExactly(
                equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()),
                equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()),
                equalTo(SemanticAttributes.EXCEPTION_TYPE, IllegalStateException.class.getName()),
                equalTo(SemanticAttributes.EXCEPTION_MESSAGE, "hello"),
                satisfies(
                    SemanticAttributes.EXCEPTION_STACKTRACE,
                    v -> v.contains(JavaUtilLoggingTest.class.getName())));
      } else {
        assertThat(log)
            .hasAttributesSatisfyingExactly(
                equalTo(SemanticAttributes.THREAD_NAME, Thread.currentThread().getName()),
                equalTo(SemanticAttributes.THREAD_ID, Thread.currentThread().getId()));
      }

      if (withParent) {
        assertThat(log).hasSpanContext(testing.spans().get(0).getSpanContext());
      } else {
        assertThat(log.getSpanContext().isValid()).isFalse();
      }

    } else {
      Thread.sleep(500); // sleep a bit just to make sure no log is captured
      assertThat(testing.logRecords()).isEmpty();
    }
  }

  private static void performLogging(
      Level level, LoggerMethod loggerMethod, boolean withParam, boolean logException) {
    if (logException) {
      if (withParam) {
        // this is the best j.u.l. can do
        logger.log(level, new IllegalStateException("hello"), () -> "xyz: 123");
      } else {
        logger.log(level, "xyz", new IllegalStateException("hello"));
      }
    } else {
      if (withParam) {
        logger.log(level, "xyz: {0}", 123);
      } else {
        loggerMethod.call(logger, "xyz");
      }
    }
  }

  @FunctionalInterface
  interface LoggerMethod {
    void call(Logger logger, String msg);
  }
}
