/*
 * 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.
 */


#include "pagespeed/apache/simple_buffered_apache_fetch.h"

#include <functional>
#include <memory>

#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "pagespeed/apache/apache_httpd_includes.h"
#include "pagespeed/apache/header_util.h"
#include "pagespeed/apache/mock_apache.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/thread.h"
#include "pagespeed/kernel/cache/delay_cache.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"

namespace {

const char kJsData[]        = "alert ( 'hello, world!' ) ";
const char kJsUrl[]         = "http://example.com/foo.js";
const char kCacheFragment[] = "";
const char kExpectedHeaders[] =
    "Content-Type: text/plain\n"
    "Set-Cookie: test=cookie\n";

}  // namespace

namespace net_instaweb {

class SimpleBufferedApacheFetchTest : public RewriteTestBase {
 public:
  SimpleBufferedApacheFetchTest()
      : request_headers_(new RequestHeaders()),
        request_ctx_(RequestContext::NewTestRequestContextWithTimer(
            server_context_->thread_system(), timer())) {
  }

  ~SimpleBufferedApacheFetchTest() override {
    MockApache::CleanupRequest(&request_);
    MockApache::Terminate();
  }

  void SetUp() override {
    RewriteTestBase::SetUp();
    MockApache::Initialize();
    MockApache::PrepareRequest(&request_);
  }

  void TearDown() override {
    fetch_.reset();
    RewriteTestBase::TearDown();
  }

  void InitFetch(HttpStatus::Code code) {
    // Takes ownership of apache_writer and request_headers.
    fetch_.reset(new SimpleBufferedApacheFetch(
        request_ctx_, request_headers_, server_context()->thread_system(),
        &request_, message_handler()));
    rewrite_driver_->SetRequestHeaders(*fetch_->request_headers());
    fetch_->response_headers()->SetStatusAndReason(code);

    // HTTP 1.1 is most common now.
    fetch_->response_headers()->set_major_version(1);
    fetch_->response_headers()->set_minor_version(1);

    // Give it a content-type, and a cookie --- so we can make sure it
    // forwards them to Apache.
    fetch_->response_headers()->MergeContentType("text/plain");
    fetch_->response_headers()->Add("Set-Cookie", "test=cookie");

    // Nothing should have been passed through to the apache_writer yet
    EXPECT_STREQ("", MockApache::ActionsSinceLastCall());
  }


  void WaitIproTest() {
    // Run through a successful completion and verify that no issues were
    // sent to the message handler.
    AddFilter(RewriteOptions::kRewriteJavascriptExternal);
    ResponseHeaders response_headers;
    DefaultResponseHeaders(kContentTypeJavascript, 600, &response_headers);
    RequestHeaders::Properties req_properties;
    req_properties.has_cookie = false;
    req_properties.has_cookie2 = false;
    http_cache()->Put(kJsUrl, kCacheFragment, req_properties,
                      ResponseHeaders::kIgnoreVaryOnResources,
                      &response_headers, kJsData, message_handler());

    GoogleUrl gurl(kJsUrl);
    InitFetch(HttpStatus::kOK);
    rewrite_driver_->FetchInPlaceResource(gurl, false /* proxy_mode */,
                                          fetch_.get());
    fetch_->Wait();
    EXPECT_EQ(0, message_handler()->TotalMessages());

    EXPECT_EQ(StrCat("ap_set_content_length(26) "
                     "ap_set_content_type(application/javascript) "
                     "ap_remove_output_filter(MOD_EXPIRES) "
                     "ap_remove_output_filter(FIXUP_HEADERS_OUT) "
                     "ap_set_content_type(application/javascript) "
                     "ap_rwrite(", kJsData, ")"),
              MockApache::ActionsSinceLastCall());
    fetch_.reset();
  }

  class ThreadRunner : public ThreadSystem::Thread {
   public:
    ThreadRunner(RewriteTestBase* fixture, std::function<void()> fn)
        : Thread(fixture->server_context()->thread_system(), "run_function",
                 ThreadSystem::kJoinable),
          fn_(fn) {
      Start();
    }

    ~ThreadRunner() override {
      Join();
    }

    void Run() override {
      fn_();
    }

   private:
    std::function<void()> fn_;
  };

  request_rec request_;
  RequestHeaders* request_headers_;
  RequestContextPtr request_ctx_;
  std::unique_ptr<SimpleBufferedApacheFetch> fetch_;
};

TEST_F(SimpleBufferedApacheFetchTest, WaitIpro) {
  WaitIproTest();
}

TEST_F(SimpleBufferedApacheFetchTest, WaitIpro2) {
  GoogleString key = http_cache()->CompositeKey(kJsUrl, kCacheFragment);
  delay_cache()->DelayKey(key);
  ThreadRunner t(this, [this, key] {
    delay_cache()->ReleaseKey(key);
  });
  WaitIproTest();
}

TEST_F(SimpleBufferedApacheFetchTest, Success) {
  InitFetch(HttpStatus::kOK);

  ThreadRunner t(this, [this] {
    EXPECT_TRUE(fetch_->Write("hello ", message_handler()));
    EXPECT_TRUE(fetch_->Write("world", message_handler()));
    EXPECT_TRUE(fetch_->Flush(message_handler()));
    EXPECT_TRUE(fetch_->Write(".", message_handler()));
    EXPECT_TRUE(fetch_->Flush(message_handler()));
    fetch_->Done(true);
  });

  fetch_->Wait();
  EXPECT_EQ(200, request_.status);

  // Two outputs are possible: we can combine writes, but not accross flush...
  const char kExpectedSequence1[] =
      "ap_set_content_type(text/plain) "
      "ap_remove_output_filter(MOD_EXPIRES) "
      "ap_remove_output_filter(FIXUP_HEADERS_OUT) "
      "ap_set_content_type(text/plain) "
      "ap_rwrite(hello world) "
      "ap_rflush() "
      "ap_rwrite(.) "
      "ap_rflush()";

  // ... or we can end up not combining if this thread pulls the first
  // write before the worker produces a 2nd writes.
  const char kExpectedSequence2[] =
      "ap_set_content_type(text/plain) "
      "ap_remove_output_filter(MOD_EXPIRES) "
      "ap_remove_output_filter(FIXUP_HEADERS_OUT) "
      "ap_set_content_type(text/plain) "
      "ap_rwrite(hello ) "
      "ap_rwrite(world) "
      "ap_rflush() "
      "ap_rwrite(.) "
      "ap_rflush()";

  GoogleString actual = MockApache::ActionsSinceLastCall();

  EXPECT_TRUE(actual == kExpectedSequence1 || actual == kExpectedSequence2);
  EXPECT_EQ(kExpectedHeaders, HeadersOutToString(&request_));
}

TEST_F(SimpleBufferedApacheFetchTest, NotFound404) {
  InitFetch(HttpStatus::kNotFound);

  ThreadRunner t(this, [this] {
    EXPECT_TRUE(fetch_->Write("Couldn't find it.", message_handler()));
    fetch_->Done(true);
  });

  fetch_->Wait();
  EXPECT_EQ(404, request_.status);
  EXPECT_EQ(kExpectedHeaders, HeadersOutToString(&request_));
  EXPECT_EQ(
      "ap_set_content_type(text/plain) "
      "ap_remove_output_filter(MOD_EXPIRES) "
      "ap_remove_output_filter(FIXUP_HEADERS_OUT) "
      "ap_set_content_type(text/plain) "
      "ap_rwrite(Couldn't find it.)",
      MockApache::ActionsSinceLastCall());
}

}  // namespace net_instaweb
