// Copyright 2012 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/browser/omnibox/ui/omnibox_text_field_ios.h"

#import "base/command_line.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/path_service.h"
#import "base/strings/string_split.h"
#import "base/test/allow_check_is_test_for_testing.h"
#import "base/test/task_environment.h"
#import "ios/chrome/browser/omnibox/public/omnibox_presentation_context.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_field_paste_delegate.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_input_delegate.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/NSString+Chromium.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/common/uikit_ui_util.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"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

class OmniboxTextFieldIOSTest : public PlatformTest {
 protected:
  void SetUp() override {
    base::test::AllowCheckIsTestForTesting();
    PlatformTest::SetUp();
    // This rect is fairly arbitrary. The text field just needs a non-zero width
    // so that the pre-edit label's text alignment can be tested.
    CGRect rect = CGRectMake(0, 0, 100, 20);
    textfield_ = [[OmniboxTextFieldIOS alloc]
              initWithFrame:rect
        presentationContext:OmniboxPresentationContext::kLocationBar];
    root_view_controller_ = GetAnyKeyWindow().rootViewController;
    [root_view_controller_.view addSubview:textfield_];
  }

  void TearDown() override {
    if ([textfield_ isFirstResponder]) {
      [textfield_ resignFirstResponder];
    }
    [textfield_ removeFromSuperview];
  }

  void ExpectRectEqual(CGRect expectedRect, CGRect actualRect) {
    EXPECT_EQ(expectedRect.origin.x, actualRect.origin.x);
    EXPECT_EQ(expectedRect.origin.y, actualRect.origin.y);
    EXPECT_EQ(expectedRect.size.width, actualRect.size.width);
    EXPECT_EQ(expectedRect.size.height, actualRect.size.height);
  }

  // Verifies that the `selectedNSRange` function properly converts from opaque
  // UITextRanges to NSRanges.  This function selects blocks of text in the text
  // field and compares the field's actual selected text to the converted
  // NSRange.
  void VerifySelectedNSRanges(NSString* text) {
    // The NSRange conversion mechanism only works when the field is first
    // responder.
    [textfield_ setText:text];
    if (![textfield_ isFirstResponder]) {
      [textfield_ becomeFirstResponder];
    }
    EXPECT_TRUE([textfield_ isFirstResponder]);

    // `i` and `j` hold the start and end offsets of the range that is currently
    // being tested.  This function iterates through all possible combinations
    // of `i` and `j`.
    NSInteger i = 0;
    NSInteger j = i + 1;
    UITextPosition* beginning = [textfield_ beginningOfDocument];
    UITextPosition* start =
        [textfield_ positionFromPosition:[textfield_ beginningOfDocument]
                                  offset:i];

    // In order to avoid making any assumptions about the length of the text in
    // the field, this test operates by incrementing the `i` and `j` offsets and
    // converting them to opaque UITextPositions.  If either `i` or `j` are
    // invalid offsets for the current field text,
    // `positionFromPosition:offset:` is documented to return nil.  This is used
    // as a signal to stop incrementing that offset and reset (or end the test).
    while (start) {
      UITextPosition* end = [textfield_ positionFromPosition:beginning
                                                      offset:j];
      while (end) {
        [textfield_
            setSelectedTextRange:[textfield_ textRangeFromPosition:start
                                                        toPosition:end]];

        // There are two ways to get the selected text:
        // 1) Ask the field for it directly.
        // 2) Compute the selected NSRange and use that to extract a substring
        //    from the field's text.
        // This block of code ensures that the two methods give identical text.
        NSRange nsrange = [textfield_ selectedNSRange];
        NSString* nstext = [[textfield_ text] substringWithRange:nsrange];
        UITextRange* uirange = [textfield_ selectedTextRange];
        NSString* uitext = [textfield_ textInRange:uirange];
        EXPECT_NSEQ(nstext, uitext);

        // Increment `j` and `end` for the next iteration of the inner while
        // loop.
        ++j;
        end = [textfield_ positionFromPosition:beginning offset:j];
      }

      // Increment `i` and `start` for the next iteration of the outer while
      // loop.  This also requires `j` to be reset.
      ++i;
      j = i + 1;
      start = [textfield_ positionFromPosition:beginning offset:i];
    }
  }

  UIViewController* root_view_controller_;
  OmniboxTextFieldIOS* textfield_;
  base::test::TaskEnvironment task_environment_;
};

// Tests that selectedNSRange and selectedTextRange returns similar values.
TEST_F(OmniboxTextFieldIOSTest, SelectedRanges) {
  base::FilePath test_data_directory;
  ASSERT_TRUE(base::PathService::Get(ios::DIR_TEST_DATA, &test_data_directory));
  base::FilePath test_file = test_data_directory.Append(
      FILE_PATH_LITERAL("omnibox/selected_ranges.txt"));
  ASSERT_TRUE(base::PathExists(test_file));

  std::string contents;
  ASSERT_TRUE(base::ReadFileToString(test_file, &contents));
  std::vector<std::string> test_strings = base::SplitString(
      contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  for (size_t i = 0; i < test_strings.size(); ++i) {
    if (test_strings[i].size() > 0) {
      VerifySelectedNSRanges([NSString cr_fromString:test_strings[i]]);
    }
  }
}

TEST_F(OmniboxTextFieldIOSTest, SelectExitsPreEditState) {
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ isPreEditing]);
  [textfield_ select:nil];
  EXPECT_FALSE([textfield_ isPreEditing]);
}

TEST_F(OmniboxTextFieldIOSTest, SelectAllExitsPreEditState) {
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ isPreEditing]);
  [textfield_ selectAll:nil];
  EXPECT_FALSE([textfield_ isPreEditing]);
}

TEST_F(OmniboxTextFieldIOSTest, CopyInPreedit) {
  id delegateMock = OCMProtocolMock(@protocol(OmniboxTextInputDelegate));
  NSString* testString = @"omnibox test string";
  [textfield_ setText:testString];
  textfield_.omniboxTextInputDelegate = delegateMock;
  [textfield_ becomeFirstResponder];
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ canPerformAction:@selector(copy:) withSender:nil]);
  [delegateMock textInputDidCopy:textfield_];
  [textfield_ copy:nil];
  EXPECT_NSEQ(textfield_.text, testString);
  EXPECT_OCMOCK_VERIFY(delegateMock);
}

TEST_F(OmniboxTextFieldIOSTest, CutInPreedit) {
  id delegateMock = OCMProtocolMock(@protocol(OmniboxTextInputDelegate));
  NSString* testString = @"omnibox test string";
  [textfield_ setText:testString];
  textfield_.omniboxTextInputDelegate = delegateMock;
  [textfield_ becomeFirstResponder];
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ canPerformAction:@selector(cut:) withSender:nil]);
  [delegateMock textInputDidCopy:textfield_];
  [textfield_ cut:nil];
  EXPECT_NSEQ(textfield_.text, @"");
  EXPECT_OCMOCK_VERIFY(delegateMock);
}

// Tests that the accessibility value is the placeholder text when the text
// field is empty.
TEST_F(OmniboxTextFieldIOSTest, AccessibilityValueWhenEmpty) {
  textfield_.text = @"";
  textfield_.placeholder = @"Placeholder Text";
  EXPECT_NSEQ(@"Placeholder Text", textfield_.accessibilityValue);
}

// Tests that the accessibility value is the text content when the text field is
// not empty.
TEST_F(OmniboxTextFieldIOSTest, AccessibilityValueWhenNotEmpty) {
  textfield_.text = @"User Text";
  textfield_.placeholder = @"Placeholder Text";
  EXPECT_NSEQ(@"User Text", textfield_.accessibilityValue);
}

// Tests that the testing value is correct when the text field is empty.
TEST_F(OmniboxTextFieldIOSTest, TextValueForTestingWhenEmpty) {
  textfield_.text = @"";
  EXPECT_NSEQ(@"||||||||", textfield_.textValueForTesting);
}

// Tests that the testing value is correct when the text field is not empty.
TEST_F(OmniboxTextFieldIOSTest, TextValueForTestingWhenNotEmpty) {
  textfield_.text = @"User Text";
  EXPECT_NSEQ(@"User Text||||||||", textfield_.textValueForTesting);
}

// Tests that the testing value is correct with autocomplete text.
TEST_F(OmniboxTextFieldIOSTest, TextValueForTestingWithAutocomplete) {
  NSAttributedString* text =
      [[NSAttributedString alloc] initWithString:@"User TextAutocomplete"];
  [textfield_ setText:text userTextLength:9];
  EXPECT_NSEQ(@"User Text||||Autocomplete||||", textfield_.textValueForTesting);
}

// Tests that the testing value is correct with additional text.
TEST_F(OmniboxTextFieldIOSTest, TextValueForTestingWithAdditionalText) {
  textfield_.text = @"User Text";
  [textfield_ setAdditionalText:@"Additional"];
  EXPECT_NSEQ(@"User Text||||||||Additional", textfield_.textValueForTesting);
}

// Tests that the testing value is correct with autocomplete and additional
// text.
TEST_F(OmniboxTextFieldIOSTest, TextValueForTestingWithBoth) {
  NSAttributedString* text =
      [[NSAttributedString alloc] initWithString:@"User TextAutocomplete"];
  [textfield_ setText:text userTextLength:9];
  [textfield_ setAdditionalText:@"Additional"];
  EXPECT_NSEQ(@"User Text||||Autocomplete||||Additional",
              textfield_.textValueForTesting);
}

}  // namespace

@interface OmniboxTextFieldPasteDelegate (Testing)
@property(nonatomic, strong) NSURL* URL;
@end

TEST_F(OmniboxTextFieldIOSTest, PasteDelegateSanitizesDragAndDrop) {
  OmniboxTextFieldPasteDelegate* delegate =
      [[OmniboxTextFieldPasteDelegate alloc] init];
  delegate.textInput = textfield_;

  UITextRange* range = OCMClassMock([UITextRange class]);

  // 1. Test standard string drop (without javascript scheme)
  NSAttributedString* item1 =
      [[NSAttributedString alloc] initWithString:@"https://example.com"];
  NSAttributedString* result1 =
      [delegate textPasteConfigurationSupporting:textfield_
                    combineItemAttributedStrings:@[ item1 ]
                                        forRange:range];
  EXPECT_NSEQ(@"https://example.com", result1.string);

  // 2. Test malicious javascript scheme drop
  NSAttributedString* item2 =
      [[NSAttributedString alloc] initWithString:@"javascript:alert(1)"];
  NSAttributedString* result2 =
      [delegate textPasteConfigurationSupporting:textfield_
                    combineItemAttributedStrings:@[ item2 ]
                                        forRange:range];
  EXPECT_NSEQ(@"alert(1)", result2.string);

  // 3. Test nested/broken javascript scheme drops
  NSAttributedString* item3 = [[NSAttributedString alloc]
      initWithString:@"java\x0d\x0ascript:alert(0)"];
  NSAttributedString* result3 =
      [delegate textPasteConfigurationSupporting:textfield_
                    combineItemAttributedStrings:@[ item3 ]
                                        forRange:range];
  EXPECT_NSEQ(@"alert(0)", result3.string);

  // 4. Test cached URL sanitization
  delegate.URL = [NSURL URLWithString:@"javascript:alert(2)"];
  NSAttributedString* result4 =
      [delegate textPasteConfigurationSupporting:textfield_
                    combineItemAttributedStrings:@[]
                                        forRange:range];
  EXPECT_NSEQ(@"alert(2)", result4.string);
  EXPECT_EQ(nil, delegate.URL);
}
