package com.avaje.tests.batchload;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

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

import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.Expr;
import com.avaje.ebean.FetchConfig;
import com.avaje.ebean.Transaction;
import com.avaje.tests.basic.MyTestDataSourcePoolListener;
import com.avaje.tests.model.basic.Address;
import com.avaje.tests.model.basic.Contact;
import com.avaje.tests.model.basic.Customer;
import com.avaje.tests.model.basic.Order;
import com.avaje.tests.model.basic.ResetBasicData;

public class TestBasicLazy extends BaseTestCase {

  @Test
  public void testQueries() {

    ResetBasicData.reset();

    Order order = Ebean.find(Order.class).select("totalAmount").setMaxRows(1).order("id")
        .findUnique();

    Assert.assertNotNull(order);

    Customer customer = order.getCustomer();
    Assert.assertNotNull(customer);
    Assert.assertNotNull(customer.getName());

    Address address = customer.getBillingAddress();
    Assert.assertNotNull(address);
    Assert.assertNotNull(address.getCity());
  }

  public void test_N1N() {
    ResetBasicData.reset();

    // safety check to see if our customer we are going to use for the test has
    // some contacts
    Customer c = Ebean.find(Customer.class).setId(1).findUnique();
    Assert.assertNotNull(c.getContacts());
    Assert.assertTrue("no contacts on test customer 1", !c.getContacts().isEmpty());

    // start transaction so we have a "long running" persistence context
    Transaction tx = Ebean.beginTransaction();
    try {
      List<Order> order = Ebean.find(Order.class).where(Expr.eq("customer.id", 1)).findList();

      Assert.assertNotNull(order);
      Assert.assertTrue(!order.isEmpty());

      Customer customer = order.get(0).getCustomer();
      Assert.assertNotNull(customer);
      Assert.assertEquals(1, customer.getId().intValue());

      // this should lazily fetch the contacts
      List<Contact> contacts = customer.getContacts();

      Assert.assertNotNull(contacts);
      Assert.assertTrue("contacts not lazily fetched", !contacts.isEmpty());
    } finally {
      tx.commit();
    }
  }

  public void testRaceCondition_Simple() throws Throwable {
    ResetBasicData.reset();

    Order order = Ebean.find(Order.class).select("totalAmount").setMaxRows(1).order("id")
        .findUnique();

    Assert.assertNotNull(order);

    final Customer customer = order.getCustomer();
    Assert.assertNotNull(customer);

    Assert.assertTrue(Ebean.getBeanState(customer).isReference());

    final Throwable throwables[] = new Throwable[2];
    Thread t1 = new Thread() {
      @Override
      public void run() {
        try {
          Assert.assertNotNull(customer.getName());
        } catch (Throwable e) {
          throwables[0] = e;
        }
      }
    };

    Thread t2 = new Thread() {
      @Override
      public void run() {
        try {
          Assert.assertNotNull(customer.getName());
        } catch (Throwable e) {
          throwables[1] = e;
        }
      }
    };

    try {
      // prepare for race condition
      MyTestDataSourcePoolListener.SLEEP_AFTER_BORROW = 2000;

      t1.start();
      t2.start();
      t1.join();
      t2.join();
    } finally {
      MyTestDataSourcePoolListener.SLEEP_AFTER_BORROW = 0;
    }

    Assert.assertFalse(Ebean.getBeanState(customer).isReference());

    if (throwables[0] != null) {
      throw throwables[0];
    }
    if (throwables[1] != null) {
      throw throwables[1];
    }
  }

  private final AtomicBoolean mutex = new AtomicBoolean(false);
  private List<Order> orders;

  private List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<Throwable>());

  private class FetchThread extends Thread {
    private int index;

    private FetchThread(ThreadGroup tg, int index) {
      super(tg, "fetcher-" + index);
      this.index = index;
    }

    @Override
    public void run() {
      synchronized (mutex) {
        System.err.println("** WAIT **");
        try {
          while (!mutex.get()) {
            mutex.wait(100);
          }
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }

      try {
        System.err.println("** DO LAZY FETCH **");
        orders.get(index).getCustomer().getName();
      } catch (Throwable e) {
        exceptions.add(e);
      }
    }
  }

  public void testRaceCondition_Complex() throws Throwable {
    ResetBasicData.reset();

    ThreadGroup tg = new ThreadGroup("fetchers");
    new FetchThread(tg, 0).start();
    new FetchThread(tg, 1).start();
    new FetchThread(tg, 2).start();
    new FetchThread(tg, 3).start();
    new FetchThread(tg, 0).start();
    new FetchThread(tg, 1).start();
    new FetchThread(tg, 2).start();
    new FetchThread(tg, 3).start();

    orders = Ebean.find(Order.class).fetch("customer", new FetchConfig().lazy(100)).findList();
    Assert.assertTrue(orders.size() >= 4);

    try {
      MyTestDataSourcePoolListener.SLEEP_AFTER_BORROW = 2000;

      synchronized (mutex) {
        mutex.set(true);
        mutex.notifyAll();
      }

      while (tg.activeCount() > 0) {
        Thread.sleep(100);
      }
    } finally {
      MyTestDataSourcePoolListener.SLEEP_AFTER_BORROW = 0;
    }

    if (!exceptions.isEmpty()) {
      System.err.println("Seen Exceptions:");
      for (Throwable exception : exceptions) {
        exception.printStackTrace();
      }
      Assert.fail();
    }
  }
}