/*
 * Copyright 2010 Google Inc.
 * 
 * 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.web.bindery.requestfactory.gwt.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.web.bindery.event.shared.EventBus;
import com.google.web.bindery.event.shared.SimpleEventBus;
import com.google.web.bindery.requestfactory.shared.EntityProxy;
import com.google.web.bindery.requestfactory.shared.ExtraTypes;
import com.google.web.bindery.requestfactory.shared.ProxyFor;
import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.RequestFactory;
import com.google.web.bindery.requestfactory.shared.Service;
import com.google.web.bindery.requestfactory.shared.SimpleFooProxy;
import com.google.web.bindery.requestfactory.shared.TestRequestFactory;

import java.util.Arrays;
import java.util.List;

/**
 * Tests behavior of RequestFactory when using polymorphic Request return types
 * and other non-trivial type hierarchies.
 */
public class RequestFactoryPolymorphicTest extends GWTTestCase {

  /**
   * Mandatory javadoc.
   */
  public static class A {
    private static int idCount = 0;

    public static A findA(int id) {
      A toReturn = new A();
      toReturn.id = id;
      return toReturn;
    }

    private String a = "a";

    protected int id = idCount++;

    private A nextA;

    public String getA() {
      return a;
    }

    public int getId() {
      return id;
    }

    public A getNextA() {
      if (nextA == null) {
        nextA = new A();
      }
      return nextA;
    }

    public int getVersion() {
      return 0;
    }

    public void setA(String value) {
      a = value;
    }
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(A.class)
  public interface AProxy extends EntityProxy, HasA {
    AProxy getNextA();

    // Mix in getA() from HasA
    void setA(String value);
  }

  /**
   * Mandatory javadoc.
   */
  public static class ASub extends A {
  }

  /**
   * Mandatory javadoc.
   */
  public static class B extends A {
    public static B findB(int id) {
      B toReturn = new B();
      toReturn.id = id;
      return toReturn;
    }

    private String b = "b";

    public String getB() {
      return b;
    }

    public void setB(String value) {
      b = value;
    }
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(B.class)
  public interface B1Proxy extends AProxy {
    String getB();

    void setB(String value);
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(B.class)
  public interface B2Proxy extends AProxy {
    String getB();

    void setB(String value);
  }

  /**
   * Mandatory javadoc.
   */
  public static class BSub extends B {
  }

  /**
   * Mandatory javadoc.
   */
  public static class C extends B {
    public static C findC(int id) {
      C toReturn = new C();
      toReturn.id = id;
      return toReturn;
    }

    private String c = "c";

    private C nextC;

    public String getC() {
      return c;
    }

    public C getNextC() {
      if (nextC == null) {
        nextC = new C();
      }

      return nextC;
    }

    public void setC(String value) {
      c = value;
    }
  }
  /**
   * Mandatory javadoc.
   */
  @ProxyFor(C.class)
  public interface C1Proxy extends B1Proxy {
    String getC();

    C1Proxy getNextC();

    void setC(String value);
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(C.class)
  public interface C2Proxy extends B2Proxy {
    String getC();

    void setC(String value);
  }

  /**
   * Mandatory javadoc.
   */
  public static class CSub extends C {
  }

  /**
   * Mandatory javadoc.
   */
  public static class D extends A {
    public static D findD(int id) {
      D toReturn = new D();
      toReturn.id = id;
      return toReturn;
    }

    private String d = "d";

    public String getD() {
      return d;
    }

    public void setD(String value) {
      d = value;
    }
  }

  /**
   * Mandatory javadoc.
   */
  @ExtraTypes(D2Proxy.class)
  @ProxyFor(D.class)
  public interface D1Proxy extends AProxy {
    String getD();

    void setD(String value);
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(D.class)
  public interface D2Proxy extends EntityProxy {
    String getD();

    void setD(String value);
  }

  /**
   * This class should not be referenced except as a superclass of
   * {@link MoreDerivedProxy}.
   */
  @ProxyFor(D.class)
  public interface D3Proxy extends EntityProxy {
    String getD();

    void setD(String value);
  }

  /**
   * Mandatory javadoc.
   */
  public static class DSub extends D {
  }

  /**
   * Mandatory javadoc.
   */
  public interface HasA {
    String getA();
  }

  /**
   * Mandatory javadoc.
   */
  public static class Impl {
    public static A AasA() {
      return new A();
    }

    public static A BasA() {
      return new B();
    }

    public static B BasB() {
      return new B();
    }

    public static A CasA() {
      return new C();
    }

    public static B CasB() {
      return new C();
    }

    public static String checkA(Object obj) {
      return A.class.equals(obj.getClass()) && "A".equals(((A) obj).getA()) ? "" : "checkA";
    }

    public static String checkB(Object obj) {
      return B.class.equals(obj.getClass()) && "B".equals(((B) obj).getB()) ? "" : "checkB";
    }

    public static String checkC(Object obj) {
      return C.class.equals(obj.getClass()) && "C".equals(((C) obj).getC()) ? "" : "checkC";
    }

    public static String checkD(Object obj) {
      return D.class.equals(obj.getClass()) && "D".equals(((D) obj).getD()) ? "" : "checkD";
    }

    public static String checkList(List<Object> list) {
      if (list.size() != 4) {
        return "size";
      }
      String temp;
      temp = checkA(list.get(0));
      if (!temp.isEmpty()) {
        return temp;
      }
      temp = checkB(list.get(1));
      if (!temp.isEmpty()) {
        return temp;
      }
      temp = checkC(list.get(2));
      if (!temp.isEmpty()) {
        return temp;
      }
      temp = checkD(list.get(3));
      if (!temp.isEmpty()) {
        return temp;
      }
      return "";
    }

    public static String checkW(Object obj) {
      return W.class.equals(obj.getClass()) && "W".equals(((W) obj).getW()) ? "" : "checkW";
    }

    public static String checkZ(Object obj) {
      return Z.class.equals(obj.getClass()) && "Z".equals(((Z) obj).getZ()) ? "" : "checkZ";
    }

    public static A DasA() {
      return new D();
    }

    public static D DasD() {
      return new D();
    }

    public static List<A> testCollection() {
      return Arrays.asList(new A(), new B(), new C(), new D());
    }

    public static List<A> testCollectionSub() {
      return Arrays.asList(new ASub(), new BSub(), new CSub(), new DSub());
    }

    public static W W() {
      return new W();
    }

    public static W W2() {
      return new W();
    }

    public static Z Z() {
      return new Z();
    }

    public static Z Z2() {
      return new Z();
    }
  }

  /**
   * Check diamond inheritance.
   */
  @ProxyFor(D.class)
  public interface MoreDerivedProxy extends AProxy, D1Proxy, D2Proxy, D3Proxy {
  }

  /**
   * The W and Z types are used with the WZProxy and ZWProxy to demonstrate
   * proxy interface inheritance even when their proxy-for types aren't related.
   */
  public static class W {
    private static int idCount;

    public static W findW(int id) {
      W toReturn = new W();
      toReturn.id = id;
      return toReturn;
    }

    private int id = idCount++;

    private String w = "w";

    private String z = "z";

    public int getId() {
      return id;
    }

    public int getVersion() {
      return 0;
    }

    public String getW() {
      return w;
    }

    public String getZ() {
      return z;
    }

    public void setW(String w) {
      this.w = w;
    }

    public void setZ(String z) {
      this.z = z;
    }
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(W.class)
  public interface WProxy extends EntityProxy {
    String getW();

    void setW(String w);
  }
  /**
   * Mandatory javadoc.
   */
  @ProxyFor(W.class)
  public interface WZProxy extends ZProxy {
    String getW();

    void setW(String w);
  }
  /**
   * @see W
   */
  public static class Z extends A {
    private static int idCount;

    public static Z findZ(int id) {
      Z toReturn = new Z();
      toReturn.id = id;
      return toReturn;
    }

    private int id = idCount++;

    private String w = "w";

    private String z = "z";

    @Override
    public int getId() {
      return id;
    }

    @Override
    public int getVersion() {
      return 0;
    }

    public String getW() {
      return w;
    }

    public String getZ() {
      return z;
    }

    public void setW(String w) {
      this.w = w;
    }

    public void setZ(String z) {
      this.z = z;
    }
  }
  /**
   * Mandatory javadoc.
   */
  @ProxyFor(Z.class)
  public interface ZProxy extends EntityProxy {
    String getZ();

    void setZ(String z);
  }

  /**
   * Mandatory javadoc.
   */
  @ProxyFor(Z.class)
  public interface ZWProxy extends WProxy {
    String getZ();

    void setZ(String z);
  }

  /**
   * Mandatory javadoc.
   */
  @ExtraTypes(B2Proxy.class)
  protected interface Factory extends RequestFactory {
    Context ctx();
  }

  /**
   * Verifies that the received proxy's proxy type is exactly {@code clazz} and
   * checks the values of the properties.
   */
  static class CastAndCheckReceiver extends Receiver<EntityProxy> {
    public static CastAndCheckReceiver of(Class<?> clazz) {
      return new CastAndCheckReceiver(clazz);
    }

    private final Class<?> clazz;

    public CastAndCheckReceiver(Class<?> clazz) {
      this.clazz = clazz;
    }

    @Override
    public void onSuccess(EntityProxy response) {
      assertNotNull(response);
      assertEquals(clazz, response.stableId().getProxyClass());
      if (response instanceof HasA) {
        assertEquals("a", ((HasA) response).getA());
      }
      if (response instanceof B1Proxy) {
        assertEquals("b", ((B1Proxy) response).getB());
      }
      if (response instanceof B2Proxy) {
        assertEquals("b", ((B2Proxy) response).getB());
      }
      if (response instanceof C1Proxy) {
        assertEquals("c", ((C1Proxy) response).getC());
      }
      if (response instanceof C2Proxy) {
        assertEquals("c", ((C2Proxy) response).getC());
      }
      if (response instanceof D1Proxy) {
        assertEquals("d", ((D1Proxy) response).getD());
      }
      if (response instanceof D2Proxy) {
        assertEquals("d", ((D2Proxy) response).getD());
      }
    }
  }

  @ExtraTypes({C1Proxy.class, C2Proxy.class, MoreDerivedProxy.class})
  @Service(Impl.class)
  interface Context extends RequestContext {
    Request<AProxy> AasA();

    Request<AProxy> BasA();

    Request<B1Proxy> BasB();

    Request<AProxy> CasA();

    Request<B1Proxy> CasB();

    Request<String> checkA(EntityProxy proxy);

    Request<String> checkB(EntityProxy proxy);

    Request<String> checkC(EntityProxy proxy);

    Request<String> checkD(EntityProxy proxy);

    Request<String> checkList(List<EntityProxy> list);

    Request<String> checkW(EntityProxy proxy);

    Request<String> checkZ(EntityProxy proxy);

    Request<AProxy> DasA();

    Request<D1Proxy> DasD();

    Request<List<AProxy>> testCollection();

    Request<List<AProxy>> testCollectionSub();

    Request<WProxy> W();

    Request<WZProxy> W2();

    Request<ZProxy> Z();

    Request<ZWProxy> Z2();
  }

  /**
   * Checks that the incoming list is {@code [ A, B, C, D ]}.
   */
  static class ListChecker extends Receiver<List<AProxy>> {
    @Override
    public void onSuccess(List<AProxy> response) {
      new CastAndCheckReceiver(AProxy.class).onSuccess(response.get(0));
      new CastAndCheckReceiver(B1Proxy.class).onSuccess(response.get(1));
      new CastAndCheckReceiver(C1Proxy.class).onSuccess(response.get(2));
      new CastAndCheckReceiver(MoreDerivedProxy.class).onSuccess(response.get(3));
    }
  }

  private final Receiver<String> checkReceiver = new Receiver<String>() {

    @Override
    public void onSuccess(String response) {
      assertEquals("", response);
    }
  };

  private static final int TEST_DELAY = 5000;

  protected Factory factory;

  @Override
  public String getModuleName() {
    return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite";
  }

  public void testChain() {
    delayTestFinish(TEST_DELAY);
    Context ctx = factory.ctx();
    ctx.CasA().with("nextA.nextA").fire(new Receiver<AProxy>() {
      @Override
      public void onSuccess(AProxy response) {
        new CastAndCheckReceiver(AProxy.class).onSuccess(response.getNextA().getNextA());
        assertNull(response.getNextA().getNextA().getNextA());
        assertNull(((C1Proxy) response).getNextC());
        finishTest();
      }
    });
  }

  public void testChainWithExtras() {
    delayTestFinish(TEST_DELAY);
    Context ctx = factory.ctx();
    ctx.CasA().with("nextC.nextC").fire(new Receiver<AProxy>() {
      @Override
      public void onSuccess(AProxy response) {
        assertNull(response.getNextA());
        C1Proxy cast = (C1Proxy) response;
        new CastAndCheckReceiver(C1Proxy.class).onSuccess(cast.getNextC().getNextC());
        assertNull(cast.getNextC().getNextC().getNextC());
        finishTest();
      }
    });
  }

  public void testChainWithWildcards() {
    delayTestFinish(TEST_DELAY);
    Context ctx = factory.ctx();
    ctx.CasA().with("*.*").fire(new Receiver<AProxy>() {
      @Override
      public void onSuccess(AProxy response) {
        new CastAndCheckReceiver(AProxy.class).onSuccess(response.getNextA().getNextA());
        C1Proxy cast = (C1Proxy) response;
        new CastAndCheckReceiver(C1Proxy.class).onSuccess(cast.getNextC().getNextC());
        assertNull(cast.getNextC().getNextC().getNextC());
        finishTest();
      }
    });
  }

  public void testCreation() {
    delayTestFinish(TEST_DELAY);
    Context ctx = factory.ctx();
    checkA(ctx, AProxy.class);
    checkB(ctx, B1Proxy.class);
    checkB(ctx, B2Proxy.class);
    checkC(ctx, C1Proxy.class);
    checkC(ctx, C2Proxy.class);
    checkD(ctx, D1Proxy.class);
    checkD(ctx, D2Proxy.class);
    // D3Proxy is a proxy supertype, assignable to BaseProxy and has @ProxyFor
    checkD(ctx, D3Proxy.class);
    checkD(ctx, MoreDerivedProxy.class);
    checkW(ctx, WProxy.class);
    checkW(ctx, WZProxy.class);
    checkZ(ctx, ZProxy.class);
    checkZ(ctx, ZWProxy.class);
    ctx.fire(new Receiver<Void>() {
      @Override
      public void onSuccess(Void response) {
        finishTest();
      }
    });
  }

  /**
   * Ensure heterogeneous collections work.
   */
  public void testCreationList() {
    Context ctx = factory.ctx();
    ctx.checkList(
        Arrays.asList(create(ctx, AProxy.class), create(ctx, B2Proxy.class), create(ctx,
            C2Proxy.class), create(ctx, D2Proxy.class))).to(checkReceiver);
    ctx.fire(new Receiver<Void>() {
      @Override
      public void onSuccess(Void response) {
        finishTest();
      }
    });
  }

  public void testGenericRequest() {
    TestRequestFactory rf = GWT.create(TestRequestFactory.class);
    EventBus eventBus = new SimpleEventBus();
    rf.initialize(eventBus);
    SimpleFooProxy simpleFoo = rf.testGenericRequest().create(SimpleFooProxy.class);
    assertNull(simpleFoo.getUserName());
  }

  public void testRetrieval() {
    delayTestFinish(TEST_DELAY);
    Context ctx = factory.ctx();
    ctx.AasA().to(CastAndCheckReceiver.of(AProxy.class));
    ctx.BasA().to(CastAndCheckReceiver.of(B1Proxy.class));
    ctx.BasB().to(CastAndCheckReceiver.of(B1Proxy.class));
    ctx.CasA().to(CastAndCheckReceiver.of(C1Proxy.class));
    ctx.CasB().to(CastAndCheckReceiver.of(C1Proxy.class));
    ctx.DasA().to(CastAndCheckReceiver.of(MoreDerivedProxy.class));
    ctx.DasD().to(CastAndCheckReceiver.of(MoreDerivedProxy.class));
    ctx.W().to(CastAndCheckReceiver.of(WProxy.class));
    ctx.W2().to(CastAndCheckReceiver.of(WZProxy.class));
    ctx.Z().to(CastAndCheckReceiver.of(ZProxy.class));
    ctx.Z2().to(CastAndCheckReceiver.of(ZWProxy.class));
    ctx.fire(new Receiver<Void>() {
      @Override
      public void onSuccess(Void response) {
        finishTest();
      }
    });
  }

  public void testRetrievalCollection() {
    delayTestFinish(TEST_DELAY);
    Context ctx = factory.ctx();
    ctx.testCollection().to(new ListChecker());
    ctx.testCollectionSub().to(new ListChecker());
    ctx.fire(new Receiver<Void>() {
      @Override
      public void onSuccess(Void response) {
        finishTest();
      }
    });
  }

  protected Factory createFactory() {
    Factory f = GWT.create(Factory.class);
    f.initialize(new SimpleEventBus());
    return f;
  }

  @Override
  protected void gwtSetUp() throws Exception {
    factory = createFactory();
  }

  private void checkA(Context ctx, Class<? extends AProxy> clazz) {
    ctx.checkA(create(ctx, clazz)).to(checkReceiver);
  }

  private void checkB(Context ctx, Class<? extends EntityProxy> clazz) {
    ctx.checkB(create(ctx, clazz)).to(checkReceiver);
  }

  private void checkC(Context ctx, Class<? extends EntityProxy> clazz) {
    ctx.checkC(create(ctx, clazz)).to(checkReceiver);
  }

  private void checkD(Context ctx, Class<? extends EntityProxy> clazz) {
    ctx.checkD(create(ctx, clazz)).to(checkReceiver);
  }

  private void checkW(Context ctx, Class<? extends EntityProxy> clazz) {
    ctx.checkW(create(ctx, clazz)).to(checkReceiver);
  }

  private void checkZ(Context ctx, Class<? extends EntityProxy> clazz) {
    ctx.checkZ(create(ctx, clazz)).to(checkReceiver);
  }

  private <T extends EntityProxy> T create(Context ctx, Class<T> clazz) {
    T obj = ctx.create(clazz);
    if (obj instanceof AProxy) {
      ((AProxy) obj).setA("A");
    }
    if (obj instanceof B1Proxy) {
      ((B1Proxy) obj).setB("B");
    }
    if (obj instanceof B2Proxy) {
      ((B2Proxy) obj).setB("B");
    }
    if (obj instanceof C1Proxy) {
      ((C1Proxy) obj).setC("C");
    }
    if (obj instanceof C2Proxy) {
      ((C2Proxy) obj).setC("C");
    }
    if (obj instanceof D1Proxy) {
      ((D1Proxy) obj).setD("D");
    }
    if (obj instanceof D2Proxy) {
      ((D2Proxy) obj).setD("D");
    }
    if (obj instanceof D3Proxy) {
      ((D3Proxy) obj).setD("D");
    }
    if (obj instanceof WProxy) {
      ((WProxy) obj).setW("W");
    }
    if (obj instanceof ZProxy) {
      ((ZProxy) obj).setZ("Z");
    }
    if (obj instanceof WZProxy) {
      ((WZProxy) obj).setW("W");
    }
    if (obj instanceof ZWProxy) {
      ((ZWProxy) obj).setZ("Z");
    }
    return obj;
  }
}
