/*
 * 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.dev.jjs.test;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.impl.LoadingStrategyBase;
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;

/**
 * Tests runAsync server/network failure handling.
 *
 * This is skipped in dev mode because runAsync never fails in dev mode.
 */
@DoNotRunWith(Platform.Devel)
public class RunAsyncFailureTest extends GWTTestCase {

  abstract static class MyRunAsyncCallback implements RunAsyncCallback {
    private static int sToken = 0;
    private int attempt;
    private int expectedSuccessfulAttempt;
    private int token;
    
    public MyRunAsyncCallback(int attempt, int expectedSuccessfulAttempt) {
      this.attempt = attempt;
      this.expectedSuccessfulAttempt = expectedSuccessfulAttempt;
      token = sToken++;
    }
    
    public int getToken() {
      return token;
    }
    
    public boolean onSuccessHelper(String test) {
      int token = getToken();
      log("onSuccess: attempt = " + attempt + ", token = " + token);
      if (attempt == expectedSuccessfulAttempt) {
        return true;
      } else {
        // We don't really care about the test string, but we need to use it
        // somewhere so it doesn't get dead stripped out.  Each test passes
        // in a unique string so it ends up in it's fragment.
        fail(test + " - Succeeded on attempt: " + attempt + 
            " but should have succeeded on attempt: " + expectedSuccessfulAttempt);
      }
      return false;
    }
    
    protected void onFailureHelper(Throwable caught, Timer t) {
      // Fail the test if too many attempts have taken place.
      if (attempt > 5) {
        fail("Too many failures");
      }
      
      int token = getToken();
      log("onFailure: attempt = " + attempt + ", token = " + token
          + ", caught = " + caught);
      t.schedule(100);
    }

    private native void log(String message) /*-{
      // Enable this for testing on Safari/WebKit browsers
      // $wnd.console.log(message);
     }-*/;
  }

  private static final int RUNASYNC_TIMEOUT = 30000;
  
  private static int staticWrittenByAsync;

  @Override
  public String getModuleName() {
    return "com.google.gwt.dev.jjs.RunAsyncFailure";
  }
  
  /**
   * Some subclasses of this test use linkers that do not support retries, in
   * which case the expected number of manual retries before success will always
   * be 4.
   */
  protected boolean supportsRetries() {
    return true;
  }
  
  // Note that each tests needs a new subclass of MyRunAsyncCallback because
  // otherwise the runAsync code will cache the fragment and later tests will
  // always succeed on the first try.
  
  private void runAsync1(final int attempt, final int expectedSuccessfulAttempt) {
    GWT.runAsync(new MyRunAsyncCallback(attempt, expectedSuccessfulAttempt) {
      @Override
      public void onFailure(Throwable caught) {
        onFailureHelper(caught, new Timer() {
          @Override
          public void run() {
            runAsync1(attempt + 1, expectedSuccessfulAttempt);
          }
        });
      }

      @Override
      public void onSuccess() {
        if (onSuccessHelper("DOWNLOAD_FAILURE_TEST_1")) { finishTest(); }
      }
    });
  }
  
  private void runAsync2(final int attempt, final int expectedSuccessfulAttempt) {
    GWT.runAsync(new MyRunAsyncCallback(attempt, expectedSuccessfulAttempt) {
      @Override
      public void onFailure(Throwable caught) {
        onFailureHelper(caught, new Timer() {
          @Override
          public void run() {
            runAsync2(attempt + 1, expectedSuccessfulAttempt);
          }
        });
      }

      @Override
      public void onSuccess() {
        if (onSuccessHelper("DOWNLOAD_FAILURE_TEST_2")) { finishTest(); }
      }
    });
  }
  
  private void runAsync3(final int attempt, final int expectedSuccessfulAttempt) {
    GWT.runAsync(new MyRunAsyncCallback(attempt, expectedSuccessfulAttempt) {
      @Override
      public void onFailure(Throwable caught) {
        onFailureHelper(caught, new Timer() {
          @Override
          public void run() {
            runAsync3(attempt + 1, expectedSuccessfulAttempt);
          }
        });
      }

      @Override
      public void onSuccess() {
        if (onSuccessHelper("DOWNLOAD_FAILURE_TEST_3")) { finishTest(); }
      }
    });
  }
  
  private void runAsync4() {
    GWT.runAsync(new RunAsyncCallback() {
      @Override
      public void onFailure(Throwable caught) {
        // This call should fail since no retries are done if the code downloads
        // successfully, but fails to install.
        finishTest();
      }
      @Override
      public void onSuccess() {
        // Use the string "INSTALL_FAILURE_TEST" so we can identify this
        // fragment on the server.  In the fail message is good enough.
        fail("INSTALL_FAILURE_TEST - Code should have failed to install!");
      }
    });
  }

  private void runAsync5() {
    GWT.runAsync(new RunAsyncCallback() {
      @Override
      public void onFailure(Throwable caught) {
        staticWrittenByAsync++;
      }
      @Override
      public void onSuccess() {
        // Use the string "INSTALL_FAILURE_TEST" so we can identify this
        // fragment on the server.  In the fail message is good enough.
        fail("INSTALL_FAILURE_TEST_2 - Code should have failed to install!");
      }
    });
  }
  
  /**
   * Test the basic functionality of retrying runAsync until is succeeds.
   * A Timer is used to avoid nesting GWT.runAsync calls.
   */
  public void testHttpFailureRetries() {
    delayTestFinish(RUNASYNC_TIMEOUT);
    // Default is 3, but we set it explicitly, since other tests may run first
    LoadingStrategyBase.MAX_AUTO_RETRY_COUNT = 2;
    // In RunAsyncFailureServlet, the 5th time is the charm, but the code
    // by default retries 3 times every time we call runAsync, so this
    // should succeed on the second runAsync call, which is attempt #1.
    runAsync1(0, supportsRetries() ? 1 : 4);
  }
  
  public void testHttpFailureRetries2() {
    delayTestFinish(RUNASYNC_TIMEOUT);
    LoadingStrategyBase.MAX_AUTO_RETRY_COUNT = 0;
    // In RunAsyncFailureServlet, the 5th time is the charm, so if we do not
    // let the code do any retries, we'll need to retry 4 times before succeeding
    runAsync2(0, 4);
  }
  
  public void testBuiltInRetries() {
    delayTestFinish(RUNASYNC_TIMEOUT);
    LoadingStrategyBase.MAX_AUTO_RETRY_COUNT = 4;
    // In RunAsyncFailureServlet, the 5th time is the charm, so retrying 4 times
    // should be enough to get it on the first try.
    runAsync3(0, supportsRetries() ? 0 : 4);
  }
  
  public void testDownloadSuccessButInstallFailure() {
    delayTestFinish(RUNASYNC_TIMEOUT);
    LoadingStrategyBase.MAX_AUTO_RETRY_COUNT = 3;
    runAsync4();
  }

  public void testDownloadSuccessButInstallFailureStillRunsAsync() {
    delayTestFinish(RUNASYNC_TIMEOUT);
    LoadingStrategyBase.MAX_AUTO_RETRY_COUNT = 3;
    staticWrittenByAsync = 0;

    assertRunAsyncIsAsync();

    // Give it little bit more time to loaded and try runAsync again
    Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
      @Override public boolean execute() {
        if (staticWrittenByAsync == 0) {
          return true;
        }

        // Code is loaded, let's assert it still runs async
        assertRunAsyncIsAsync();

        finishTest();
        return false;
      }
    }, 100);
  }

  private void assertRunAsyncIsAsync() {
    final int lastValue = staticWrittenByAsync;
    runAsync5();
    assertEquals(lastValue, staticWrittenByAsync);
  }
}
