// Copyright (c) 2017 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "tests/cefclient/browser/server_test.h"

#include <algorithm>
#include <string>

#include "include/base/cef_bind.h"
#include "include/base/cef_weak_ptr.h"
#include "include/cef_parser.h"
#include "include/cef_server.h"
#include "include/wrapper/cef_closure_task.h"
#include "tests/shared/browser/resource_util.h"

namespace client {
namespace server_test {

namespace {

// Application-specific error codes.
const int kMessageFormatError = 1;
const int kActionStateError = 1;

// JSON dictionary keys.
const char kActionKey[] = "action";
const char kResultKey[] = "result";
const char kPortKey[] = "port";
const char kStatusKey[] = "status";
const char kMessageKey[] = "message";

// Required URL for cefQuery execution.
const char kTestUrl[] = "http://tests/server";

// Server default values.
const char kServerAddress[] = "127.0.0.1";
const int kServerPortDefault = 8099;
const int kServerBacklog = 10;
const char kDefaultPath[] = "websocket.html";

// Handles the HTTP/WebSocket server.
class ServerHandler : public CefServerHandler {
 public:
  typedef base::Callback<void(bool /* success */)> CompleteCallback;

  ServerHandler() {}

  // |complete_callback| will be executed on the UI thread after completion.
  void StartServer(int port, const CompleteCallback& complete_callback) {
    CEF_REQUIRE_UI_THREAD();
    DCHECK(!server_);
    DCHECK(port >= 1025 && port <= 65535);
    port_ = port;
    complete_callback_ = complete_callback;
    CefServer::CreateServer(kServerAddress, port, kServerBacklog, this);
  }

  // |complete_callback| will be executed on the UI thread after completion.
  void StopServer(const CompleteCallback& complete_callback) {
    CEF_REQUIRE_UI_THREAD();
    DCHECK(server_);
    complete_callback_ = complete_callback;
    server_->Shutdown();
  }

  // CefServerHandler methods are called on the server thread.

  void OnServerCreated(CefRefPtr<CefServer> server) override {
    DCHECK(!server_);
    server_ = server;
    RunCompleteCallback(server->IsRunning());
  }

  void OnServerDestroyed(CefRefPtr<CefServer> server) override {
    DCHECK(server_);
    server_ = nullptr;
    RunCompleteCallback(true);
  }

  void OnClientConnected(CefRefPtr<CefServer> server,
                         int connection_id) override {}

  void OnClientDisconnected(CefRefPtr<CefServer> server,
                            int connection_id) override {}

  void OnHttpRequest(CefRefPtr<CefServer> server,
                     int connection_id,
                     const CefString& client_address,
                     CefRefPtr<CefRequest> request) override {
    // Parse the request URL and retrieve the path without leading slash.
    CefURLParts url_parts;
    CefParseURL(request->GetURL(), url_parts);
    std::string path = CefString(&url_parts.path);
    if (!path.empty() && path[0] == '/')
      path = path.substr(1);

    if (path.empty())
      path = kDefaultPath;

    std::string mime_type;
    const size_t sep = path.find_last_of(".");
    if (sep != std::string::npos) {
      // Determine the mime type based on the extension.
      mime_type = CefGetMimeType(path.substr(sep + 1));
    } else {
      // No extension. Assume html.
      path += ".html";
    }
    if (mime_type.empty())
      mime_type = "text/html";

    CefRefPtr<CefStreamReader> stream;
    CefResponse::HeaderMap extra_headers;

    if (path == "request.html") {
      // Return the request contents.
      stream = test_runner::GetDumpResponse(request, extra_headers);
    }

    if (!stream) {
      // Load any resource supported by cefclient.
      stream = GetBinaryResourceReader(path.c_str());
    }

    if (stream) {
      SendHttpResponseStream(server, connection_id, mime_type, stream,
                             extra_headers);
    } else {
      server->SendHttp404Response(connection_id);
    }
  }

  void OnWebSocketRequest(CefRefPtr<CefServer> server,
                          int connection_id,
                          const CefString& client_address,
                          CefRefPtr<CefRequest> request,
                          CefRefPtr<CefCallback> callback) override {
    // Always accept WebSocket connections.
    callback->Continue();
  }

  void OnWebSocketConnected(CefRefPtr<CefServer> server,
                            int connection_id) override {}

  void OnWebSocketMessage(CefRefPtr<CefServer> server,
                          int connection_id,
                          const void* data,
                          size_t data_size) override {
    // Echo the reverse of the message.
    std::string message(static_cast<const char*>(data), data_size);
    std::reverse(message.begin(), message.end());

    server->SendWebSocketMessage(connection_id, message.data(), message.size());
  }

  int port() const { return port_; }

 private:
  void RunCompleteCallback(bool success) {
    if (!CefCurrentlyOn(TID_UI)) {
      CefPostTask(TID_UI, base::Bind(&ServerHandler::RunCompleteCallback, this,
                                     success));
      return;
    }

    if (!complete_callback_.is_null()) {
      complete_callback_.Run(success);
      complete_callback_.Reset();
    }
  }

  static void SendHttpResponseStream(CefRefPtr<CefServer> server,
                                     int connection_id,
                                     const std::string& mime_type,
                                     CefRefPtr<CefStreamReader> stream,
                                     CefResponse::HeaderMap extra_headers) {
    // Determine the stream size.
    stream->Seek(0, SEEK_END);
    int64 content_length = stream->Tell();
    stream->Seek(0, SEEK_SET);

    // Send response headers.
    server->SendHttpResponse(connection_id, 200, mime_type, content_length,
                             extra_headers);

    // Send stream contents.
    char buffer[8192];
    size_t read;
    do {
      read = stream->Read(buffer, 1, sizeof(buffer));
      if (read > 0)
        server->SendRawData(connection_id, buffer, read);
    } while (!stream->Eof() && read != 0);

    // Close the connection.
    server->CloseConnection(connection_id);
  }

  CefRefPtr<CefServer> server_;

  // The below members are only accessed on the UI thread.
  int port_;
  CompleteCallback complete_callback_;

  IMPLEMENT_REFCOUNTING(ServerHandler);
  DISALLOW_COPY_AND_ASSIGN(ServerHandler);
};

// Handle messages in the browser process.
class Handler : public CefMessageRouterBrowserSide::Handler {
 public:
  Handler() : weak_ptr_factory_(this) {}

  virtual ~Handler() {
    if (handler_) {
      handler_->StopServer(ServerHandler::CompleteCallback());
      handler_ = nullptr;
    }
  }

  // Called due to cefQuery execution in server.html.
  virtual bool OnQuery(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame,
                       int64 query_id,
                       const CefString& request,
                       bool persistent,
                       CefRefPtr<Callback> callback) OVERRIDE {
    CEF_REQUIRE_UI_THREAD();

    // Only handle messages from the test URL.
    const std::string& url = frame->GetURL();
    if (url.find(kTestUrl) != 0)
      return false;

    // Parse |request| as a JSON dictionary.
    CefRefPtr<CefDictionaryValue> request_dict = ParseJSON(request);
    if (!request_dict) {
      callback->Failure(kMessageFormatError, "Incorrect message format");
      return true;
    }

    if (!VerifyKey(request_dict, kActionKey, VTYPE_STRING, callback))
      return true;

    const std::string& action = request_dict->GetString(kActionKey);
    if (action == "query") {
      HandleQueryAction(request_dict, callback);
    } else if (action == "start") {
      HandleStartAction(request_dict, callback);
    } else if (action == "stop") {
      HandleStopAction(request_dict, callback);
    } else {
      callback->Failure(kMessageFormatError, "Unrecognized action: " + action);
    }

    return true;
  }

 private:
  // Return current server status.
  void HandleQueryAction(CefRefPtr<CefDictionaryValue> request_dict,
                         CefRefPtr<Callback> callback) {
    CefRefPtr<CefDictionaryValue> result_dict = CefDictionaryValue::Create();
    if (handler_) {
      result_dict->SetInt(kPortKey, handler_->port());
      result_dict->SetString(kStatusKey, "running");
    } else {
      result_dict->SetInt(kPortKey, kServerPortDefault);
      result_dict->SetString(kStatusKey, "stopped");
    }
    SendResponse(callback, true, result_dict);
  }

  // Start the server.
  void HandleStartAction(CefRefPtr<CefDictionaryValue> request_dict,
                         CefRefPtr<Callback> callback) {
    if (handler_) {
      callback->Failure(kActionStateError, "Server is currently running");
      return;
    }

    if (!VerifyKey(request_dict, kPortKey, VTYPE_INT, callback))
      return;

    const int port = request_dict->GetInt(kPortKey);
    if (port < 8000 || port > 65535) {
      callback->Failure(kMessageFormatError, "Invalid port number specified");
      return;
    }

    handler_ = new ServerHandler();

    // Start the server. OnComplete will be executed upon completion.
    handler_->StartServer(port,
                          base::Bind(&Handler::OnStartComplete,
                                     weak_ptr_factory_.GetWeakPtr(), callback));
  }

  // Stop the server.
  void HandleStopAction(CefRefPtr<CefDictionaryValue> request_dict,
                        CefRefPtr<Callback> callback) {
    if (!handler_) {
      callback->Failure(kActionStateError, "Server is not currently running");
      return;
    }

    // Stop the server. OnComplete will be executed upon completion.
    handler_->StopServer(base::Bind(&Handler::OnStopComplete,
                                    weak_ptr_factory_.GetWeakPtr(), callback));

    handler_ = nullptr;
  }

  // Server start completed.
  void OnStartComplete(CefRefPtr<Callback> callback, bool success) {
    CEF_REQUIRE_UI_THREAD();
    CefRefPtr<CefDictionaryValue> result_dict = CefDictionaryValue::Create();
    if (!success) {
      handler_ = nullptr;
      result_dict->SetString(kMessageKey, "Server failed to start.");
    }
    SendResponse(callback, success, result_dict);
  }

  // Server stop completed.
  void OnStopComplete(CefRefPtr<Callback> callback, bool success) {
    CEF_REQUIRE_UI_THREAD();
    CefRefPtr<CefDictionaryValue> result_dict = CefDictionaryValue::Create();
    if (!success) {
      result_dict->SetString(kMessageKey, "Server failed to stop.");
    }
    SendResponse(callback, success, result_dict);
  }

  // Send a response in the format expected by server.html.
  static void SendResponse(CefRefPtr<Callback> callback,
                           bool success,
                           CefRefPtr<CefDictionaryValue> result_dict) {
    if (!result_dict) {
      result_dict = CefDictionaryValue::Create();
    }
    result_dict->SetString(kResultKey, success ? "success" : "failure");
    CefRefPtr<CefValue> value = CefValue::Create();
    value->SetDictionary(result_dict);
    const std::string& response = CefWriteJSON(value, JSON_WRITER_DEFAULT);
    callback->Success(response);
  }

  // Convert a JSON string to a dictionary value.
  static CefRefPtr<CefDictionaryValue> ParseJSON(const CefString& string) {
    CefRefPtr<CefValue> value = CefParseJSON(string, JSON_PARSER_RFC);
    if (value.get() && value->GetType() == VTYPE_DICTIONARY)
      return value->GetDictionary();
    return NULL;
  }

  // Verify that |key| exists in |dictionary| and has type |value_type|. Fails
  // |callback| and returns false on failure.
  static bool VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,
                        const char* key,
                        cef_value_type_t value_type,
                        CefRefPtr<Callback> callback) {
    if (!dictionary->HasKey(key) || dictionary->GetType(key) != value_type) {
      callback->Failure(
          kMessageFormatError,
          "Missing or incorrectly formatted message key: " + std::string(key));
      return false;
    }
    return true;
  }

  // Non-nullptr while the server is running.
  CefRefPtr<ServerHandler> handler_;

  // Must be the last member.
  base::WeakPtrFactory<Handler> weak_ptr_factory_;
};

}  // namespace

void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers) {
  handlers.insert(new Handler());
}

}  // namespace server_test
}  // namespace client
