// Copyright 2018 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.persistence.transaction;

import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import google.registry.persistence.PersistenceXmlUtility;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.Entity;
import org.junit.rules.ExternalResource;

/**
 * Checks for JPA {@link Entity entities} that are declared in persistence.xml but not persisted in
 * integration tests.
 */
public class JpaEntityCoverage extends ExternalResource {

  // TODO(weiminyu): update this set when entities written to Cloud SQL and tests are added.
  private static final ImmutableSet<String> IGNORE_ENTITIES =
      ImmutableSet.of(
          "DelegationSignerData", "DesignatedContact", "GracePeriod", "RegistrarContact");

  private static final ImmutableSet<Class> ALL_JPA_ENTITIES =
      PersistenceXmlUtility.getManagedClasses().stream()
          .filter(e -> !IGNORE_ENTITIES.contains(e.getSimpleName()))
          .filter(e -> e.getAnnotation(Entity.class) != null)
          .collect(ImmutableSet.toImmutableSet());
  private static final Set<Class> allCoveredJpaEntities = Sets.newHashSet();
  // Map of test class name to boolean flag indicating if it tests any JPA entities.
  private static final Map<String, Boolean> testsJpaEntities = Maps.newHashMap();

  // Provides class name of the test being executed.
  private final TestCaseWatcher watcher;

  public JpaEntityCoverage(TestCaseWatcher watcher) {
    this.watcher = watcher;
  }

  @Override
  public void before() {
    testsJpaEntities.putIfAbsent(watcher.getTestClass(), false);
  }

  @Override
  public void after() {
    ALL_JPA_ENTITIES.stream()
        .filter(JpaEntityCoverage::isPersisted)
        .forEach(
            entity -> {
              allCoveredJpaEntities.add(entity);
              testsJpaEntities.put(watcher.getTestClass(), true);
            });
  }

  public static void init() {
    allCoveredJpaEntities.clear();
    testsJpaEntities.clear();
  }

  public static Set<Class> getUncoveredEntities() {
    return Sets.difference(ALL_JPA_ENTITIES, allCoveredJpaEntities);
  }

  public static ImmutableList<String> getIrrelevantTestClasses() {
    return testsJpaEntities.entrySet().stream()
        .filter(e -> !e.getValue())
        .map(Map.Entry::getKey)
        .collect(ImmutableList.toImmutableList());
  }

  /**
   * Checks if at least one instance of {@code entityType} has been persisted during the test, and
   * if yes, reads one of them back. If the read is successful, there is no incompatibility between
   * server and schema regarding type.
   *
   * @return true if an instance of {@code entityType} is found in the database and can be read
   */
  private static boolean isPersisted(Class entityType) {
    List result =
        jpaTm()
            .transact(
                () ->
                    jpaTm()
                        .getEntityManager()
                        .createQuery(
                            String.format("SELECT e FROM %s e", getJpaEntityName(entityType)),
                            entityType)
                        .setMaxResults(1)
                        .getResultList());
    return !result.isEmpty() && entityType.isInstance(result.get(0));
  }

  private static String getJpaEntityName(Class entityType) {
    Entity entityAnnotation = (Entity) entityType.getAnnotation(Entity.class);
    checkState(
        entityAnnotation != null, "Unexpected non-entity type %s", entityType.getSimpleName());
    return Strings.isNullOrEmpty(entityAnnotation.name())
        ? entityType.getSimpleName()
        : entityAnnotation.name();
  }
}

@@ -1079,24 +1079,54 @@
 XmlUtility;%0A
+import java.sql.SQLException;%0A
 import java.
@@ -3710,24 +3710,36 @@
 tityType) %7B%0A
+    try %7B%0A  
     List res
@@ -3744,32 +3744,34 @@
 esult =%0A        
+  
 jpaTm()%0A        
@@ -3774,16 +3774,18 @@
         
+ 
+ 
 .transac
@@ -3807,14 +3807,18 @@
     
+  
 () -%3E%0A
+  
     
@@ -3861,24 +3861,26 @@
             
+  
 .getEntityMa
@@ -3911,16 +3911,18 @@
         
+  
 .createQ
@@ -3927,16 +3927,18 @@
 eQuery(%0A
+  
         
@@ -4048,24 +4048,26 @@
             
+  
 entityType)%0A
@@ -4066,16 +4066,18 @@
 tyType)%0A
+  
         
@@ -4130,24 +4130,26 @@
             
+  
 .getResultLi
@@ -4151,24 +4151,26 @@
 ultList());%0A
+  
     return !
@@ -4223,24 +4223,611 @@
 lt.get(0));%0A
+    %7D catch (RuntimeException e) %7B%0A      // See if this was caused by a %22relation does not exist%22 error.%0A      Throwable cause = e;%0A      while ((cause = cause.getCause()) != null) %7B%0A        if (cause instanceof SQLException%0A            && cause.getMessage().matches(%22(?s).*relation .* does not exist.*%22)) %7B%0A          throw new RuntimeException(%0A              %22SQLException occurred.  If you've updated the set of entities, make sure you've %22%0A                  + %22also updated the golden schema.  See db/README.md for details.%22,%0A              e);%0A        %7D%0A      %7D%0A%0A      throw e;%0A    %7D%0A
   %7D%0A%0A  priva

Improve error information in coverage test. (#537)