// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/web/navigation/crw_wk_navigation_states.h"

#import <WebKit/WebKit.h>

#import "base/test/task_environment.h"
#import "base/test/tracing/test_trace_processor.h"
#import "base/test/tracing/trace_test_utils.h"
#import "base/trace_event/trace_event.h"
#import "ios/web/navigation/navigation_context_impl.h"
#import "ios/web/navigation/navigation_item_impl.h"
#import "net/http/http_response_headers.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "url/gurl.h"

namespace {
const char kTestUrl1[] = "https://test1.test/";
const char kTestUrl2[] = "https://test2.test/";
}  // namespace

namespace web {

// Test fixture for CRWWKNavigationStates testing.
class CRWWKNavigationStatesTest : public PlatformTest {
 protected:
  CRWWKNavigationStatesTest()
      : navigation1_(static_cast<WKNavigation*>([[NSObject alloc] init])),
        navigation2_(static_cast<WKNavigation*>([[NSObject alloc] init])),
        navigation3_(static_cast<WKNavigation*>([[NSObject alloc] init])),
        states_([[CRWWKNavigationStates alloc] init]) {}

 protected:
  WKNavigation* navigation1_;
  WKNavigation* navigation2_;
  WKNavigation* navigation3_;
  CRWWKNavigationStates* states_;
};

// Tests `removeNavigation:` method.
TEST_F(CRWWKNavigationStatesTest, RemovingNavigation) {
  // navigation_1 is the only navigation and it is the latest.
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
  ASSERT_EQ(WKNavigationState::REQUESTED,
            [states_ stateForNavigation:navigation1_]);
  ASSERT_EQ(navigation1_, [states_ lastAddedNavigation]);
  [states_ removeNavigation:navigation1_];
  EXPECT_FALSE([states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::NONE, [states_ stateForNavigation:navigation1_]);
}

// Tests `lastAddedNavigation` method.
TEST_F(CRWWKNavigationStatesTest, LastAddedNavigation) {
  // navigation_1 is the only navigation and it is the latest.
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
  EXPECT_EQ(WKNavigationState::REQUESTED,
            [states_ stateForNavigation:navigation1_]);
  EXPECT_EQ(navigation1_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::REQUESTED, [states_ lastAddedNavigationState]);

  // navigation_2 is added later and hence the latest.
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation2_];
  EXPECT_EQ(WKNavigationState::REQUESTED,
            [states_ stateForNavigation:navigation2_]);
  EXPECT_EQ(navigation2_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::REQUESTED, [states_ lastAddedNavigationState]);

  // Updating state for existing navigation does not make it the latest.
  [states_ setState:WKNavigationState::STARTED forNavigation:navigation1_];
  EXPECT_EQ(WKNavigationState::STARTED,
            [states_ stateForNavigation:navigation1_]);
  EXPECT_EQ(navigation2_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::REQUESTED, [states_ lastAddedNavigationState]);

  // navigation_2 is still the latest.
  [states_ setState:WKNavigationState::STARTED forNavigation:navigation2_];
  EXPECT_EQ(WKNavigationState::STARTED,
            [states_ stateForNavigation:navigation2_]);
  EXPECT_EQ(navigation2_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::STARTED, [states_ lastAddedNavigationState]);

  // navigation_3 is added later and hence the latest.
  std::unique_ptr<web::NavigationContextImpl> context =
      NavigationContextImpl::CreateNavigationContext(
          nullptr /*web_state*/, GURL(kTestUrl1), /*has_user_gesture=*/false,
          ui::PageTransition::PAGE_TRANSITION_SERVER_REDIRECT,
          /*is_renderer_initiated=*/true);
  [states_ setContext:std::move(context) forNavigation:navigation3_];
  EXPECT_EQ(navigation3_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::NONE, [states_ lastAddedNavigationState]);
}

// Tests `lastNavigationWithPendingItemInNavigationContext` method.
TEST_F(CRWWKNavigationStatesTest,
       LastNavigationWithPendingItemInNavigationContext) {
  // Empty state.
  EXPECT_FALSE([states_ lastNavigationWithPendingItemInNavigationContext]);

  // Navigation without context.
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
  EXPECT_FALSE([states_ lastNavigationWithPendingItemInNavigationContext]);

  // Navigation with context that does not have pending item.
  std::unique_ptr<web::NavigationContextImpl> context =
      NavigationContextImpl::CreateNavigationContext(
          nullptr /*web_state*/, GURL(kTestUrl1), /*has_user_gesture=*/false,
          ui::PageTransition::PAGE_TRANSITION_SERVER_REDIRECT,
          /*is_renderer_initiated=*/true);
  web::NavigationContextImpl* context_ptr = context.get();
  [states_ setContext:std::move(context) forNavigation:navigation1_];
  EXPECT_FALSE([states_ lastNavigationWithPendingItemInNavigationContext]);

  // Navigation with context that has pending item.
  auto item = std::make_unique<NavigationItemImpl>();
  context_ptr->SetNavigationItemUniqueID(item->GetUniqueID());
  context_ptr->SetItem(std::move(item));
  EXPECT_EQ(navigation1_,
            [states_ lastNavigationWithPendingItemInNavigationContext]);

  // Newest context does not have pending item.
  std::unique_ptr<web::NavigationContextImpl> context2 =
      NavigationContextImpl::CreateNavigationContext(
          nullptr /*web_state*/, GURL(kTestUrl1), /*has_user_gesture=*/false,
          ui::PageTransition::PAGE_TRANSITION_SERVER_REDIRECT,
          /*is_renderer_initiated=*/true);
  web::NavigationContextImpl* context_ptr2 = context2.get();
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation2_];
  [states_ setContext:std::move(context2) forNavigation:navigation2_];
  EXPECT_EQ(navigation1_,
            [states_ lastNavigationWithPendingItemInNavigationContext]);

  // Navigation with newest context that has pending item.
  auto item2 = std::make_unique<NavigationItemImpl>();
  context_ptr2->SetNavigationItemUniqueID(item2->GetUniqueID());
  context_ptr2->SetItem(std::move(item2));
  EXPECT_EQ(navigation2_,
            [states_ lastNavigationWithPendingItemInNavigationContext]);
}

// Tests `setContext:forNavigation:` and `contextForNavigation:` methods.
TEST_F(CRWWKNavigationStatesTest, Context) {
  EXPECT_FALSE([states_ contextForNavigation:navigation1_]);
  EXPECT_FALSE([states_ contextForNavigation:navigation2_]);
  EXPECT_FALSE([states_ contextForNavigation:navigation3_]);

  // Add first context.
  std::unique_ptr<web::NavigationContextImpl> context1 =
      NavigationContextImpl::CreateNavigationContext(
          nullptr /*web_state*/, GURL(kTestUrl1), /*has_user_gesture=*/false,
          ui::PageTransition::PAGE_TRANSITION_RELOAD,
          /*is_renderer_initiated=*/false);
  context1->SetIsSameDocument(true);
  [states_ setContext:std::move(context1) forNavigation:navigation1_];
  EXPECT_FALSE([states_ contextForNavigation:navigation2_]);
  EXPECT_FALSE([states_ contextForNavigation:navigation3_]);
  ASSERT_TRUE([states_ contextForNavigation:navigation1_]);
  EXPECT_EQ(GURL(kTestUrl1),
            [states_ contextForNavigation:navigation1_]->GetUrl());
  EXPECT_TRUE([states_ contextForNavigation:navigation1_]->IsSameDocument());
  EXPECT_FALSE([states_ contextForNavigation:navigation1_]->GetError());
  EXPECT_FALSE(
      [states_ contextForNavigation:navigation1_]->IsRendererInitiated());

  // Replace existing context.
  std::unique_ptr<web::NavigationContextImpl> context2 =
      NavigationContextImpl::CreateNavigationContext(
          nullptr /*web_state*/, GURL(kTestUrl2), /*has_user_gesture=*/false,
          ui::PageTransition::PAGE_TRANSITION_GENERATED,
          /*is_renderer_initiated=*/true);
  NSError* error = [[NSError alloc] initWithDomain:@"" code:0 userInfo:nil];
  context2->SetError(error);
  [states_ setContext:std::move(context2) forNavigation:navigation1_];
  EXPECT_FALSE([states_ contextForNavigation:navigation2_]);
  EXPECT_FALSE([states_ contextForNavigation:navigation3_]);
  ASSERT_TRUE([states_ contextForNavigation:navigation1_]);
  EXPECT_EQ(GURL(kTestUrl2),
            [states_ contextForNavigation:navigation1_]->GetUrl());
  EXPECT_FALSE([states_ contextForNavigation:navigation1_]->IsSameDocument());
  EXPECT_EQ(error, [states_ contextForNavigation:navigation1_]->GetError());
  EXPECT_TRUE(
      [states_ contextForNavigation:navigation1_]->IsRendererInitiated());

  // Extract existing context.
  std::unique_ptr<web::NavigationContextImpl> extractedContext =
      [states_ removeNavigation:navigation1_];
  EXPECT_EQ(GURL(kTestUrl2), extractedContext->GetUrl());
  EXPECT_FALSE(extractedContext->IsSameDocument());
  EXPECT_EQ(error, extractedContext->GetError());
  EXPECT_TRUE(extractedContext->IsRendererInitiated());
}

// Tests null WKNavigation object.
TEST_F(CRWWKNavigationStatesTest, NullNavigation) {
  // navigation_1 is the only navigation and it is the latest.
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
  EXPECT_EQ(WKNavigationState::REQUESTED,
            [states_ stateForNavigation:navigation1_]);
  ASSERT_EQ(navigation1_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::REQUESTED, [states_ lastAddedNavigationState]);

  // null navigation is added later and hence the latest.
  [states_ setState:WKNavigationState::STARTED forNavigation:nil];
  EXPECT_EQ(WKNavigationState::STARTED, [states_ stateForNavigation:nil]);
  EXPECT_FALSE([states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::STARTED, [states_ lastAddedNavigationState]);

  // navigation_1 is the latest again after removing null navigation.
  [states_ removeNavigation:nil];
  ASSERT_EQ(navigation1_, [states_ lastAddedNavigation]);
  EXPECT_EQ(WKNavigationState::REQUESTED, [states_ lastAddedNavigationState]);
}

// Tests -[CRWWKNavigationStates pendingNavigations].
TEST_F(CRWWKNavigationStatesTest, PendingNavigations) {
  ASSERT_EQ(0U, [states_ pendingNavigations].count);

  // Add pending navigation_1.
  [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
  ASSERT_EQ(WKNavigationState::REQUESTED,
            [states_ stateForNavigation:navigation1_]);
  ASSERT_EQ(1U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation1_]);

  // Add pending navigation_2.
  [states_ setState:WKNavigationState::STARTED forNavigation:navigation2_];
  ASSERT_EQ(WKNavigationState::STARTED,
            [states_ stateForNavigation:navigation2_]);
  ASSERT_EQ(2U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation1_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation2_]);

  // Add pending navigation_3.
  [states_ setState:WKNavigationState::STARTED forNavigation:navigation3_];
  ASSERT_EQ(WKNavigationState::STARTED,
            [states_ stateForNavigation:navigation3_]);
  ASSERT_EQ(3U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation1_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation2_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation3_]);

  // Add pending null navigation.
  [states_ setState:WKNavigationState::STARTED forNavigation:nil];
  ASSERT_EQ(WKNavigationState::STARTED, [states_ stateForNavigation:nil]);
  ASSERT_EQ(4U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation1_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation2_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation3_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:[NSNull null]]);

  // Provisionally fail null navigation.
  [states_ setState:WKNavigationState::PROVISIONALY_FAILED forNavigation:nil];
  ASSERT_EQ(WKNavigationState::PROVISIONALY_FAILED,
            [states_ stateForNavigation:nil]);
  ASSERT_EQ(3U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation1_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation2_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation3_]);

  // Commit navigation_1.
  EXPECT_FALSE([states_ isCommittedNavigation:navigation1_]);
  [states_ setState:WKNavigationState::COMMITTED forNavigation:navigation1_];
  ASSERT_EQ(WKNavigationState::COMMITTED,
            [states_ stateForNavigation:navigation1_]);
  ASSERT_EQ(2U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation2_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation3_]);
  EXPECT_TRUE([states_ isCommittedNavigation:navigation1_]);

  // Finish navigation_1.
  [states_ setState:WKNavigationState::FINISHED forNavigation:navigation1_];
  ASSERT_EQ(WKNavigationState::FINISHED,
            [states_ stateForNavigation:navigation1_]);
  ASSERT_EQ(2U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation2_]);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation3_]);

  // Remove navigation_2.
  [states_ removeNavigation:navigation2_];
  ASSERT_EQ(1U, [states_ pendingNavigations].count);
  EXPECT_TRUE([[states_ pendingNavigations] containsObject:navigation3_]);

  // Fail navigation_3.
  [states_ setState:WKNavigationState::FAILED forNavigation:navigation3_];
  ASSERT_EQ(WKNavigationState::FAILED,
            [states_ stateForNavigation:navigation3_]);
  ASSERT_EQ(0U, [states_ pendingNavigations].count);
}

TEST_F(CRWWKNavigationStatesTest, TracingTransitions) {
  base::test::TracingEnvironment tracing_environment;
  base::test::TaskEnvironment task_environment;
  base::test::TestTraceProcessor test_trace_processor;

  test_trace_processor.StartTrace("navigation");

  {
    [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::STARTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::REDIRECTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::COMMITTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::FINISHED forNavigation:navigation1_];
  }

  auto status = test_trace_processor.StopAndParseTrace();
  ASSERT_TRUE(status.ok()) << status.message();

  auto result = test_trace_processor.RunQuery(R"(
    SELECT
      name,
      EXTRACT_ARG(arg_set_id, 'debug.end_state') as end_state
    FROM slice
    WHERE category = 'navigation'
    ORDER BY ts, name
  )");

  ASSERT_TRUE(result.has_value()) << result.error();

  EXPECT_THAT(result.value(),
              ::testing::ElementsAre(
                  std::vector<std::string>{"name", "end_state"},
                  std::vector<std::string>{"Requested", "Started"},
                  std::vector<std::string>{"Started", "Redirected"},
                  std::vector<std::string>{"Redirected", "Committed"},
                  std::vector<std::string>{"Committed", "Finished"}));
}

TEST_F(CRWWKNavigationStatesTest, TracingTransitionsFailed) {
  base::test::TracingEnvironment tracing_environment;
  base::test::TaskEnvironment task_environment;
  base::test::TestTraceProcessor test_trace_processor;

  test_trace_processor.StartTrace("navigation");

  {
    [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::STARTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::FAILED forNavigation:navigation1_];
  }

  auto status = test_trace_processor.StopAndParseTrace();
  ASSERT_TRUE(status.ok()) << status.message();

  auto result = test_trace_processor.RunQuery(R"(
    SELECT
      name,
      EXTRACT_ARG(arg_set_id, 'debug.end_state') as end_state
    FROM slice
    WHERE category = 'navigation'
    ORDER BY ts, name
  )");
  ASSERT_TRUE(result.has_value()) << result.error();

  EXPECT_THAT(
      result.value(),
      ::testing::ElementsAre(std::vector<std::string>{"name", "end_state"},
                             std::vector<std::string>{"Requested", "Started"},
                             std::vector<std::string>{"Started", "Failed"}));
}

TEST_F(CRWWKNavigationStatesTest, TracingTransitionsProvisionalFailed) {
  base::test::TracingEnvironment tracing_environment;
  base::test::TaskEnvironment task_environment;
  base::test::TestTraceProcessor test_trace_processor;

  test_trace_processor.StartTrace("navigation");

  {
    [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::STARTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::PROVISIONALY_FAILED
        forNavigation:navigation1_];
  }

  auto status = test_trace_processor.StopAndParseTrace();
  ASSERT_TRUE(status.ok()) << status.message();

  auto result = test_trace_processor.RunQuery(R"(
    SELECT
      name,
      EXTRACT_ARG(arg_set_id, 'debug.end_state') as end_state
    FROM slice
    WHERE category = 'navigation'
    ORDER BY ts, name
  )");
  ASSERT_TRUE(result.has_value()) << result.error();

  EXPECT_THAT(result.value(),
              ::testing::ElementsAre(
                  std::vector<std::string>{"name", "end_state"},
                  std::vector<std::string>{"Requested", "Started"},
                  std::vector<std::string>{"Started", "ProvisionalyFailed"}));
}

TEST_F(CRWWKNavigationStatesTest, TracingTransitionsRemoved) {
  base::test::TracingEnvironment tracing_environment;
  base::test::TaskEnvironment task_environment;
  base::test::TestTraceProcessor test_trace_processor;

  test_trace_processor.StartTrace("navigation");

  {
    [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::STARTED forNavigation:navigation1_];
    [states_ removeNavigation:navigation1_];
  }

  auto status = test_trace_processor.StopAndParseTrace();
  ASSERT_TRUE(status.ok()) << status.message();

  auto result = test_trace_processor.RunQuery(R"(
    SELECT
      name,
      EXTRACT_ARG(arg_set_id, 'debug.end_state') as end_state
    FROM slice
    WHERE category = 'navigation'
    ORDER BY ts, name
  )");
  ASSERT_TRUE(result.has_value()) << result.error();

  EXPECT_THAT(
      result.value(),
      ::testing::ElementsAre(std::vector<std::string>{"name", "end_state"},
                             std::vector<std::string>{"Requested", "Started"},
                             std::vector<std::string>{"Started", "Cancelled"}));
}

TEST_F(CRWWKNavigationStatesTest, TracingTransitionsRemovedConsecutive) {
  base::test::TracingEnvironment tracing_environment;
  base::test::TaskEnvironment task_environment;
  base::test::TestTraceProcessor test_trace_processor;

  test_trace_processor.StartTrace("navigation");

  {
    [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation1_];
    [states_ setState:WKNavigationState::STARTED forNavigation:navigation1_];
    [states_ removeNavigation:navigation1_];

    [states_ setState:WKNavigationState::REQUESTED forNavigation:navigation2_];
    [states_ setState:WKNavigationState::STARTED forNavigation:navigation2_];
    [states_ setState:WKNavigationState::FINISHED forNavigation:navigation2_];
  }

  auto status = test_trace_processor.StopAndParseTrace();
  ASSERT_TRUE(status.ok()) << status.message();

  auto result = test_trace_processor.RunQuery(R"(
    SELECT
      name,
      EXTRACT_ARG(arg_set_id, 'debug.end_state') as end_state
    FROM slice
    WHERE category = 'navigation'
    ORDER BY ts, name
  )");
  ASSERT_TRUE(result.has_value()) << result.error();

  EXPECT_THAT(
      result.value(),
      ::testing::ElementsAre(std::vector<std::string>{"name", "end_state"},
                             std::vector<std::string>{"Requested", "Started"},
                             std::vector<std::string>{"Started", "Cancelled"},
                             std::vector<std::string>{"Requested", "Started"},
                             std::vector<std::string>{"Started", "Finished"}));
}

}  // namespace web
