// Copyright 2016 Google Inc. 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 com.google.api.ads.adwords.awreporting.model.persistence.sql;

import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;

import com.google.api.ads.adwords.awreporting.model.entities.AccountPerformanceReport;
import com.google.api.ads.adwords.awreporting.model.entities.DateRangeAndType;
import com.google.api.ads.adwords.awreporting.model.persistence.EntityPersister;
import java.util.Arrays;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.exception.LockAcquisitionException;
import org.joda.time.LocalDate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

/**
 * Test case for the {@code SqlReportEntitiesPersister} class.
 *
 * <p>Note: Tests in this class do not use assertThrows as this is not available without updating to
 * JUnit5.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
    classes = {SqlReportEntitiesPersisterTest.Config.class},
    loader = AnnotationConfigContextLoader.class)
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class SqlReportEntitiesPersisterTest {

  @Autowired
  @Qualifier("sqlEntitiesPersister")
  private EntityPersister sqlEntitiesPersister;

  @Autowired private Session session;

  @Autowired private SessionFactory sessionFactory;

  @Autowired private SqlReportEntitiesPersister.Config config;

  @Mock private Criteria criteria;

  private InOrder sequence;

  @Configuration
  @ComponentScan(
      basePackageClasses = {
        com.google.api.ads.adwords.awreporting.model.persistence.sql.SqlReportEntitiesPersister
            .class
      })
  @EnableRetry
  public static class Config {

    @Bean
    public SessionFactory sessionFactory() {
      return Mockito.mock(SessionFactory.class);
    }

    @Bean
    public Session session() {
      return Mockito.mock(Session.class);
    }

    @Bean
    SqlReportEntitiesPersister.Config config() {
      return new SqlReportEntitiesPersister.Config();
    }
  }

  @Before
  public void setUp() {
    Mockito.when(sessionFactory.getCurrentSession()).thenReturn(session);
    Mockito.when(session.createCriteria(AccountPerformanceReport.class)).thenReturn(criteria);

    sequence = Mockito.inOrder(session);
  }

  @Test
  public void persistReportEntities_savesSingleInstance() {
    AccountPerformanceReport report = generateReport(1);

    sqlEntitiesPersister.persistReportEntities(Arrays.asList(report));

    sequence.verify(session).saveOrUpdate(report);
  }

  @Test
  public void persistReportEntities_savesInBatches() {
    config.setBatchSize(2);

    List<AccountPerformanceReport> reports =
        Arrays.asList(generateReport(1), generateReport(2), generateReport(3));

    sqlEntitiesPersister.persistReportEntities(reports);

    sequence.verify(session).saveOrUpdate(reports.get(0));
    sequence.verify(session).saveOrUpdate(reports.get(1));
    sequence.verify(session).flush();
    sequence.verify(session).clear();

    sequence.verify(session).saveOrUpdate(reports.get(2));
  }

  @Test
  public void persistReportEntities_abortsOnSqlException() {
    Mockito.doThrow(new HibernateException("")).when(session).saveOrUpdate(any());

    try {
      sqlEntitiesPersister.persistReportEntities(Arrays.asList(generateReport(1)));
      fail();
    } catch (HibernateException ex) {
      // expected
    }
  }

  @Test
  public void persistReportEntities_retriesAfterDeadlockThenFails() {
    AccountPerformanceReport report = generateReport(1);

    Mockito.doThrow(new LockAcquisitionException("", null)).when(session).saveOrUpdate(any());

    try {
      sqlEntitiesPersister.persistReportEntities(Arrays.asList(report));
      fail();
    } catch (LockAcquisitionException ex) {
      // expected
    }

    sequence.verify(session, Mockito.times(20)).saveOrUpdate(report);
  }

  @Test
  public void persistReportEntities_retriesAfterDeadlockThenSucceeds() {
    AccountPerformanceReport report = generateReport(1);

    Mockito.doThrow(new LockAcquisitionException("", null))
        .doNothing()
        .when(session)
        .saveOrUpdate(any());

    sqlEntitiesPersister.persistReportEntities(Arrays.asList(report));

    sequence.verify(session, Mockito.times(2)).saveOrUpdate(report);
  }

  @Test
  public void persistReportEntities_setsManualFlushModeAndResetsIt() {
      AccountPerformanceReport report = generateReport(1);

      Mockito.when(session.getHibernateFlushMode()).thenReturn(FlushMode.AUTO);

      sqlEntitiesPersister.persistReportEntities(Arrays.asList(report));

      sequence.verify(session).getHibernateFlushMode();
      sequence.verify(session).setHibernateFlushMode(FlushMode.MANUAL);
      sequence.verify(session).setHibernateFlushMode(FlushMode.AUTO);
  }

  private AccountPerformanceReport generateReport(long accountId) {
    AccountPerformanceReport report = new AccountPerformanceReport(123L, accountId);
    report.setAccountDescriptiveName("testAccount");

    LocalDate today = LocalDate.now();
    DateRangeAndType dateRange = DateRangeAndType.fromValues(today, today, null);
    report.setDateRangeType(dateRange.getTypeStr());
    report.setStartDate(dateRange.getStartDateStr());
    report.setEndDate(dateRange.getEndDateStr());
    return report;
  }
}