// Copyright 2024 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/web/model/choose_file/choose_file_tab_helper.h"

#import <WebKit/WebKit.h>

#import <memory>

#import "base/functional/callback_helpers.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "ios/chrome/browser/shared/public/commands/file_upload_panel_commands.h"
#import "ios/chrome/browser/web/model/choose_file/fake_choose_file_controller.h"
#import "ios/chrome/browser/web/model/choose_file/last_tap_location_tab_helper.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/gtest/include/gtest/gtest.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"

// Test suite for ChooseFileTabHelper.
class ChooseFileTabHelperTest : public PlatformTest {
 public:
  void SetUp() override {
    PlatformTest::SetUp();
    web_state_ = std::make_unique<web::FakeWebState>();
    web_state_->WasShown();
    ChooseFileTabHelper::CreateForWebState(web_state_.get());
    LastTapLocationTabHelper::CreateForWebState(web_state_.get());
    tab_helper_ = ChooseFileTabHelper::FromWebState(web_state_.get());
  }

  void SetLastTap(CGPoint point, base::TimeTicks time) {
    LastTapLocationTabHelper::FromWebState(web_state_.get())
        ->SetLastTapForTesting(point, time);
  }

 protected:
  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<web::FakeWebState> web_state_;
  raw_ptr<ChooseFileTabHelper> tab_helper_;
};

// Tests that calling `StopChoosingFiles()` submits file selection and that
// `IsChoosingFiles()` returns false afterwards.
TEST_F(ChooseFileTabHelperTest, StopChoosingFiles) {
  EXPECT_FALSE(tab_helper_->IsChoosingFiles());

  __block bool selection_submitted = false;

  // Test that calling `StopChoosingFiles()` forwards its arguments to the
  // controller and ends file selection.
  auto controller = std::make_unique<FakeChooseFileController>(
      ChooseFileEvent::Builder()
          .SetAllowMultipleFiles(false)
          .SetHasSelectedFile(false)
          .SetWebState(web_state_.get())
          .Build());
  NSURL* file_url = [NSURL fileURLWithPath:@"/path/to/file"];
  NSArray<NSURL*>* file_urls = @[ file_url ];
  NSString* display_string = @"display_string";
  UIImage* icon_image = [[UIImage alloc] init];
  controller->SetSubmitSelectionCompletion(
      base::BindOnce(^(const FakeChooseFileController& control) {
        selection_submitted = true;
        EXPECT_NSEQ(file_urls, control.submitted_file_urls());
        EXPECT_NSEQ(display_string, control.submitted_display_string());
        EXPECT_NSEQ(icon_image, control.submitted_icon_image());
      }));
  tab_helper_->StartChoosingFiles(std::move(controller));
  EXPECT_FALSE(selection_submitted);
  EXPECT_TRUE(tab_helper_->IsChoosingFiles());
  tab_helper_->AddFileUrlReadyForSelection(file_url, [NSDate date]);
  tab_helper_->StopChoosingFiles(file_urls, display_string, icon_image);
  EXPECT_TRUE(selection_submitted);
  EXPECT_FALSE(tab_helper_->IsChoosingFiles());

  // Reset `selection_submitted`.
  selection_submitted = false;

  // Test that calling `StopChoosingFiles()` with no arguments ends file
  // selection.
  controller = std::make_unique<FakeChooseFileController>(
      ChooseFileEvent::Builder()
          .SetAllowMultipleFiles(false)
          .SetHasSelectedFile(false)
          .SetWebState(web_state_.get())
          .Build());
  tab_helper_->StartChoosingFiles(std::move(controller));
  EXPECT_TRUE(tab_helper_->IsChoosingFiles());
  tab_helper_->StopChoosingFiles();
  EXPECT_FALSE(tab_helper_->IsChoosingFiles());
}

// Tests that starting navigation to a different document stops file selection.
TEST_F(ChooseFileTabHelperTest, DidStartNavigation) {
  EXPECT_FALSE(tab_helper_->IsChoosingFiles());

  auto controller = std::make_unique<FakeChooseFileController>(
      ChooseFileEvent::Builder()
          .SetAllowMultipleFiles(false)
          .SetHasSelectedFile(false)
          .SetWebState(web_state_.get())
          .Build());
  tab_helper_->StartChoosingFiles(std::move(controller));
  EXPECT_TRUE(tab_helper_->IsChoosingFiles());

  auto navigation_context = std::make_unique<web::FakeNavigationContext>();

  navigation_context->SetIsSameDocument(true);
  tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
  EXPECT_TRUE(tab_helper_->IsChoosingFiles());

  navigation_context->SetIsSameDocument(false);
  tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
  EXPECT_FALSE(tab_helper_->IsChoosingFiles());
}

// Tests that `SetIsPresentingFilePicker()` and `IsPresentingFilePicker()` calls
// are forwarded to the controller.
TEST_F(ChooseFileTabHelperTest, SetIsPresentingFilePicker) {
  auto controller = std::make_unique<FakeChooseFileController>(
      ChooseFileEvent::Builder()
          .SetAllowMultipleFiles(false)
          .SetHasSelectedFile(false)
          .SetWebState(web_state_.get())
          .Build());
  ChooseFileController* controller_ptr = controller.get();
  tab_helper_->StartChoosingFiles(std::move(controller));
  ASSERT_TRUE(tab_helper_->IsChoosingFiles());

  tab_helper_->SetIsPresentingFilePicker(false);
  EXPECT_FALSE(tab_helper_->IsPresentingFilePicker());
  EXPECT_FALSE(controller_ptr->IsPresentingFilePicker());

  tab_helper_->SetIsPresentingFilePicker(true);
  EXPECT_TRUE(tab_helper_->IsPresentingFilePicker());
  EXPECT_TRUE(controller_ptr->IsPresentingFilePicker());
}

// Tests that `GetChooseFileEvent()` returns the event passed to the controller
// at construction.
TEST_F(ChooseFileTabHelperTest, GetChooseFileEvent) {
  ChooseFileEvent event = ChooseFileEvent::Builder()
                              .SetAllowMultipleFiles(false)
                              .SetHasSelectedFile(false)
                              .SetWebState(web_state_.get())
                              .Build();
  auto controller = std::make_unique<FakeChooseFileController>(event);
  tab_helper_->StartChoosingFiles(std::move(controller));
  const ChooseFileEvent tab_helper_event = tab_helper_->GetChooseFileEvent();
  EXPECT_EQ(event.allow_multiple_files, tab_helper_event.allow_multiple_files);
  EXPECT_EQ(event.accept_file_extensions,
            tab_helper_event.accept_file_extensions);
  EXPECT_EQ(event.accept_mime_types, tab_helper_event.accept_mime_types);
  EXPECT_EQ(event.web_state.get(), tab_helper_event.web_state.get());
  EXPECT_EQ(event.time, tab_helper_event.time);
}

// Tests that `SetLastChooseFileEvent()`, `ResetLastChooseFileEvent()` and
// `HasLastChooseFileEvent()` work as expected.
TEST_F(ChooseFileTabHelperTest, LastChooseFileEvent) {
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

  ChooseFileEvent event = ChooseFileEvent::Builder()
                              .SetAllowMultipleFiles(false)
                              .SetHasSelectedFile(false)
                              .SetWebState(web_state_.get())
                              .Build();
  tab_helper_->SetLastChooseFileEvent(event);
  EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());

  std::optional<ChooseFileEvent> tab_helper_event =
      tab_helper_->ResetLastChooseFileEvent();
  ASSERT_TRUE(tab_helper_event.has_value());
  EXPECT_EQ(event.allow_multiple_files,
            tab_helper_event.value().allow_multiple_files);
  EXPECT_EQ(event.accept_file_extensions,
            tab_helper_event.value().accept_file_extensions);
  EXPECT_EQ(event.accept_mime_types,
            tab_helper_event.value().accept_mime_types);
  EXPECT_EQ(event.web_state.get(), tab_helper_event.value().web_state.get());
  EXPECT_EQ(event.time, tab_helper_event.value().time);

  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
}

// Tests that starting navigation to a different document resets the last
// ChooseFileEvent.
TEST_F(ChooseFileTabHelperTest, DidStartNavigationResetsLastChooseFileEvent) {
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

  ChooseFileEvent event = ChooseFileEvent::Builder()
                              .SetAllowMultipleFiles(false)
                              .SetHasSelectedFile(false)
                              .SetWebState(web_state_.get())
                              .Build();
  tab_helper_->SetLastChooseFileEvent(event);
  EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());

  auto navigation_context = std::make_unique<web::FakeNavigationContext>();

  // Same document navigation should NOT reset last_choose_file_event_.
  navigation_context->SetIsSameDocument(true);
  tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
  EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());

  // Cross-document navigation should reset last_choose_file_event_.
  navigation_context->SetIsSameDocument(false);
  tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
}

// Tests that file events and panel presentation requests are ignored while a
// cross-document navigation is pending across documents.
TEST_F(ChooseFileTabHelperTest, PendingNavigationIgnoresChooseFileEvent) {
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

  ChooseFileEvent event = ChooseFileEvent::Builder()
                              .SetAllowMultipleFiles(false)
                              .SetHasSelectedFile(false)
                              .SetWebState(web_state_.get())
                              .Build();

  auto navigation_context = std::make_unique<web::FakeNavigationContext>();
  navigation_context->SetIsSameDocument(false);
  tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

  // The outgoing document attempts to set a new event while the navigation is
  // pending. It should be ignored.
  tab_helper_->SetLastChooseFileEvent(event);
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

  if (@available(iOS 18.4, *)) {
    id parameters = [OCMockObject mockForClass:[WKOpenPanelParameters class]];
    __block bool completion_called = false;
    tab_helper_->RunOpenPanel(parameters, /*frame=*/nil,
                              base::BindOnce(^(NSArray<NSURL*>* result_urls) {
                                completion_called = true;
                                EXPECT_NSEQ(nil, result_urls);
                              }));
    EXPECT_TRUE(completion_called);
    EXPECT_FALSE(tab_helper_->IsChoosingFiles());
  }

  // Uncommitted cross-document navigation finishes. Storing events should work
  // again.
  navigation_context->SetHasCommitted(false);
  tab_helper_->DidFinishNavigation(web_state_.get(), navigation_context.get());
  tab_helper_->SetLastChooseFileEvent(event);
  EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());

  // Committed cross-document navigation should reset last_choose_file_event_.
  navigation_context->SetIsSameDocument(false);
  navigation_context->SetHasCommitted(true);
  tab_helper_->DidStartNavigation(web_state_.get(), navigation_context.get());
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
}

// Tests that hiding the tab resets the last ChooseFileEvent.
TEST_F(ChooseFileTabHelperTest, WasHiddenResetsLastChooseFileEvent) {
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

  ChooseFileEvent event = ChooseFileEvent::Builder()
                              .SetAllowMultipleFiles(false)
                              .SetHasSelectedFile(false)
                              .SetWebState(web_state_.get())
                              .Build();
  tab_helper_->SetLastChooseFileEvent(event);
  EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());

  tab_helper_->WasHidden(web_state_.get());
  EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());
}

// Tests that `RunOpenPanel()` starts file selection in the tab and shows the
// file upload panel.
TEST_F(ChooseFileTabHelperTest, RunOpenPanel) {
  if (@available(iOS 18.4, *)) {
    EXPECT_FALSE(tab_helper_->IsChoosingFiles());

    ChooseFileEvent event = ChooseFileEvent::Builder()
                                .SetAllowMultipleFiles(true)
                                .SetOnlyAllowDirectory(false)
                                .SetWebState(web_state_.get())
                                .Build();
    tab_helper_->SetLastChooseFileEvent(event);
    EXPECT_TRUE(tab_helper_->HasLastChooseFileEvent());

    __block bool completion_called = false;
    NSURL* file_url = [NSURL fileURLWithPath:@"/path/to/file"];
    NSArray<NSURL*>* file_urls = @[ file_url ];

    id parameters = [OCMockObject mockForClass:[WKOpenPanelParameters class]];
    [[[parameters stub] andReturnValue:@YES] allowsMultipleSelection];
    [[[parameters stub] andReturnValue:@NO] allowsDirectories];

    id handler = OCMProtocolMock(@protocol(FileUploadPanelCommands));
    tab_helper_->SetFileUploadPanelHandler(handler);
    OCMExpect([handler showFileUploadPanel]);

    tab_helper_->RunOpenPanel(parameters, /*frame=*/nil,
                              base::BindOnce(^(NSArray<NSURL*>* result_urls) {
                                completion_called = true;
                                EXPECT_NSEQ(file_urls, result_urls);
                              }));
    EXPECT_OCMOCK_VERIFY(handler);

    EXPECT_TRUE(tab_helper_->IsChoosingFiles());
    EXPECT_FALSE(tab_helper_->HasLastChooseFileEvent());

    tab_helper_->AddFileUrlReadyForSelection(file_url, [NSDate date]);
    tab_helper_->StopChoosingFiles(file_urls, /*display_string=*/nil,
                                   /*icon_image=*/nil);

    EXPECT_TRUE(completion_called);
    EXPECT_FALSE(tab_helper_->IsChoosingFiles());
  }
}

// Tests that `RunOpenPanel()` records histograms correctly.
TEST_F(ChooseFileTabHelperTest, RunOpenPanelHistograms) {
  if (@available(iOS 18.4, *)) {
    // No event.
    base::HistogramTester histogram_tester;
    id parameters = [OCMockObject mockForClass:[WKOpenPanelParameters class]];
    [[[parameters stub] andReturnValue:@YES] allowsMultipleSelection];
    [[[parameters stub] andReturnValue:@NO] allowsDirectories];
    tab_helper_->RunOpenPanel(parameters, nil, base::DoNothing());
    histogram_tester.ExpectUniqueSample("IOS.Web.FileInput.EventMatched", false,
                                        1);
    histogram_tester.ExpectTotalCount(
        "IOS.Web.FileInput.MultipleAttributeMismatched", 0);
    histogram_tester.ExpectTotalCount(
        "IOS.Web.FileInput.DirectoryAttributeMismatched", 0);

    // Matched event.
    ChooseFileEvent event = ChooseFileEvent::Builder()
                                .SetAllowMultipleFiles(true)
                                .SetOnlyAllowDirectory(false)
                                .SetWebState(web_state_.get())
                                .Build();
    tab_helper_->SetLastChooseFileEvent(event);
    tab_helper_->RunOpenPanel(parameters, nil, base::DoNothing());
    histogram_tester.ExpectBucketCount("IOS.Web.FileInput.EventMatched", true,
                                       1);
    histogram_tester.ExpectTotalCount(
        "IOS.Web.FileInput.MultipleAttributeMismatched", 0);
    histogram_tester.ExpectTotalCount(
        "IOS.Web.FileInput.DirectoryAttributeMismatched", 0);

    // Mismatched multiple.
    event = ChooseFileEvent::Builder()
                .SetAllowMultipleFiles(false)
                .SetOnlyAllowDirectory(false)
                .SetWebState(web_state_.get())
                .Build();
    tab_helper_->SetLastChooseFileEvent(event);
    tab_helper_->RunOpenPanel(parameters, nil, base::DoNothing());
    histogram_tester.ExpectBucketCount("IOS.Web.FileInput.EventMatched", true,
                                       2);
    histogram_tester.ExpectUniqueSample(
        "IOS.Web.FileInput.MultipleAttributeMismatched", true, 1);
    histogram_tester.ExpectTotalCount(
        "IOS.Web.FileInput.DirectoryAttributeMismatched", 0);

    // Mismatched directory.
    event = ChooseFileEvent::Builder()
                .SetAllowMultipleFiles(true)
                .SetOnlyAllowDirectory(true)
                .SetWebState(web_state_.get())
                .Build();
    tab_helper_->SetLastChooseFileEvent(event);
    tab_helper_->RunOpenPanel(parameters, nil, base::DoNothing());
    histogram_tester.ExpectBucketCount("IOS.Web.FileInput.EventMatched", true,
                                       3);
    histogram_tester.ExpectTotalCount(
        "IOS.Web.FileInput.MultipleAttributeMismatched", 1);
    histogram_tester.ExpectUniqueSample(
        "IOS.Web.FileInput.DirectoryAttributeMismatched", false, 1);
  }
}

// Tests that RunOpenPanel overrides coordinates with a recent native tap
// location when there is a recent touch interaction and VoiceOver is inactive.
TEST_F(ChooseFileTabHelperTest, RunOpenPanel_TapAnchoring) {
  if (@available(iOS 18.4, *)) {
    // Set up a recent native tap location at (150, 250) less than 1 second ago.
    CGPoint tap_location = CGPointMake(150, 250);
    SetLastTap(tap_location, base::TimeTicks::Now());

    // Request coordinates at (999, 999) in the event.
    ChooseFileEvent event = ChooseFileEvent::Builder()
                                .SetAllowMultipleFiles(true)
                                .SetOnlyAllowDirectory(false)
                                .SetWebState(web_state_.get())
                                .SetScreenLocation(CGPointMake(999, 999))
                                .Build();
    tab_helper_->SetLastChooseFileEvent(event);

    id parameters = [OCMockObject mockForClass:[WKOpenPanelParameters class]];
    [[[parameters stub] andReturnValue:@YES] allowsMultipleSelection];
    [[[parameters stub] andReturnValue:@NO] allowsDirectories];

    id handler = OCMProtocolMock(@protocol(FileUploadPanelCommands));
    tab_helper_->SetFileUploadPanelHandler(handler);
    OCMExpect([handler showFileUploadPanel]);

    tab_helper_->RunOpenPanel(parameters, /*frame=*/nil, base::DoNothing());
    EXPECT_OCMOCK_VERIFY(handler);

    // Verify that the coordinates were overridden to the native tap point.
    ASSERT_TRUE(tab_helper_->IsChoosingFiles());
    EXPECT_TRUE(CGPointEqualToPoint(
        tap_location, tab_helper_->GetChooseFileEvent().screen_location));
  }
}

// Tests that RunOpenPanel does not override requested coordinates if the native
// tap is stale.
TEST_F(ChooseFileTabHelperTest, RunOpenPanel_StaleTapFallback) {
  if (@available(iOS 18.4, *)) {
    // Set up a stale native tap location at (150, 250) 5 seconds ago.
    CGPoint tap_location = CGPointMake(150, 250);
    SetLastTap(tap_location, base::TimeTicks::Now() - base::Seconds(5));

    // Request coordinates at (300, 400) in the event.
    CGPoint requested_location = CGPointMake(300, 400);
    ChooseFileEvent event = ChooseFileEvent::Builder()
                                .SetAllowMultipleFiles(true)
                                .SetOnlyAllowDirectory(false)
                                .SetWebState(web_state_.get())
                                .SetScreenLocation(requested_location)
                                .Build();
    tab_helper_->SetLastChooseFileEvent(event);

    id parameters = [OCMockObject mockForClass:[WKOpenPanelParameters class]];
    [[[parameters stub] andReturnValue:@YES] allowsMultipleSelection];
    [[[parameters stub] andReturnValue:@NO] allowsDirectories];

    id handler = OCMProtocolMock(@protocol(FileUploadPanelCommands));
    tab_helper_->SetFileUploadPanelHandler(handler);
    OCMExpect([handler showFileUploadPanel]);

    tab_helper_->RunOpenPanel(parameters, /*frame=*/nil, base::DoNothing());
    EXPECT_OCMOCK_VERIFY(handler);

    // Verify that the coordinates were NOT overridden, keeping the requested
    // location.
    ASSERT_TRUE(tab_helper_->IsChoosingFiles());
    EXPECT_TRUE(CGPointEqualToPoint(
        requested_location, tab_helper_->GetChooseFileEvent().screen_location));
  }
}

// Tests that RunOpenPanel falls back to the native tap location if requested
// coordinates are CGPointZero, even if the tap is stale.
TEST_F(ChooseFileTabHelperTest, RunOpenPanel_StaleTapFallbackToDefault) {
  if (@available(iOS 18.4, *)) {
    // Set up a stale native tap location at (150, 250) 5 seconds ago.
    CGPoint tap_location = CGPointMake(150, 250);
    SetLastTap(tap_location, base::TimeTicks::Now() - base::Seconds(5));

    // Build an event with CGPointZero coordinates.
    ChooseFileEvent event = ChooseFileEvent::Builder()
                                .SetAllowMultipleFiles(true)
                                .SetOnlyAllowDirectory(false)
                                .SetWebState(web_state_.get())
                                .SetScreenLocation(CGPointZero)
                                .Build();
    tab_helper_->SetLastChooseFileEvent(event);

    id parameters = [OCMockObject mockForClass:[WKOpenPanelParameters class]];
    [[[parameters stub] andReturnValue:@YES] allowsMultipleSelection];
    [[[parameters stub] andReturnValue:@NO] allowsDirectories];

    id handler = OCMProtocolMock(@protocol(FileUploadPanelCommands));
    tab_helper_->SetFileUploadPanelHandler(handler);
    OCMExpect([handler showFileUploadPanel]);

    tab_helper_->RunOpenPanel(parameters, /*frame=*/nil, base::DoNothing());
    EXPECT_OCMOCK_VERIFY(handler);

    // Verify that the coordinates fallback to the native tap point.
    ASSERT_TRUE(tab_helper_->IsChoosingFiles());
    EXPECT_TRUE(CGPointEqualToPoint(
        tap_location, tab_helper_->GetChooseFileEvent().screen_location));
  }
}

// Tests that IsCustomOpenPanelSupported is set to true on creation for realized
// web state.
TEST_F(ChooseFileTabHelperTest, TestIsCustomOpenPanelSupported) {
  EXPECT_TRUE(web_state_->IsCustomOpenPanelSupported());
}
