// 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/chrome/app/application_delegate/url_opener.h"

#import <Foundation/Foundation.h>

#import "base/check_op.h"
#import "base/test/with_feature_override.h"
#import "ios/chrome/app/application_delegate/fake_tab_opener.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/application_delegate/url_opener_params.h"
#import "ios/chrome/app/profile/profile_init_stage.h"
#import "ios/chrome/app/startup/chrome_app_startup_parameters.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/fake_connection_information.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/testing/open_url_context.h"
#import "ios/web/public/test/web_task_environment.h"
#import "net/base/apple/url_conversions.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

#pragma mark - stubs and test fakes

@interface StubStartupInformation : NSObject <StartupInformation>
@end
@implementation StubStartupInformation
@synthesize isFirstRun = _isFirstRun;
@synthesize isColdStart = _isColdStart;
@synthesize appLaunchTime = _appLaunchTime;
@synthesize didFinishLaunchingTime = _didFinishLaunchingTime;
@synthesize firstSceneConnectionTime = _firstSceneConnectionTime;
@synthesize isTerminating = _isTerminating;

- (FirstUserActionRecorder*)firstUserActionRecorder {
  return nil;
}

- (void)resetFirstUserActionRecorder {
}

- (void)expireFirstUserActionRecorder {
}

- (void)expireFirstUserActionRecorderAfterDelay:(NSTimeInterval)delay {
}

- (void)activateFirstUserActionRecorderWithBackgroundTime:
    (NSTimeInterval)backgroundTime {
}

- (void)stopChromeMain {
}

- (NSDictionary*)launchOptions {
  return @{};
}

@end

#pragma mark -

class URLOpenerTest : public PlatformTest {
 protected:
  URLOpenerTest() {
    startup_information_mock_ =
        OCMStrictProtocolMock(@protocol(StartupInformation));
    connection_information_mock_ =
        OCMStrictProtocolMock(@protocol(ConnectionInformation));
    tab_opener_mock_ = OCMStrictProtocolMock(@protocol(TabOpening));
  }

  ~URLOpenerTest() override {
    EXPECT_OCMOCK_VERIFY(startup_information_mock_);
    EXPECT_OCMOCK_VERIFY(connection_information_mock_);
    EXPECT_OCMOCK_VERIFY(tab_opener_mock_);
  }

  id<StartupInformation> startup_information_mock_;
  id<ConnectionInformation> connection_information_mock_;
  id<TabOpening> tab_opener_mock_;

 private:
  web::WebTaskEnvironment task_environment_;
};

TEST_F(URLOpenerTest, HandleOpenURL) {
  // A set of tests for robustness of
  // application:openURL:options:tabOpener:startupInformation:
  // It verifies that the function handles correctly different URLs parsed by
  // ChromeAppStartupParameters.

  id<StartupInformation> startupInformation =
      [[StubStartupInformation alloc] init];
  id<ConnectionInformation> connectionInformation =
      [[FakeConnectionInformation alloc] init];

  // The array with the different states to tests (active, not active).
  NSArray* applicationStatesToTest = @[ @YES, @NO ];

  FakeTabOpener* tabOpener = [[FakeTabOpener alloc] init];

  // The keys for this dictionary is the URL to call openURL:. The value
  // from the key is either YES or NO to indicate if this is a valid URL
  // or not.

  NSDictionary* urlsToTest = @{
    [NSNull null] : @NO,
    @"" : @NO,
    // Tests for http, googlechrome, and chromium scheme URLs.
    @"http://www.google.com/" : @YES,
    @"https://www.google.com/settings/account/" : @YES,
    @"googlechrome://www.google.com/" : @YES,
    @"googlechromes://www.google.com/settings/account/" : @YES,
    @"chromium://www.google.com/" : @YES,
    @"chromiums://www.google.com/settings/account/" : @YES,

    // Google search results page URLs.
    @"https://www.google.com/search?q=pony&"
     "sugexp=chrome,mod=7&sourceid=chrome&ie=UTF-8" : @YES,
    @"googlechromes://www.google.com/search?q=pony&"
     "sugexp=chrome,mod=7&sourceid=chrome&ie=UTF-8" : @YES,

    // Other protocols.
    @"chromium-x-callback://x-callback-url/open?url=https://"
     "www.google.com&x-success=http://success" : @YES,
    @"file://localhost/path/to/file.pdf" : @YES,

    // Invalid format input URL will be ignored.
    @"this.is.not.a.valid.url" : @NO,

    // Valid format but invalid data.
    @"this://is/garbage/but/valid" : @YES
  };

  NSArray* sourcesToTest = @[
    @"", @"com.google.GoogleMobile", @"com.google.GooglePlus",
    @"com.google.SomeOtherProduct", @"com.apple.mobilesafari",
    @"com.othercompany.otherproduct"
  ];
  // See documentation for `annotation` property in
  // UIDocumentInteractionstartupInformation Class Reference.  The following
  // values are mostly to detect garbage-in situations and ensure that the app
  // won't crash or garbage out.
  NSArray* annotationsToTest = @[
    [NSNull null], [NSArray arrayWithObjects:@"foo", @"bar", nil],
    [NSDictionary dictionaryWithObject:@"bar" forKey:@"foo"],
    @"a string annotation object"
  ];
  for (id urlString in [urlsToTest allKeys]) {
    for (id source in sourcesToTest) {
      for (id annotation in annotationsToTest) {
        for (NSNumber* applicationActive in applicationStatesToTest) {
          BOOL applicationIsActive = [applicationActive boolValue];
          connectionInformation.startupParameters = nil;
          [tabOpener resetURL];
          NSURL* testUrl = urlString == [NSNull null]
                               ? nil
                               : [NSURL URLWithString:urlString];
          BOOL isValid = [[urlsToTest objectForKey:urlString] boolValue];

          TestSceneOpenURLOptions* options =
              [[TestSceneOpenURLOptions alloc] init];
          options.sourceApplication = source;
          options.annotation = annotation;

          TestOpenURLContext* context = [[TestOpenURLContext alloc] init];
          context.URL = testUrl;
          context.options = (id)options;  //< Unsafe cast intended.

          URLOpenerParams* urlOpenerParams = [[URLOpenerParams alloc]
              initWithUIOpenURLContext:(id)context];  //< Unsafe cast intended.

          ChromeAppStartupParameters* params = [ChromeAppStartupParameters
              startupParametersWithURL:testUrl
                     sourceApplication:nil
                       applicationMode:ApplicationModeForTabOpening::
                                           UNDETERMINED
                  forceApplicationMode:NO];

          // Action.
          BOOL result = [URLOpener openURL:urlOpenerParams
                         applicationActive:applicationIsActive
                                 tabOpener:tabOpener
                     connectionInformation:connectionInformation
                        startupInformation:startupInformation
                               prefService:nil
                                 initStage:ProfileInitStage::kFinal];

          // Tests.
          EXPECT_EQ(isValid, result);
          if (!applicationIsActive) {
            if (result) {
              EXPECT_EQ([params externalURL],
                        connectionInformation.startupParameters.externalURL);
            } else {
              EXPECT_EQ(nil, connectionInformation.startupParameters);
            }
          } else if (result) {
            if ([params completeURL].SchemeIsFile()) {
              // External file:// URL will be loaded by WebState, which expects
              // complete // file:// URL. chrome:// URL is expected to be
              // displayed in the omnibox, and omnibox shows virtual URL.
              EXPECT_EQ([params completeURL],
                        tabOpener.urlLoadParams.web_params.url);
              EXPECT_EQ([params externalURL],
                        tabOpener.urlLoadParams.web_params.virtual_url);
            } else {
              // External chromium-x-callback:// URL will be loaded by
              // WebState, which expects externalURL URL.
              EXPECT_EQ([params externalURL],
                        tabOpener.urlLoadParams.web_params.url);
            }
            tabOpener.completionBlock();
            EXPECT_EQ(nil, connectionInformation.startupParameters);
          }
        }
      }
    }
  }
}

// Tests that -handleApplication set startup parameters as expected.
TEST_F(URLOpenerTest, VerifyLaunchOptions) {
  // Setup.
  NSURL* url = [NSURL URLWithString:@"chromium://www.google.com"];
  URLOpenerParams* urlOpenerParams =
      [[URLOpenerParams alloc] initWithURL:url
                         sourceApplication:@"com.apple.mobilesafari"];

  OCMExpect([startup_information_mock_ resetFirstUserActionRecorder]);
  __block ChromeAppStartupParameters* params = nil;
  OCMExpect([connection_information_mock_
      setStartupParameters:[OCMArg checkWithBlock:^(
                                       ChromeAppStartupParameters* p) {
        params = p;
        EXPECT_NSEQ(net::NSURLWithGURL(p.completeURL), url);
        EXPECT_EQ(p.callerApp, CALLER_APP_APPLE_MOBILESAFARI);
        return YES;
      }]]);
  OCMExpect([connection_information_mock_ startupParameters]).andReturn(params);

  // Action.
  [URLOpener handleLaunchOptions:urlOpenerParams
                       tabOpener:tab_opener_mock_
           connectionInformation:connection_information_mock_
              startupInformation:startup_information_mock_
                     prefService:nil
                       initStage:ProfileInitStage::kFinal];
}

// Tests that -handleApplication set startup parameters as expected with options
// as nil.
TEST_F(URLOpenerTest, VerifyLaunchOptionsNil) {
  // Creates a mock with no stub. This test will pass only if we don't use these
  // objects.

  // Action.
  [URLOpener handleLaunchOptions:nil
                       tabOpener:nil
           connectionInformation:connection_information_mock_
              startupInformation:startup_information_mock_
                     prefService:nil
                       initStage:ProfileInitStage::kStart];
}

// Tests that -handleApplication set startup parameters as expected with no
// source application.
TEST_F(URLOpenerTest, VerifyLaunchOptionsWithNoSourceApplication) {
  // Setup.
  NSURL* url = [NSURL URLWithString:@"chromium://www.google.com"];
  URLOpenerParams* urlOpenerParams = [[URLOpenerParams alloc] initWithURL:url
                                                        sourceApplication:nil];

  FakeTabOpener* tab_opener_mock_ = [[FakeTabOpener alloc] init];

  __block ChromeAppStartupParameters* params = nil;
  OCMExpect([connection_information_mock_
      setStartupParameters:[OCMArg checkWithBlock:^(
                                       ChromeAppStartupParameters* p) {
        params = p;
        EXPECT_NSEQ(net::NSURLWithGURL(p.completeURL), url);
        EXPECT_EQ(p.callerApp, CALLER_APP_NOT_AVAILABLE);
        return YES;
      }]]);
  OCMExpect([connection_information_mock_ startupParameters]).andReturn(params);
  OCMExpect([startup_information_mock_ resetFirstUserActionRecorder]);

  // Action.
  [URLOpener handleLaunchOptions:urlOpenerParams
                       tabOpener:tab_opener_mock_
           connectionInformation:connection_information_mock_
              startupInformation:startup_information_mock_
                     prefService:nil
                       initStage:ProfileInitStage::kFinal];
}

// Tests that -handleApplication set startup parameters as expected with no url.
TEST_F(URLOpenerTest, VerifyLaunchOptionsWithNoURL) {
  // Setup.
  URLOpenerParams* urlOpenerParams =
      [[URLOpenerParams alloc] initWithURL:nil
                         sourceApplication:@"com.apple.mobilesafari"];

  // Action.
  [URLOpener handleLaunchOptions:urlOpenerParams
                       tabOpener:nil
           connectionInformation:connection_information_mock_
              startupInformation:startup_information_mock_
                     prefService:nil
                       initStage:ProfileInitStage::kStart];
}

// Tests that -handleApplication set startup parameters as expected with a bad
// url.
TEST_F(URLOpenerTest, VerifyLaunchOptionsWithBadURL) {
  // Setup.
  NSURL* url = [NSURL URLWithString:@"chromium.www.google.com"];
  URLOpenerParams* urlOpenerParams =
      [[URLOpenerParams alloc] initWithURL:url
                         sourceApplication:@"com.apple.mobilesafari"];
  OCMExpect([startup_information_mock_ resetFirstUserActionRecorder]);
  OCMExpect([connection_information_mock_ setStartupParameters:[OCMArg isNil]]);
  OCMExpect([connection_information_mock_ startupParameters]);

  // Action.
  [URLOpener handleLaunchOptions:urlOpenerParams
                       tabOpener:tab_opener_mock_
           connectionInformation:connection_information_mock_
              startupInformation:startup_information_mock_
                     prefService:nil
                       initStage:ProfileInitStage::kFinal];
}

// Tests URL is not opened if the FRE is presented.
TEST_F(URLOpenerTest, PresentingFirstRunUI) {
  // Setup.
  NSURL* url = [NSURL URLWithString:@"chromium://www.google.com"];
  URLOpenerParams* urlOpenerParams =
      [[URLOpenerParams alloc] initWithURL:url
                         sourceApplication:@"com.apple.mobilesafari"];
  __block ChromeAppStartupParameters* params = nil;
  OCMExpect([connection_information_mock_
      setStartupParameters:[OCMArg checkWithBlock:^(
                                       ChromeAppStartupParameters* p) {
        params = p;
        EXPECT_NSEQ(net::NSURLWithGURL(p.completeURL), url);
        EXPECT_EQ(p.callerApp, CALLER_APP_APPLE_MOBILESAFARI);
        return YES;
      }]]);
  OCMExpect([connection_information_mock_ startupParameters]).andReturn(params);

  // Action.
  [URLOpener handleLaunchOptions:urlOpenerParams
                       tabOpener:tab_opener_mock_
           connectionInformation:connection_information_mock_
              startupInformation:startup_information_mock_
                     prefService:nil
                       initStage:ProfileInitStage::kFirstRun];
}
