package com.avaje.tests.cache;

import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;

import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.cache.ServerCache;
import com.avaje.ebean.cache.ServerCacheManager;
import com.avaje.ebeaninternal.server.cache.CachedManyIds;
import com.avaje.tests.model.basic.Contact;
import com.avaje.tests.model.basic.Country;
import com.avaje.tests.model.basic.Customer;
import com.avaje.tests.model.basic.OCachedBean;
import com.avaje.tests.model.basic.ResetBasicData;

public class TestCacheCollectionIds extends BaseTestCase {

  ServerCacheManager cacheManager = Ebean.getServerCacheManager();
  
  @Test
  public void test() {

    ResetBasicData.reset();
    awaitL2Cache();

    ServerCache custCache = cacheManager.getBeanCache(Customer.class);
    ServerCache contactCache = cacheManager.getBeanCache(Contact.class);
    ServerCache custManyIdsCache = cacheManager.getCollectionIdsCache(Customer.class, "contacts");

    // cacheManager.setCaching(Customer.class, true);
    // cacheManager.setCaching(Contact.class, true);

    custCache.clear();
    custManyIdsCache.clear();

    List<Customer> list = Ebean.find(Customer.class).setAutoTune(false).setLoadBeanCache(true)
        .order().asc("id").findList();

    Assert.assertTrue(list.size() > 1);
    // Assert.assertEquals(list.size(),
    // custCache.getStatistics(false).getSize());

    Customer customer = list.get(0);
    List<Contact> contacts = customer.getContacts();
    // Assert.assertEquals(0, custManyIdsCache.getStatistics(false).getSize());
    contacts.size();
    Assert.assertTrue(contacts.size() > 1);
    // Assert.assertEquals(1, custManyIdsCache.getStatistics(false).getSize());
    // Assert.assertEquals(0,
    // custManyIdsCache.getStatistics(false).getHitCount());

    fetchCustomer(customer.getId());
    // Assert.assertEquals(1,
    // custManyIdsCache.getStatistics(false).getHitCount());

    fetchCustomer(customer.getId());
    // Assert.assertEquals(2,
    // custManyIdsCache.getStatistics(false).getHitCount());

    int currentNumContacts = fetchCustomer(customer.getId());
    // Assert.assertEquals(3,
    // custManyIdsCache.getStatistics(false).getHitCount());

    Contact newContact = ResetBasicData.createContact("Check", "CollIds");
    newContact.setCustomer(customer);

    Ebean.save(newContact);
    awaitL2Cache();

    int currentNumContacts2 = fetchCustomer(customer.getId());
    Assert.assertEquals(currentNumContacts + 1, currentNumContacts2);

  }

  private int fetchCustomer(Integer id) {

    Customer customer2 = Ebean.find(Customer.class, id);

    List<Contact> contacts2 = customer2.getContacts();
    contacts2.size();
    for (Contact contact : contacts2) {
      contact.getFirstName();
      contact.getEmail();
    }
    return contacts2.size();
  }

  /**
   * When updating a ManyToMany relations also the collection cache must be updated.
   */
  @Test
  public void testUpdatingCollectionCacheForManyToManyRelations() {
    // arrange
    ResetBasicData.reset();

    OCachedBean cachedBean = new OCachedBean();
    cachedBean.setName("hello");
    cachedBean.getCountries().add(Ebean.find(Country.class, "NZ"));
    cachedBean.getCountries().add(Ebean.find(Country.class, "AU"));

    Ebean.save(cachedBean);

    // used to just load the cache - trigger loading 
    OCachedBean dummyToLoad = Ebean.find(OCachedBean.class, cachedBean.getId());
    dummyToLoad.getCountries().size();
    
    ServerCache cachedBeanCountriesCache = cacheManager.getCollectionIdsCache(OCachedBean.class, "countries");
    CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(cachedBean.getId());
    
    // confirm the starting data and cache entry
    Assert.assertEquals(2, dummyToLoad.getCountries().size());
    Assert.assertEquals(2, cachedManyIds.getIdList().size());

    
    // act
    OCachedBean loadedBean = Ebean.find(OCachedBean.class, cachedBean.getId());
    loadedBean.getCountries().clear();
    loadedBean.getCountries().add(Ebean.find(Country.class, "AU"));

    Ebean.save(loadedBean);
    awaitL2Cache();

    // Get the data to assert/check against
    OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId());
    cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(result.getId());

    // assert that data and cache both show correct data
    Assert.assertEquals(1, result.getCountries().size());
    Assert.assertEquals(1, cachedManyIds.getIdList().size());
    Assert.assertFalse(cachedManyIds.getIdList().contains("NZ"));
    Assert.assertTrue(cachedManyIds.getIdList().contains("AU"));
  }

  
  /**
   * When updating a ManyToMany relations also the collection cache must be updated.
   * Alternate to above test where in this case the bean is dirty - loadedBean.setName("goodbye");.
   */
  @Test
  public void testUpdatingCollectionCacheForManyToManyRelationsWithUpdatedBean() {
    // arrange
    ResetBasicData.reset();

    OCachedBean cachedBean = new OCachedBean();
    cachedBean.setName("hello");
    cachedBean.getCountries().add(Ebean.find(Country.class, "NZ"));
    cachedBean.getCountries().add(Ebean.find(Country.class, "AU"));

    Ebean.save(cachedBean);

    // used to just load the cache - trigger loading 
    OCachedBean dummyToLoad = Ebean.find(OCachedBean.class, cachedBean.getId());
    dummyToLoad.getCountries().size();
    
    ServerCache cachedBeanCountriesCache = cacheManager.getCollectionIdsCache(OCachedBean.class, "countries");
    CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(cachedBean.getId());
    
    // confirm the starting data and cache entry
    Assert.assertEquals(2, dummyToLoad.getCountries().size());
    Assert.assertEquals(2, cachedManyIds.getIdList().size());

    
    // act - this time update the name property so the bean is dirty
    OCachedBean loadedBean = Ebean.find(OCachedBean.class, cachedBean.getId());
    loadedBean.setName("goodbye");
    loadedBean.getCountries().clear();
    loadedBean.getCountries().add(Ebean.find(Country.class, "AU"));

    Ebean.save(loadedBean);
    awaitL2Cache();

    // Get the data to assert/check against
    OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId());
    cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(result.getId());

    // assert that data and cache both show correct data
    Assert.assertEquals(1, result.getCountries().size());
    Assert.assertEquals(1, cachedManyIds.getIdList().size());
    Assert.assertFalse(cachedManyIds.getIdList().contains("NZ"));
    Assert.assertTrue(cachedManyIds.getIdList().contains("AU"));
  }

  /**
   * When updating a ManyToMany relations also the collection cache must be updated.
   */
  @Test
  public void testUpdatingCollectionCacheForManyToManyRelationsWithinStatelessUpdate() {
    // arrange
    ResetBasicData.reset();

    OCachedBean cachedBean = new OCachedBean();
    cachedBean.setName("cachedBeanTest");
    cachedBean.getCountries().add(Ebean.find(Country.class, "NZ"));
    cachedBean.getCountries().add(Ebean.find(Country.class, "AU"));

    Ebean.save(cachedBean);

    // clear the cache
    ServerCache cachedBeanCountriesCache = cacheManager.getCollectionIdsCache(OCachedBean.class, "countries");
    cachedBeanCountriesCache.clear();
    Assert.assertEquals(0, cachedBeanCountriesCache.size());

    // load the cache
    OCachedBean dummyLoad = Ebean.find(OCachedBean.class, cachedBean.getId());
    List<Country> dummyCountries = dummyLoad.getCountries();
    Assert.assertEquals(2, dummyCountries.size());

    // assert that the cache contains the expected entry
    Assert.assertEquals("countries cache now loaded with 1 entry", 1, cachedBeanCountriesCache.size());
    CachedManyIds dummyEntry = (CachedManyIds) cachedBeanCountriesCache.get(dummyLoad.getId());
    Assert.assertNotNull(dummyEntry);
    Assert.assertEquals("2 ids in the entry", 2, dummyEntry.getIdList().size());
    Assert.assertTrue(dummyEntry.getIdList().contains("NZ"));
    Assert.assertTrue(dummyEntry.getIdList().contains("AU"));
    
    
    // act - this should invalidate our cache entry
    OCachedBean update = new OCachedBean();
    update.setId(cachedBean.getId());
    update.setName("modified");
    update.getCountries().add(Ebean.find(Country.class, "AU"));

    Ebean.update(update);
    awaitL2Cache();

    Assert.assertEquals("countries entry still there (but updated)", 1, cachedBeanCountriesCache.size());

    CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(update.getId());

    // assert cache updated
    Assert.assertEquals(1, cachedManyIds.getIdList().size());
    Assert.assertFalse(cachedManyIds.getIdList().contains("NZ"));
    Assert.assertTrue(cachedManyIds.getIdList().contains("AU"));
    
    // assert countries good
    OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId());
    Assert.assertEquals(1, result.getCountries().size());

  }

  @Test
  public void testUpdating_noChange() throws InterruptedException {

    ResetBasicData.reset();

    Ebean.beginTransaction();
    try {

      OCachedBean cachedBean = new OCachedBean();
      cachedBean.setName("helloForUpdate");
      Ebean.save(cachedBean);

      cachedBean = Ebean.find(OCachedBean.class, cachedBean.getId());

      cachedBean.setName("mod");
      ArrayList<Country> list = new ArrayList<Country>();
      list.add(Ebean.find(Country.class, "NZ"));
      cachedBean.setCountries(list);
      Ebean.save(cachedBean);


      cachedBean.setName("mod2");
      Ebean.save(cachedBean);

      Ebean.commitTransaction();
    } finally {
      Ebean.endTransaction();
    }


    Thread.sleep(2000);
  }
}
