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

package io.opentelemetry.javaagent.instrumentation.hibernate.v4_0;

import static io.opentelemetry.api.common.AttributeKey.stringKey;
import static io.opentelemetry.api.trace.SpanKind.CLIENT;
import static io.opentelemetry.api.trace.SpanKind.INTERNAL;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
import static org.junit.jupiter.api.Named.named;

import io.opentelemetry.sdk.trace.data.StatusData;
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.ReplicationMode;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@SuppressWarnings("deprecation") // 'lock' is a deprecated method in the Session class
class SessionTest extends AbstractHibernateTest {

  @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation
  @ParameterizedTest
  @MethodSource("provideArgumentsHibernateAction")
  void testHibernateAction(Parameter parameter) {
    testing.runWithSpan(
        "parent",
        () -> {
          Session session = sessionFactory.openSession();
          session.beginTransaction();
          parameter.sessionMethodTest.accept(session, prepopulated.get(0));
          session.getTransaction().commit();
          session.close();
        });

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span ->
                    span.hasName("Session." + parameter.methodName + " " + Value.class.getName())
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            satisfies(
                                stringKey("hibernate.session_id"),
                                val -> val.isInstanceOf(String.class))),
                span ->
                    span.hasKind(CLIENT)
                        .hasParent(trace.getSpan(1))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                val -> val.isInstanceOf(String.class)),
                            satisfies(
                                DbIncubatingAttributes.DB_OPERATION,
                                val -> val.isInstanceOf(String.class)),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id"))))));
  }

  private static Stream<Arguments> provideArgumentsHibernateAction() {
    return Stream.of(
        Arguments.of(
            named(
                "lock",
                new Parameter(
                    "lock",
                    (Session session, Value val) -> session.lock(val, LockMode.READ),
                    null))),
        Arguments.of(
            named(
                "lock with entity name",
                new Parameter(
                    "lock",
                    (Session session, Value val) ->
                        session.lock(Value.class.getName(), val, LockMode.READ),
                    null))),
        Arguments.of(
            named(
                "lock with null name",
                new Parameter(
                    "lock",
                    (Session session, Value val) -> session.lock(null, val, LockMode.READ),
                    null))),
        Arguments.of(
            named(
                "buildLockRequest",
                new Parameter(
                    "lock",
                    (Session session, Value val) ->
                        session.buildLockRequest(LockOptions.READ).lock(val),
                    null))),
        Arguments.of(named("refresh", new Parameter("refresh", Session::refresh, null))),
        Arguments.of(
            named(
                "refresh with entity name",
                new Parameter(
                    "refresh",
                    (Session session, Value val) -> session.refresh(Value.class.getName(), val),
                    null))),
        Arguments.of(
            named(
                "get with entity name",
                new Parameter(
                    "get",
                    (Session session, Value val) -> session.get(Value.class.getName(), val.getId()),
                    null))),
        Arguments.of(
            named(
                "get with entity class",
                new Parameter(
                    "get",
                    (Session session, Value val) -> session.get(Value.class, val.getId()),
                    null))));
  }

  @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation
  @ParameterizedTest
  @MethodSource("provideArgumentsHibernateActionStateless")
  void testHibernateActionStateless(Parameter parameter) {

    testing.runWithSpan(
        "parent",
        () -> {
          StatelessSession session = sessionFactory.openStatelessSession();
          session.beginTransaction();
          parameter.statelessSessionMethodTest.accept(session, prepopulated.get(0));
          session.getTransaction().commit();
          session.close();
        });

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span ->
                    span.hasName("Session." + parameter.methodName + " " + Value.class.getName())
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            satisfies(
                                stringKey("hibernate.session_id"),
                                val -> val.isInstanceOf(String.class))),
                span ->
                    span.hasKind(CLIENT)
                        .hasParent(trace.getSpan(1))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                val -> val.isInstanceOf(String.class)),
                            satisfies(
                                DbIncubatingAttributes.DB_OPERATION,
                                val -> val.isInstanceOf(String.class)),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id"))))));
  }

  private static Stream<Arguments> provideArgumentsHibernateActionStateless() {
    return Stream.of(
        Arguments.of(named("refresh", new Parameter("refresh", null, StatelessSession::refresh))),
        Arguments.of(
            named(
                "refresh with entity name",
                new Parameter(
                    "refresh",
                    null,
                    (StatelessSession session, Value val) ->
                        session.refresh(Value.class.getName(), val)))),
        Arguments.of(
            named(
                "get with entity name",
                new Parameter(
                    "get",
                    null,
                    (StatelessSession session, Value val) ->
                        session.get(Value.class.getName(), val.getId())))),
        Arguments.of(
            named(
                "get with entity class",
                new Parameter(
                    "get",
                    null,
                    (StatelessSession session, Value val) ->
                        session.get(Value.class, val.getId())))),
        Arguments.of(
            named(
                "insert",
                new Parameter(
                    "insert",
                    null,
                    (StatelessSession session, Value val) ->
                        session.insert(new Value("insert me"))))),
        Arguments.of(
            named(
                "insert with entity name",
                new Parameter(
                    "insert",
                    null,
                    (StatelessSession session, Value val) ->
                        session.insert(Value.class.getName(), new Value("insert me"))))),
        Arguments.of(
            named(
                "insert with null entity name",
                new Parameter(
                    "insert",
                    null,
                    (StatelessSession session, Value val) ->
                        session.insert(null, new Value("insert me"))))),
        Arguments.of(
            named(
                "update (StatelessSession)",
                new Parameter(
                    "update",
                    null,
                    (StatelessSession session, Value val) -> {
                      val.setName("New name");
                      session.update(val);
                    }))),
        Arguments.of(
            named(
                "update with entity name (StatelessSession)",
                new Parameter(
                    "update",
                    null,
                    (StatelessSession session, Value val) -> {
                      val.setName("New name");
                      session.update(Value.class.getName(), val);
                    }))),
        Arguments.of(
            named(
                "delete (Session)",
                new Parameter(
                    "delete",
                    null,
                    (StatelessSession session, Value val) -> {
                      session.delete(val);
                      prepopulated.remove(val);
                    }))),
        Arguments.of(
            named(
                "delete with entity name (Session)",
                new Parameter(
                    "delete",
                    null,
                    (StatelessSession session, Value val) -> {
                      session.delete(Value.class.getName(), val);
                      prepopulated.remove(val);
                    }))));
  }

  @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation
  @ParameterizedTest
  @MethodSource("provideArgumentsHibernateReplicate")
  void testHibernateReplicate(Parameter parameter) {
    testing.runWithSpan(
        "parent",
        () -> {
          Session session = sessionFactory.openSession();
          session.beginTransaction();
          parameter.sessionMethodTest.accept(session, prepopulated.get(0));
          session.getTransaction().commit();
          session.close();
        });

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span ->
                    span.hasName("Session." + parameter.methodName + " " + Value.class.getName())
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            satisfies(
                                stringKey("hibernate.session_id"),
                                val -> val.isInstanceOf(String.class))),
                span ->
                    span.hasKind(CLIENT)
                        .hasParent(trace.getSpan(1))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                val -> val.isInstanceOf(String.class)),
                            satisfies(
                                DbIncubatingAttributes.DB_OPERATION,
                                val -> val.isInstanceOf(String.class)),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id")))),
                span ->
                    span.hasKind(CLIENT)
                        .hasParent(trace.getSpan(3))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                val -> val.isInstanceOf(String.class)),
                            satisfies(
                                DbIncubatingAttributes.DB_OPERATION,
                                val -> val.isInstanceOf(String.class)),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value"))));
  }

  private static Stream<Arguments> provideArgumentsHibernateReplicate() {
    return Stream.of(
        Arguments.of(
            named(
                "replicate",
                new Parameter(
                    "replicate",
                    (Session session, Value val) -> {
                      Value replicated = new Value(val.getName() + " replicated");
                      replicated.setId(val.getId());
                      session.replicate(replicated, ReplicationMode.OVERWRITE);
                    },
                    null))),
        Arguments.of(
            named(
                "replicate by entity name",
                new Parameter(
                    "replicate",
                    (Session session, Value val) -> {
                      Value replicated = new Value(val.getName() + " replicated");
                      replicated.setId(val.getId());
                      session.replicate(
                          Value.class.getName(), replicated, ReplicationMode.OVERWRITE);
                    },
                    null))));
  }

  @Test
  void testHibernateFailedReplicate() {
    Throwable mappingException =
        testing.runWithSpan(
            "parent",
            () -> {
              Session session = sessionFactory.openSession();
              session.beginTransaction();

              Throwable exception =
                  catchThrowable(
                      () -> {
                        session.replicate(123L /* Not a valid entity */, ReplicationMode.OVERWRITE);
                      });

              session.getTransaction().commit();
              session.close();
              return exception;
            });

    assertThat(mappingException.getClass()).isEqualTo(MappingException.class);

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span ->
                    span.hasName("Session.replicate java.lang.Long")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasStatus(StatusData.error())
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id"))))
                        .hasException(mappingException),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id"))))));
  }

  @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation
  @ParameterizedTest
  @MethodSource("provideArgumentsHibernateCommitAction")
  void testHibernateCommitAction(Parameter parameter) {

    testing.runWithSpan(
        "parent",
        () -> {
          Session session = sessionFactory.openSession();
          session.beginTransaction();
          parameter.sessionMethodTest.accept(session, prepopulated.get(0));
          session.getTransaction().commit();
          session.close();
        });

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span ->
                    span.hasName("Session." + parameter.methodName + " " + Value.class.getName())
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            satisfies(
                                stringKey("hibernate.session_id"),
                                val -> val.isInstanceOf(String.class))),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id")))),
                span ->
                    span.hasKind(CLIENT)
                        .hasParent(trace.getSpan(2))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                val -> val.isInstanceOf(String.class)),
                            satisfies(
                                DbIncubatingAttributes.DB_OPERATION,
                                val -> val.isInstanceOf(String.class)),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value"))));
  }

  private static Stream<Arguments> provideArgumentsHibernateCommitAction() {
    return Stream.of(
        Arguments.of(
            named(
                "save",
                new Parameter(
                    "save",
                    (Session session, Value val) -> {
                      session.save(new Value("Another value"));
                    },
                    null))),
        Arguments.of(
            named(
                "save with entity name",
                new Parameter(
                    "save",
                    (Session session, Value val) -> {
                      session.save(Value.class.getName(), new Value("Another value"));
                    },
                    null))),
        Arguments.of(
            named(
                "saveOrUpdate save",
                new Parameter(
                    "saveOrUpdate",
                    (Session session, Value val) -> {
                      session.saveOrUpdate(new Value("Value"));
                    },
                    null))),
        Arguments.of(
            named(
                "saveOrUpdate save with entity name",
                new Parameter(
                    "saveOrUpdate",
                    (Session session, Value val) -> {
                      session.saveOrUpdate(Value.class.getName(), new Value("Value"));
                    },
                    null))),
        Arguments.of(
            named(
                "saveOrUpdate update with entity name",
                new Parameter(
                    "saveOrUpdate",
                    (Session session, Value val) -> {
                      val.setName("New name");
                      session.saveOrUpdate(Value.class.getName(), val);
                    },
                    null))),
        Arguments.of(
            named(
                "merge",
                new Parameter(
                    "merge",
                    (Session session, Value val) -> {
                      session.merge(new Value("merge me in"));
                    },
                    null))),
        Arguments.of(
            named(
                "merge with entity name",
                new Parameter(
                    "merge",
                    (Session session, Value val) -> {
                      session.merge(Value.class.getName(), new Value("merge me in"));
                    },
                    null))),
        Arguments.of(
            named(
                "persist",
                new Parameter(
                    "persist",
                    (Session session, Value val) -> {
                      session.persist(new Value("merge me in"));
                    },
                    null))),
        Arguments.of(
            named(
                "persist with entity name",
                new Parameter(
                    "persist",
                    (Session session, Value val) -> {
                      session.persist(Value.class.getName(), new Value("merge me in"));
                    },
                    null))),
        Arguments.of(
            named(
                "persist with null entity name",
                new Parameter(
                    "persist",
                    (Session session, Value val) -> {
                      session.persist(null, new Value("merge me in"));
                    },
                    null))),
        Arguments.of(
            named(
                "update (Session)",
                new Parameter(
                    "update",
                    (Session session, Value val) -> {
                      val.setName("New name");
                      session.update(val);
                    },
                    null))),
        Arguments.of(
            named(
                "update by entityName (Session)",
                new Parameter(
                    "update",
                    (Session session, Value val) -> {
                      val.setName("New name");
                      session.update(Value.class.getName(), val);
                    },
                    null))),
        Arguments.of(
            named(
                "delete (Session)",
                new Parameter(
                    "delete",
                    (Session session, Value val) -> {
                      session.delete(val);
                      prepopulated.remove(val);
                    },
                    null))),
        Arguments.of(
            named(
                "delete by entityName (Session)",
                new Parameter(
                    "delete",
                    (Session session, Value val) -> {
                      session.delete(Value.class.getName(), val);
                      prepopulated.remove(val);
                    },
                    null))));
  }

  @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation
  @ParameterizedTest
  @MethodSource("provideArgumentsStateQuery")
  void testAttachesStateToQueryCreated(Consumer<Session> queryBuilder) {

    testing.runWithSpan(
        "parent",
        () -> {
          Session session = sessionFactory.openSession();
          session.beginTransaction();
          queryBuilder.accept(session);
          session.getTransaction().commit();
          session.close();
        });

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("parent").hasKind(INTERNAL).hasNoParent(),
                span ->
                    span.hasName("SELECT Value")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            satisfies(
                                stringKey("hibernate.session_id"),
                                val -> val.isInstanceOf(String.class))),
                span ->
                    span.hasKind(CLIENT)
                        .hasParent(trace.getSpan(1))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                val -> val.isInstanceOf(String.class)),
                            satisfies(
                                DbIncubatingAttributes.DB_OPERATION,
                                val -> val.isInstanceOf(String.class)),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id"))))));
  }

  private static Stream<Arguments> provideArgumentsStateQuery() {
    return Stream.of(
        Arguments.of(
            named(
                "createQuery",
                ((Consumer<Session>) session -> session.createQuery("from Value").list())),
            Arguments.of(
                named(
                    "getNamedQuery",
                    ((Consumer<Session>)
                        session -> session.getNamedQuery("TestNamedQuery").list())),
                Arguments.of(
                    named(
                        "createSQLQuery",
                        (Consumer<Session>)
                            session -> session.createSQLQuery("SELECT * FROM Value").list())))));
  }

  @SuppressWarnings("deprecation") // TODO DbIncubatingAttributes.DB_CONNECTION_STRING deprecation
  @Test
  void testHibernateOverlappingSessions() {
    testing.runWithSpan(
        "overlapping Sessions",
        () -> {
          Session session1 = sessionFactory.openSession();
          session1.beginTransaction();

          StatelessSession session2 = sessionFactory.openStatelessSession();
          Session session3 = sessionFactory.openSession();

          Value value1 = new Value("Value 1");
          session1.save(value1);
          session2.insert(new Value("Value 2"));
          session3.save(new Value("Value 3"));
          session1.delete(value1);

          session2.close();
          session1.getTransaction().commit();
          session1.close();
          session3.close();
        });

    AtomicReference<String> sessionId1 = new AtomicReference<>();
    AtomicReference<String> sessionId2 = new AtomicReference<>();
    AtomicReference<String> sessionId3 = new AtomicReference<>();

    testing.waitAndAssertTraces(
        trace ->
            trace.hasSpansSatisfyingExactly(
                span -> span.hasName("overlapping Sessions"),
                span -> {
                  span.hasName("Session.save " + Value.class.getName())
                      .hasKind(INTERNAL)
                      .hasParent(trace.getSpan(0))
                      .hasAttributesSatisfyingExactly(
                          satisfies(
                              stringKey("hibernate.session_id"),
                              val -> val.isInstanceOf(String.class)));
                  sessionId1.set(
                      trace.getSpan(1).getAttributes().get(stringKey("hibernate.session_id")));
                },
                span -> {
                  span.hasName("Session.insert " + Value.class.getName())
                      .hasKind(INTERNAL)
                      .hasParent(trace.getSpan(0))
                      .hasAttributesSatisfyingExactly(
                          satisfies(
                              stringKey("hibernate.session_id"),
                              val -> val.isInstanceOf(String.class)));
                  sessionId2.set(
                      trace.getSpan(2).getAttributes().get(stringKey("hibernate.session_id")));
                },
                span ->
                    span.hasName("INSERT db1.Value")
                        .hasKind(CLIENT)
                        .hasParent(trace.getSpan(2))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                stringAssert -> stringAssert.startsWith("insert")),
                            equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")),
                span -> {
                  span.hasName("Session.save " + Value.class.getName())
                      .hasKind(INTERNAL)
                      .hasParent(trace.getSpan(0))
                      .hasAttributesSatisfyingExactly(
                          satisfies(
                              stringKey("hibernate.session_id"),
                              val -> val.isInstanceOf(String.class)));
                  sessionId3.set(
                      trace.getSpan(4).getAttributes().get(stringKey("hibernate.session_id")));
                },
                span ->
                    span.hasName("Session.delete " + Value.class.getName())
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            satisfies(
                                stringKey("hibernate.session_id"),
                                val -> val.isInstanceOf(String.class))),
                span ->
                    span.hasName("Transaction.commit")
                        .hasKind(INTERNAL)
                        .hasParent(trace.getSpan(0))
                        .hasAttributesSatisfyingExactly(
                            equalTo(
                                stringKey("hibernate.session_id"),
                                trace
                                    .getSpan(1)
                                    .getAttributes()
                                    .get(stringKey("hibernate.session_id")))),
                span ->
                    span.hasName("INSERT db1.Value")
                        .hasKind(CLIENT)
                        .hasParent(trace.getSpan(6))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                stringAssert -> stringAssert.startsWith("insert")),
                            equalTo(DbIncubatingAttributes.DB_OPERATION, "INSERT"),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value")),
                span ->
                    span.hasName("DELETE db1.Value")
                        .hasKind(CLIENT)
                        .hasParent(trace.getSpan(6))
                        .hasAttributesSatisfyingExactly(
                            equalTo(DbIncubatingAttributes.DB_SYSTEM, "h2"),
                            equalTo(DbIncubatingAttributes.DB_NAME, "db1"),
                            equalTo(DbIncubatingAttributes.DB_USER, "sa"),
                            equalTo(DbIncubatingAttributes.DB_CONNECTION_STRING, "h2:mem:"),
                            satisfies(
                                DbIncubatingAttributes.DB_STATEMENT,
                                stringAssert -> stringAssert.startsWith("delete")),
                            equalTo(DbIncubatingAttributes.DB_OPERATION, "DELETE"),
                            equalTo(DbIncubatingAttributes.DB_SQL_TABLE, "Value"))));

    assertThat(sessionId1.get()).isNotEqualTo(sessionId2.get());
    assertThat(sessionId1.get()).isNotEqualTo(sessionId3.get());
    assertThat(sessionId2.get()).isNotEqualTo(sessionId3.get());
  }

  private static class Parameter {
    public final String methodName;
    public final BiConsumer<Session, Value> sessionMethodTest;
    public final BiConsumer<StatelessSession, Value> statelessSessionMethodTest;

    public Parameter(
        String methodName,
        BiConsumer<Session, Value> sessionMethodTest,
        BiConsumer<StatelessSession, Value> statelessSessionMethodTest) {
      this.methodName = methodName;
      this.sessionMethodTest = sessionMethodTest;
      this.statelessSessionMethodTest = statelessSessionMethodTest;
    }
  }
}
