/*
 * Copyright 2009 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.gwt.jsonp.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * Tests for {@link JsonpRequest}.
 */
public class JsonpRequestTest extends GWTTestCase {

  /**
   * Checks that an error is received.
   */
  private class AssertFailureCallback<T> implements AsyncCallback<T> {
    private String expectedMessage;
    private int id;

    public AssertFailureCallback(String expectedMessage) {
      id = currentTestId;
      this.expectedMessage = expectedMessage;
    }

    @Override
    public void onFailure(Throwable throwable) {
      if (id == currentTestId) {
        assertEquals(expectedMessage, throwable.getMessage());
        finishTest();
      }
    }

    @Override
    public void onSuccess(T value) {
      if (id == currentTestId) {
        fail();
      }
    }
  }

  /**
   * Checks that the received value is as expected.
   */
  private class AssertSuccessCallback<T> implements AsyncCallback<T> {
    private final T expectedValue;
    private final int id;
    private final Counter counter;

    private AssertSuccessCallback(T expectedValue) {
      this(expectedValue, null);
    }

    private AssertSuccessCallback(T expectedValue, Counter counter) {
      this.id = currentTestId;
      this.counter = counter;
      this.expectedValue = expectedValue;
    }

    @Override
    public void onFailure(Throwable throwable) {
      if (id == currentTestId) {
        fail();
      }
    }

    @Override
    public void onSuccess(T value) {
      if (id == currentTestId) {
        assertEquals(expectedValue, value);
        if (counter != null) {
          counter.count();
        } else {
          finishTest();
        }
      }
    }
  }

  /**
   * Checks that a timeout happens.
   */
  private class AssertTimeoutExceptionCallback<T> implements AsyncCallback<T> {
    @Override
    public void onFailure(Throwable throwable) {
      assertTrue(throwable instanceof TimeoutException);
      finishTest();
    }

    @Override
    public void onSuccess(T value) {
      fail();
    }
  }

  /**
   * A class that counts results and calls finishTest when the right number
   * have been received.
   */
  private class Counter {
  
    private int count;

    public Counter(int count) {
      this.count = count;
    }
  
    public void count() {
      if (--count < 0) {
        fail("Too many results");
      }
      if (count == 0) {
        finishTest();
      }
    }
  }

  /**
   * The maximum amount of time to wait for a response in milliseconds.
   */
  private static final int RESPONSE_DELAY = 2500;

  /**
   * The ID of the current test.
   */
  protected static int currentTestId;

  private static String echo(String value) {
    return GWT.getModuleBaseURL() + "echo?action=SUCCESS&value=" + value;
  }

  private static String echoDelayed(String value) {
    return echoDelayed(value, 500);
  }

  private static String echoDelayed(String value, long delayMs) {
    return GWT.getModuleBaseURL() + "echo?action=SUCCESS&delay=" + delayMs
       + "&value=" + value;
  }

  private static String echoFailure(String error) {
    return GWT.getModuleBaseURL() + "echo?action=FAILURE&error=" + error;
  }

  private static String echoTimeout() {
    return GWT.getModuleBaseURL() + "echo?action=TIMEOUT";
  }

  private JsonpRequestBuilder jsonp;

  @Override
  public String getModuleName() {
    return "com.google.gwt.jsonp.JsonpTest";
  }

  public void testBooleanFalse() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestBoolean(echo("false"), new AssertSuccessCallback<Boolean>(
        Boolean.FALSE));
  }

  public void testBooleanTrue() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestBoolean(echo("true"), new AssertSuccessCallback<Boolean>(
        Boolean.TRUE));
  }

  /**
   * Fails in devmode with HtmlUnit, JS "null" exception.
   * <p>
   * Call occurs through postponedActions in HtmlUnit that execute
   * synchronously. Should be async. Need to file HtmlUnitBug.
   */
  @DoNotRunWith(Platform.HtmlUnitBug)
  public void testCancel() {
    delayTestFinish(2000);
    // setup a server request that will delay for 500ms
    JsonpRequest<String> req = jsonp.requestString(echoDelayed("'A'", 500),
        new AssertFailureCallback<String>("A"));
    // cancel it before it comes back
    req.cancel();
    // wait 1s to make sure we don't get a callback
    new Timer() {
      @Override
      public void run() {
        finishTest();
      }
    }.schedule(1000);
  }

  public void testDelay() {
    delayTestFinish(RESPONSE_DELAY);
    JsonpRequest<String> req = jsonp.requestString(echoDelayed("'A'"),
        new AssertSuccessCallback<String>("A"));
  }

  public void testDouble() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestDouble(echo("123.456"), new AssertSuccessCallback<Double>(
        123.456));
  }

  public void testFailureCallback() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.setFailureCallbackParam("failureCallback");
    jsonp.requestString(echoFailure("ERROR"),
        new AssertFailureCallback<String>("ERROR"));
  }

  public void testIds() {
    delayTestFinish(RESPONSE_DELAY);
    Counter counter = new Counter(2);
    JsonpRequest<String> reqA = jsonp.requestString(echo("'A'"),
        new AssertSuccessCallback<String>("A", counter));
    JsonpRequest<String> reqB = jsonp.requestString(echo("'B'"),
        new AssertSuccessCallback<String>("B", counter));
    // WARNING: knows the current structure of IDs
    int idA = Integer.parseInt(reqA.getCallbackId().substring(1));
    int idB = Integer.parseInt(reqB.getCallbackId().substring(1));
    assertEquals("Unexpected ID sequence", idA + 1, idB);
  }

  public void testInteger() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestInteger(echo("123"), new AssertSuccessCallback<Integer>(123));
  }

  /**
   * Tests that if no failure callback is defined, the servlet receives well
   * only a 'callback' parameter, and sends back the error to it.
   */
  public void testNoFailureCallback() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.setFailureCallbackParam(null);
    jsonp.requestString(echoFailure("ERROR"),
        new AssertSuccessCallback<String>("ERROR"));
  }

  public void testNullBoolean() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestBoolean(echo("null"), new AssertSuccessCallback<Boolean>(null));
  }

  public void testNullDouble() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestDouble(echo("null"), new AssertSuccessCallback<Double>(null));
  }

  public void testNullInteger() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestInteger(echo("null"), new AssertSuccessCallback<Integer>(null));
  }

  public void testNullString() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestString(echo("null"), new AssertSuccessCallback<String>(null));
  }

  public void testOverlapped() {
    delayTestFinish(RESPONSE_DELAY);
    Counter counter = new Counter(3);
    JsonpRequest<String> reqA = jsonp.requestString(echoDelayed("'A'", 750),
        new AssertSuccessCallback<String>("A", counter));
    JsonpRequest<String> reqB = jsonp.requestString(echoDelayed("'B'", 500),
        new AssertSuccessCallback<String>("B", counter));
    JsonpRequest<String> reqC = jsonp.requestString(echo("'C'"),
        new AssertSuccessCallback<String>("C", counter));
  }

  public void testPredeterminedIds() {
    delayTestFinish(RESPONSE_DELAY);
    String PREDETERMINED = "pred";
    jsonp.setPredeterminedId(PREDETERMINED);
    JsonpRequest<String> reqA = jsonp.requestString(echo("'A'"),
        new AssertSuccessCallback<String>("A"));
    String idA = reqA.getCallbackId().substring(1);
    assertEquals("Unexpected ID sequence", PREDETERMINED, idA);
    jsonp.setPredeterminedId(null);
  }

  public void testString() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.requestString(echo("'Hello'"), new AssertSuccessCallback<String>(
        "Hello"));
  }

  public void testTimeout() {
    delayTestFinish(jsonp.getTimeout() + 1000);
    jsonp.requestString(echoTimeout(),
        new AssertTimeoutExceptionCallback<String>());
  }

  public void testVoid() {
    delayTestFinish(RESPONSE_DELAY);
    jsonp.send(echo(null), new AssertSuccessCallback<Void>(null));
  }

  @Override
  protected void gwtSetUp() throws Exception {
    jsonp = new JsonpRequestBuilder();
    jsonp.setTimeout(RESPONSE_DELAY + 500);
  }
  
  @Override
  protected void gwtTearDown() throws Exception {
    ++currentTestId;
  }
}
