/**
 * Copyright (c) 2014-present, Facebook, Inc.
 * All rights reserved.
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.testing;

import javax.annotation.Nullable;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import android.graphics.Bitmap;
import android.test.ActivityInstrumentationTestCase2;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;

/**
 * Base class for instrumentation tests that runs React based react application in UI mode
 */
public abstract class ReactAppInstrumentationTestCase extends
    ActivityInstrumentationTestCase2<ReactAppTestActivity> implements IdleWaiter {

  public ReactAppInstrumentationTestCase() {
    super(ReactAppTestActivity.class);
  }

  @Override
  protected void setUp() throws Exception {
    super.setUp();

    final ReactAppTestActivity activity = getActivity();
    try {
      runTestOnUiThread(new Runnable() {
        @Override
        public void run() {
          activity.loadApp(
              getReactApplicationKeyUnderTest(),
              createReactInstanceSpecForTest(),
              getEnableDevSupport());
        }
      });
    } catch (Throwable t) {
      throw new Exception("Unable to load react app", t);
    }
    waitForBridgeAndUIIdle();
    assertTrue("Layout never occurred!", activity.waitForLayout(5000));
    waitForBridgeAndUIIdle();
  }

  @Override
  protected void tearDown() throws Exception {
    ReactAppTestActivity activity = getActivity();
    super.tearDown();
    activity.waitForDestroy(5000);
  }

  public ViewGroup getRootView() {
    return (ViewGroup) getActivity().getRootView();
  }

  /**
   * This method isn't safe since it doesn't factor in layout-only view removal. Use
   * {@link #getViewByTestId(String)} instead.
   */
  @Deprecated
  public <T extends View> T getViewAtPath(int... path) {
    return ReactTestHelper.getViewAtPath((ViewGroup) getRootView().getParent(), path);
  }

  public <T extends View> T getViewByTestId(String testID) {
    return (T) ReactTestHelper
        .getViewWithReactTestId((ViewGroup) getRootView().getParent(), testID);
  }

  public SingleTouchGestureGenerator createGestureGenerator() {
    return new SingleTouchGestureGenerator(getRootView(), this);
  }

  public void waitForBridgeAndUIIdle() {
    getActivity().waitForBridgeAndUIIdle();
  }

  public void waitForBridgeAndUIIdle(long timeoutMs) {
    getActivity().waitForBridgeAndUIIdle(timeoutMs);
  }

  protected Bitmap getScreenshot() {
    // Wait for the UI to settle. If the UI is doing animations, this may be unsafe!
    getInstrumentation().waitForIdleSync();

    final CountDownLatch latch = new CountDownLatch(1);
    final BitmapHolder bitmapHolder = new BitmapHolder();
    final Runnable getScreenshotRunnable = new Runnable() {

      private static final int MAX_TRIES = 1000;
      // This is the constant used in the support library for APIs that don't have Choreographer
      private static final int FRAME_DELAY_MS = 10;

      private int mNumRuns = 0;

      @Override
      public void run() {
        mNumRuns++;
        ReactAppTestActivity activity = getActivity();
        if (!activity.isScreenshotReady()) {
          if (mNumRuns > MAX_TRIES) {
            throw new RuntimeException(
                "Waited " + MAX_TRIES + " frames to get screenshot but it's still not ready!");
          }
          activity.postDelayed(this, FRAME_DELAY_MS);
          return;
        }

        bitmapHolder.bitmap = getActivity().getCurrentScreenshot();
        latch.countDown();
      }
    };


    getActivity().runOnUiThread(getScreenshotRunnable);
    try {
      if (!latch.await(5000, TimeUnit.MILLISECONDS)) {
        throw new RuntimeException("Timed out waiting for screenshot runnable to run!");
      }
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    return Assertions.assertNotNull(bitmapHolder.bitmap);
  }

  /**
   * Implement this method to provide application key to be launched. List of available
   * application is located in TestBundle.js file
   */
  protected abstract String getReactApplicationKeyUnderTest();

  protected boolean getEnableDevSupport() {
    return false;
  }

  /**
   * Override this method to provide extra native modules to be loaded before the app starts
   */
  protected ReactInstanceSpecForTest createReactInstanceSpecForTest() {
    return new ReactInstanceSpecForTest();
  }

  protected ReactContext getReactContext() {
    return getActivity().getReactContext();
  }

  /**
   * Helper class to pass the bitmap between execution scopes in {@link #getScreenshot()}.
   */
  private static class BitmapHolder {

    public @Nullable volatile Bitmap bitmap;
  }
}
