/*
 * Copyright 2008 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.junit.server;

import com.google.gwt.core.server.StackTraceDeobfuscator;
import com.google.gwt.junit.JUnitFatalLaunchException;
import com.google.gwt.junit.JUnitMessageQueue;
import com.google.gwt.junit.JUnitMessageQueue.ClientInfoExt;
import com.google.gwt.junit.JUnitShell;
import com.google.gwt.junit.client.TimeoutException;
import com.google.gwt.junit.client.impl.JUnitHost;
import com.google.gwt.junit.client.impl.JUnitResult;
import com.google.gwt.junit.linker.JUnitSymbolMapsLinker;
import com.google.gwt.logging.shared.RemoteLoggingService;
import com.google.gwt.user.client.rpc.InvocationException;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * An RPC servlet that serves as a proxy to JUnitTestShell. Enables
 * communication between the unit test code running in a browser and the real
 * test process.
 */
public class JUnitHostImpl extends RemoteServiceServlet implements JUnitHost, RemoteLoggingService {

  /**
   * A hook into GWTUnitTestShell, the underlying unit test process.
   */
  private static JUnitMessageQueue sHost = null;

  /**
   * A maximum timeout to wait for the test system to respond with the next
   * test. The test system should respond nearly instantly if there are further
   * tests to run, unless the tests have not yet been compiled.
   */
  private static final int TIME_TO_WAIT_FOR_TESTNAME = 300000;

  /**
   * Monotonic increase counter to create unique client session ids.
   */
  private static final AtomicInteger uniqueSessionId = new AtomicInteger();

  /**
   * Tries to grab the GWTUnitTestShell sHost environment to communicate with
   * the real test process.
   */
  private static synchronized JUnitMessageQueue getHost() {
    if (sHost == null) {
      sHost = JUnitShell.getMessageQueue();
      if (sHost == null) {
        throw new InvocationException(
            "Unable to find JUnitShell; is this servlet running under GWTTestCase?");
      }
    }
    return sHost;
  }

  private StackTraceDeobfuscator deobfuscator;

  public InitialResponse getTestBlock(int blockIndex, ClientInfo clientInfo)
      throws TimeoutException {
    ClientInfoExt clientInfoExt;
    HttpServletRequest request = getThreadLocalRequest();
    if (clientInfo.getSessionId() < 0) {
      clientInfoExt = createNewClientInfo(request);
    } else {
      clientInfoExt = createClientInfo(clientInfo, request);
    }
    TestBlock initialTestBlock = getHost().getTestBlock(clientInfoExt,
        blockIndex, TIME_TO_WAIT_FOR_TESTNAME);
    // Send back the updated session id.
    return new InitialResponse(clientInfoExt.getSessionId(), initialTestBlock);
  }

  public TestBlock reportResultsAndGetTestBlock(
      HashMap<TestInfo, JUnitResult> results, int testBlock,
      ClientInfo clientInfo) throws TimeoutException {
    for (JUnitResult result : results.values()) {
      initResult(getThreadLocalRequest(), result);
    }
    JUnitMessageQueue host = getHost();
    ClientInfoExt clientInfoExt = createClientInfo(clientInfo,
        getThreadLocalRequest());
    host.reportResults(clientInfoExt, results);
    return host.getTestBlock(clientInfoExt, testBlock,
        TIME_TO_WAIT_FOR_TESTNAME);
  }

  @Override
  protected void service(HttpServletRequest request,
      HttpServletResponse response) throws ServletException, IOException {
    String requestURI = request.getRequestURI();
    if (requestURI.endsWith("/junithost/error")) {
      String msg = RPCServletUtils.readContentAsGwtRpc(request);
      System.err.println("Warning: " + msg);
    } else if (requestURI.endsWith("/junithost/error/fatal")) {
      String msg = RPCServletUtils.readContentAsGwtRpc(request);
      System.err.println("Fatal error: " + msg);
      System.exit(1);
    } else if (requestURI.endsWith("/junithost/error/launch")) {
      String requestPayload = RPCServletUtils.readContentAsGwtRpc(request);
      JUnitResult result = new JUnitResult();
      initResult(request, result);
      result.setException(new JUnitFatalLaunchException(requestPayload));
      getHost().reportFatalLaunch(createNewClientInfo(request), result);
    } else {
      super.service(request, response);
    }
  }

  private ClientInfoExt createClientInfo(ClientInfo clientInfo, HttpServletRequest request) {
    assert (clientInfo.getSessionId() >= 0);
    return new ClientInfoExt(clientInfo.getSessionId(), getClientDesc(request));
  }

  private ClientInfoExt createNewClientInfo(HttpServletRequest request) {
    return new ClientInfoExt(createSessionId(), getClientDesc(request));
  }

  private int createSessionId() {
    return uniqueSessionId.getAndIncrement();
  }

  /**
   * Returns a client description for the current request.
   */
  private String getClientDesc(HttpServletRequest request) {
    String machine = request.getRemoteHost();
    String agent = request.getHeader("User-Agent");
    return machine + " / " + agent;
  }

  private void initResult(HttpServletRequest request, JUnitResult result) {
    result.setAgent(request.getHeader("User-Agent"));
    result.setHost(request.getRemoteHost());
    Throwable throwable = result.getException();
    if (throwable != null) {
      deobfuscateStackTrace(throwable);
    }
  }

  @Override
  public String logOnServer(LogRecord lr) {
    lr.setMessage("<BROWSER> " + lr.getMessage());
    if (lr.getThrown() != null) {
      deobfuscateStackTrace(lr.getThrown());
    }
    Logger.getLogger(lr.getLoggerName()).log(lr);
    return null;
  }

  private void deobfuscateStackTrace(Throwable throwable) {
    try {
      getDeobfuscator().deobfuscateStackTrace(throwable, getPermutationStrongName());
    } catch (IOException e) {
      System.err.println("Unable to deobfuscate a stack trace due to an error:");
      e.printStackTrace();
    }
  }

  private StackTraceDeobfuscator getDeobfuscator() throws IOException {
    if (deobfuscator == null) {
      String path = getRequestModuleBasePath();
      if (!path.endsWith("/")) {
        path += "/";
      }
      path += JUnitSymbolMapsLinker.SYMBOL_MAP_DIR;
      deobfuscator = StackTraceDeobfuscator.fromUrl(getServletContext().getResource(path));
    }
    return deobfuscator;
  }
}
