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

#include "chrome/browser/smart_card/chromeos_smart_card_delegate.h"

#include <optional>

#include "ash/constants/ash_switches.h"
#include "base/check_deref.h"
#include "base/memory/raw_ref.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/smart_card/smart_card_permission_context.h"
#include "chrome/browser/smart_card/smart_card_permission_context_factory.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_profile.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_types.mojom-shared.h"
#include "components/tabs/public/tab_interface.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/smart_card_delegate.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "third_party/blink/public/common/features.h"

namespace {
constexpr char kDummyReader[] = "Dummy Reader";
}  // namespace

class ChromeOsSmartCardDelegateBrowserTest
    : public web_app::IsolatedWebAppBrowserTestHarness {
 public:
  void SetUpOnMainThread() override {
    IsolatedWebAppBrowserTestHarness::SetUpOnMainThread();
    app_ = web_app::IsolatedWebAppBuilder(
               web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
                   network::mojom::PermissionsPolicyFeature::kSmartCard))
               .BuildBundle();
    app_frame_ = OpenApp(app_->InstallChecked(profile()).app_id());
    ASSERT_TRUE(app_frame_);
  }

  void TearDownOnMainThread() override {
    app_.reset();
    app_frame_ = nullptr;
    IsolatedWebAppBrowserTestHarness::TearDownOnMainThread();
  }

  base::Time GetLastUsed(content::RenderFrameHost& rfh) {
    return content_settings::PageSpecificContentSettings::GetForFrame(&rfh)
        ->GetLastUsedTime(
            content_settings::mojom::ContentSettingsType::SMART_CARD_GUARD);
  }

  bool IsInUse(content::RenderFrameHost& rfh) {
    return content_settings::PageSpecificContentSettings::GetForFrame(&rfh)
        ->IsInUse(
            content_settings::mojom::ContentSettingsType::SMART_CARD_GUARD);
  }

  content::SmartCardDelegate* GetSmartCardDelegate() {
    return content::GetContentClientForTesting()
        ->browser()
        ->GetSmartCardDelegate();
  }

  void GrantReaderPermission() {
    SmartCardPermissionContextFactory::GetForProfile(*profile())
        .GrantPersistentReaderPermission(app_frame_->GetLastCommittedOrigin(),
                                         kDummyReader);
  }

  void GrantEphemeralReaderPermission() {
    SmartCardPermissionContextFactory::GetForProfile(*profile())
        .GrantEphemeralReaderPermission(app_frame_->GetLastCommittedOrigin(),
                                        kDummyReader);
  }

  bool HasReaderPermission(const url::Origin& origin,
                           const std::string& reader) {
    return SmartCardPermissionContextFactory::GetForProfile(*profile())
        .HasReaderPermission(origin, reader);
  }

 protected:
  std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app_;
  raw_ptr<content::RenderFrameHost> app_frame_ = nullptr;

 private:
  base::test::ScopedFeatureList scoped_feature_list_{
      blink::features::kSmartCard};
};

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       NotifyConnectionUsed) {
  auto before = base::Time::Now();
  ASSERT_GT(before, GetLastUsed(*app_frame_));

  GetSmartCardDelegate()->NotifyConnectionUsed(*app_frame_);

  ASSERT_LE(before, GetLastUsed(*app_frame_));
  ASSERT_TRUE(IsInUse(*app_frame_));
}

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       NotifyLastConnectionLost) {
  ASSERT_FALSE(IsInUse(*app_frame_));

  GetSmartCardDelegate()->NotifyConnectionUsed(*app_frame_);
  ASSERT_TRUE(IsInUse(*app_frame_));

  GetSmartCardDelegate()->NotifyLastConnectionLost(*app_frame_);
  ASSERT_FALSE(IsInUse(*app_frame_));
}

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       ReconnectionInTheBackgroundImpossible) {
  GrantReaderPermission();
  auto& pscs = CHECK_DEREF(
      content_settings::PageSpecificContentSettings::GetForFrame(app_frame_));
  // Set the last used time past the reconnection deadline.
  //
  // Depending on real time in browser tests might not be generally a good idea,
  // but this one will be stable as long as test execution doesn't travel back
  // in time. If it does, we might have bigger problems than
  // instability here.
  pscs.set_last_used_time_for_testing(ContentSettingsType::SMART_CARD_GUARD,
                                      base::Time::Now() -

                                          base::Seconds(16));

  // Hiding takes focus from the window.
  content::WebContents::FromRenderFrameHost(app_frame_)
      ->GetTopLevelNativeWindow()
      ->Hide();

  // Window does not have focus and the last connection was more than
  // `kSmartCardAllowedReconnectTime` ago. Hence, connection is not possible.
  EXPECT_FALSE(
      GetSmartCardDelegate()->HasReaderPermission(*app_frame_, kDummyReader));
}

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       ReconnectionInTheBackgroundPossible) {
  GrantReaderPermission();
  auto& pscs = CHECK_DEREF(
      content_settings::PageSpecificContentSettings::GetForFrame(app_frame_));
  // Set the last used time within the reconnection deadline (in the future).
  //
  // Depending on real time in browser tests might not be generally a good idea,
  // but this one will be stable as long as test execution from here takes less
  // than a minute. If it takes longer, we might have bigger problems than
  // instability here.
  pscs.set_last_used_time_for_testing(ContentSettingsType::SMART_CARD_GUARD,
                                      base::Time::Now() + base::Minutes(1));

  // Hiding takes focus from the window.
  content::WebContents::FromRenderFrameHost(app_frame_)
      ->GetTopLevelNativeWindow()
      ->Hide();

  // Window does not have focus but the last connection was less than
  // `kSmartCardAllowedReconnectTime` ago (probably still in the future). Hence,
  // connection is deemed a reconnection and is possible.
  EXPECT_TRUE(
      GetSmartCardDelegate()->HasReaderPermission(*app_frame_, kDummyReader));
}

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       ReconnectionInTheForegroundPossible) {
  GrantReaderPermission();
  auto& pscs = CHECK_DEREF(
      content_settings::PageSpecificContentSettings::GetForFrame(app_frame_));
  // Set the last used time past the reconnection deadline.
  //
  // Depending on real time in browser tests might not be generally a good idea,
  // but this one will be stable as long as test execution doesn't travel back
  // in time. If it does, we might have bigger problems than
  // instability here.
  pscs.set_last_used_time_for_testing(ContentSettingsType::SMART_CARD_GUARD,
                                      base::Time::Now() - base::Seconds(16));

  // Long time from last connection should not have impact on connection
  // possibility as long as the window has focus.
  EXPECT_TRUE(
      GetSmartCardDelegate()->HasReaderPermission(*app_frame_, kDummyReader));
}

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       PermissionRequestInTheBackgroundImpossible) {
  GrantReaderPermission();

  // Hiding takes focus from the window.
  content::WebContents::FromRenderFrameHost(app_frame_)
      ->GetTopLevelNativeWindow()
      ->Hide();

  base::test::TestFuture<bool> got_permission;
  // This should immediately return false, not even trying to display any
  // prompts.
  GetSmartCardDelegate()->RequestReaderPermission(*app_frame_, kDummyReader,
                                                  got_permission.GetCallback());
  EXPECT_FALSE(got_permission.Get());
}

IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
                       ClosingWindowClearsEphemeralPermissions) {
  GrantEphemeralReaderPermission();
  const auto origin = app_frame_->GetLastCommittedOrigin();
  EXPECT_TRUE(HasReaderPermission(origin, kDummyReader));

  auto* tab_interface = tabs::TabInterface::GetFromContents(
      content::WebContents::FromRenderFrameHost(app_frame_));
  // Not resetting this causes dangling raw pointer error when closing tab.
  app_frame_ = nullptr;
  tab_interface->Close();

  EXPECT_FALSE(HasReaderPermission(origin, kDummyReader));
}

class ChromeOsSmartCardDelegateBrowserTestGuestMode
    : public web_app::IsolatedWebAppBrowserTestHarness {
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(ash::switches::kGuestSession);
    command_line->AppendSwitchASCII(ash::switches::kLoginUser,
                                    user_manager::kGuestUserName);
    command_line->AppendSwitchASCII(ash::switches::kLoginProfile,
                                    TestingProfile::kTestUserProfileDir);
    command_line->AppendSwitch(switches::kIncognito);
  }
};

// This is essentially just a sanity check of whether the
// SmartCardPermissionContext will be created successfully in guest mode.
// Probably to be deleted as a part of this: TODO(crbug.com/454152416)
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTestGuestMode,
                       GuestModeCreatePermissionContext) {
  ASSERT_NO_FATAL_FAILURE(
      SmartCardPermissionContextFactory::GetForProfile(*profile()););
}
