/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */



// Test the blocking w/timeout callback helper.

#include "net/instaweb/http/public/sync_fetcher_adapter_callback.h"

#include <algorithm>

#include "base/logging.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/http/public/url_async_fetcher.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/base/thread.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/base/writer.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
#include "pagespeed/kernel/util/platform.h"

namespace net_instaweb {

class MessageHandler;

namespace {

const char kText[] = "Result";
const char kHeader[] = "X-Test-HeaderCopy";

// Writer that should never be invoked.
class TrapWriter : public Writer {
 public:
  TrapWriter() {}

  virtual bool Write(const StringPiece& str, MessageHandler* message_handler) {
    ADD_FAILURE() << "Should not do a Write";
    return false;
  }

  virtual bool Flush(MessageHandler* message_handler) {
    ADD_FAILURE() << "Should not do a Flush";
    return false;
  }
 private:
  DISALLOW_COPY_AND_ASSIGN(TrapWriter);
};

// An async fetcher that writes out a response at given number of milliseconds
// elapsed, or when asked to immediately (if delay_ms <= 0). Note that it's only
// capable of one fetch, and destroys itself after that completes. If a
// syncpoint is set using set_sync, it will be notified immediately before that.
class DelayedFetcher : public UrlAsyncFetcher {
 public:
  // Note: If sim_delay <= 0, will report immediately at Fetch.
  DelayedFetcher(ThreadSystem* thread_system,
                 Timer* timer, MessageHandler* handler,
                 int64 delay_ms, bool sim_success)
      : thread_system_(thread_system),
        timer_(timer), handler_(handler), delay_ms_(delay_ms),
        sim_success_(sim_success), fetch_pending_(false), fetch_(NULL),
        sync_(NULL) {
  }

  virtual void Fetch(const GoogleString& url, MessageHandler* handler,
                     AsyncFetch* fetch) {
    CHECK(!fetch_pending_);
    fetch_ = fetch;
    fetch_pending_ = true;

    if (delay_ms_ <= 0) {
      ReportResult();
    } else {
      InvokeCallbackThread* thread =
          new InvokeCallbackThread(thread_system_, this);
      EXPECT_TRUE(thread->Start());
    }
  }

  void set_sync(WorkerTestBase::SyncPoint* sync) { sync_ = sync; }

 private:
  ~DelayedFetcher() {}

  class InvokeCallbackThread : public ThreadSystem::Thread {
   public:
    InvokeCallbackThread(ThreadSystem* thread_system,
                         DelayedFetcher* parent)
        : Thread(thread_system, "delayed_fetch", ThreadSystem::kDetached),
          parent_(parent) {
    }

    virtual void Run() {
      parent_->timer_->SleepMs(parent_->delay_ms_);
      parent_->ReportResult();
      delete this;
    }

   private:
    DelayedFetcher* parent_;
  };

  void ReportResult() {
    ResponseHeaders headers;
    if (sim_success_) {
      fetch_->response_headers()->CopyFrom(headers);
      fetch_->response_headers()->Add(kHeader, kText);
      fetch_->response_headers()->set_status_code(HttpStatus::kOK);
      fetch_->HeadersComplete();
      fetch_->Write(kText, handler_);
    }
    fetch_->Done(sim_success_);
    if (sync_ != NULL) {
      sync_->Notify();
    }
    delete this;
  }

  ThreadSystem* thread_system_;
  Timer* timer_;
  MessageHandler* handler_;

  // Simulation settings:
  int64 delay_ms_;  // how long till we report the result
  bool sim_success_;  // whether to report success or failure

  // Fetch session:
  bool fetch_pending_;
  AsyncFetch* fetch_;

  // If non-NULL, will be used to wake up an owner when done.
  WorkerTestBase::SyncPoint* sync_;
};

}  // namespace

class SyncFetcherAdapterTest : public testing::Test {
 public:
  SyncFetcherAdapterTest()
      : timer_(Platform::CreateTimer()),
        thread_system_(Platform::CreateThreadSystem()),
        handler_(thread_system_->NewMutex()) {
  }

 protected:
  void DoFetch(UrlAsyncFetcher* fetcher, SyncFetcherAdapterCallback* callback) {
    fetcher->Fetch("http://www.example.com/", &handler_, callback);
  }

  void Wait(SyncFetcherAdapterCallback* callback, int64 timeout_ms) {
    bool locked_ok = callback->LockIfNotReleased();
    ASSERT_TRUE(locked_ok);
    // The thread safety annotation analysis doesn't recognize the ASSERT_TRUE
    // as guaranteeing that the lock is held, so provide a dummy check here to
    // provide that guarantee for the compiler.
    if (!locked_ok) {
      return;
    }
    // Should always succeed since we don't call ->Release
    // on callback until end of this method.
    int64 now_ms = timer_->NowMs();
    for (int64 end_ms = now_ms + timeout_ms;
         !callback->IsDoneLockHeld() && (now_ms < end_ms);
         now_ms = timer_->NowMs()) {
      int64 remaining_ms = std::max(static_cast<int64>(0), end_ms - now_ms);
      callback->TimedWait(remaining_ms);
    }
    callback->Unlock();
  }

  void TestSuccessfulFetch(UrlAsyncFetcher* async_fetcher) {
    GoogleString out_str;
    StringWriter out_writer(&out_str);
    RequestContextPtr ctx(
        RequestContext::NewTestRequestContext(thread_system_.get()));

    SyncFetcherAdapterCallback* callback =
        new SyncFetcherAdapterCallback(thread_system_.get(), &out_writer, ctx);
    DoFetch(async_fetcher, callback);
    Wait(callback, 1000);

    EXPECT_TRUE(callback->IsDone());
    EXPECT_TRUE(callback->success());
    EXPECT_EQ(kText, out_str);

    ConstStringStarVector values;
    EXPECT_TRUE(callback->response_headers()->Lookup(kHeader, &values));
    ASSERT_EQ(1, values.size());
    EXPECT_EQ(GoogleString(kText), *(values[0]));

    callback->Release();
  }

  void TestFailedFetch(UrlAsyncFetcher* async_fetcher, int64 timeout_ms) {
    TrapWriter writer;
    RequestContextPtr ctx(
        RequestContext::NewTestRequestContext(thread_system_.get()));
    SyncFetcherAdapterCallback* callback =
        new SyncFetcherAdapterCallback(thread_system_.get(), &writer, ctx);

    async_fetcher->Fetch("http://www.example.com", &handler_, callback);
    Wait(callback, timeout_ms);
    EXPECT_FALSE(callback->success());
    callback->Release();
  }

  void TestTimeoutFetch(DelayedFetcher* async_fetcher) {
    WorkerTestBase::SyncPoint sync(thread_system_.get());

    // We use a sync point to wait for the fetch thread to exit since the
    // fixture owns the timer and we need that to be alive from the callbacks.
    async_fetcher->set_sync(&sync);

    // First let the sync fetcher timeout, and return failure.
    TestFailedFetch(async_fetcher, 1 /* one millisecond timeout here */);
    sync.Wait();
  }

  scoped_ptr<Timer> timer_;
  scoped_ptr<ThreadSystem> thread_system_;
  MockMessageHandler handler_;
};

TEST_F(SyncFetcherAdapterTest, QuickOk) {
  DelayedFetcher* async_fetcher = new DelayedFetcher(
      thread_system_.get(), timer_.get(), &handler_, 0, true);
  TestSuccessfulFetch(async_fetcher);
}

TEST_F(SyncFetcherAdapterTest, SlowOk) {
  DelayedFetcher* async_fetcher = new DelayedFetcher(
      thread_system_.get(), timer_.get(), &handler_, 5, true);
  TestSuccessfulFetch(async_fetcher);
}

TEST_F(SyncFetcherAdapterTest, QuickFail) {
  DelayedFetcher* async_fetcher = new DelayedFetcher(
      thread_system_.get(), timer_.get(), &handler_, 0, false);
  TestFailedFetch(async_fetcher, 1000);
}

TEST_F(SyncFetcherAdapterTest, SlowFail) {
  DelayedFetcher* async_fetcher = new DelayedFetcher(
      thread_system_.get(), timer_.get(), &handler_, 5, false);
  TestFailedFetch(async_fetcher, 1000);
}

TEST_F(SyncFetcherAdapterTest, TimeoutOk) {
  DelayedFetcher* async_fetcher = new DelayedFetcher(
      thread_system_.get(), timer_.get(), &handler_, 25, true);
  TestTimeoutFetch(async_fetcher);
}

TEST_F(SyncFetcherAdapterTest, TimeoutFail) {
  DelayedFetcher* async_fetcher = new DelayedFetcher(
      thread_system_.get(), timer_.get(), &handler_, 25, false);
  TestTimeoutFetch(async_fetcher);
}

}  // namespace net_instaweb
