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

#include "ash/login/ui/access_code_input.h"

#include <memory>
#include <optional>
#include <string>

#include "ash/strings/grit/ash_strings.h"
#include "ash/test/ash_test_base.h"
#include "base/strings/string_number_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/textfield/textfield.h"

namespace ash {

namespace {
const int fixed_pin_length = 6;
}

class FixedLengthCodeInputTest : public AshTestBase {
 public:
  FixedLengthCodeInputTest() = default;
  FixedLengthCodeInputTest(const FixedLengthCodeInputTest&) = delete;
  FixedLengthCodeInputTest& operator=(const FixedLengthCodeInputTest&) = delete;
  ~FixedLengthCodeInputTest() override = default;

 protected:
  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    input_view_ = std::make_unique<FixedLengthCodeInput>(
        fixed_pin_length,
        base::BindRepeating(&FixedLengthCodeInputTest::OnInputChange,
                            base::Unretained(this)),
        base::BindRepeating(&FixedLengthCodeInputTest::OnEnter,
                            base::Unretained(this)),
        base::BindRepeating(&FixedLengthCodeInputTest::OnEscape,
                            base::Unretained(this)),
        /*obscure_pin=*/false);
    obscure_input_view_ = std::make_unique<FixedLengthCodeInput>(
        fixed_pin_length,
        base::BindRepeating(&FixedLengthCodeInputTest::OnInputChange,
                            base::Unretained(this)),
        base::BindRepeating(&FixedLengthCodeInputTest::OnEnter,
                            base::Unretained(this)),
        base::BindRepeating(&FixedLengthCodeInputTest::OnEscape,
                            base::Unretained(this)),
        /*obscure_pin=*/true);
  }

  void TearDown() override { AshTestBase::TearDown(); }

  void OnInputChange(bool last_field_active, bool complete) {
    ++on_input_change_count;
    if (complete) {
      ++on_input_change_complete_count;
    }
  }

  void OnEnter() { ++on_enter_count; }

  void OnEscape() { ++on_escape_count; }

  std::unique_ptr<FixedLengthCodeInput> input_view_;
  std::unique_ptr<FixedLengthCodeInput> obscure_input_view_;

  int on_input_change_count = 0;
  int on_input_change_complete_count = 0;
  int on_enter_count = 0;
  int on_escape_count = 0;
};

// Validates that the FixedLengthCodeInput::ContentsChanged() method handles
// correctly when the Textfield::InsertText() method is called with a digit.
TEST_F(FixedLengthCodeInputTest, ContentsChangedWithDigits) {
  FixedLengthCodeInput::TestApi test_api(input_view_.get());
  for (int index = 1; index <= fixed_pin_length; ++index) {
    int active_index = test_api.GetActiveIndex();
    EXPECT_EQ(active_index + 1, index);
    views::Textfield* textfield = test_api.GetInputTextField(active_index);
    textfield->InsertText(
        base::NumberToString16(index),
        ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
    EXPECT_EQ(on_input_change_count, index);
    EXPECT_EQ(on_input_change_complete_count,
              (index == fixed_pin_length ? 1 : 0));
  }
  std::optional<std::string> code = test_api.GetCode();
  EXPECT_TRUE(code.has_value());
  EXPECT_EQ(code.value(), "123456");
  EXPECT_EQ(on_enter_count, 0);
  EXPECT_EQ(on_escape_count, 0);
}

TEST_F(FixedLengthCodeInputTest, AccessibleProperties) {
  ui::AXNodeData data;

  input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_FALSE(data.HasState(ax::mojom::State::kProtected));
  EXPECT_EQ(data.role, ax::mojom::Role::kTextField);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            l10n_util::GetStringUTF16(
                IDS_ASH_LOGIN_PARENT_ACCESS_GENERIC_DESCRIPTION));

  data = ui::AXNodeData();
  obscure_input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_TRUE(data.HasState(ax::mojom::State::kProtected));

  std::unique_ptr<AccessibleInputField> field =
      std::make_unique<AccessibleInputField>();
  data = ui::AXNodeData();
  field->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName), u"");
}

TEST_F(FixedLengthCodeInputTest, AccessibilityTextSelectionBound) {
  ui::AXNodeData data;

  input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart), 0);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd), 0);

  input_view_->InsertDigit(4);
  input_view_->InsertDigit(4);
  input_view_->InsertDigit(4);
  data = ui::AXNodeData();
  input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart), 3);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd), 3);

  input_view_->Backspace();
  data = ui::AXNodeData();
  input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart), 2);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd), 2);

  input_view_->ClearInput();
  data = ui::AXNodeData();
  input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart), 0);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd), 1);

  input_view_->InsertDigit(4);
  data = ui::AXNodeData();
  input_view_->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart), 1);
  EXPECT_EQ(data.GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd), 1);
}

// Validates that the FixedLengthCodeInput::ContentsChanged() method handles
// and ignores correctly when the Textfield::InsertText() method is called
// with multipledigit.
TEST_F(FixedLengthCodeInputTest, ContentsChangedWithMultipleDigits) {
  FixedLengthCodeInput::TestApi test_api(input_view_.get());
  int active_index = test_api.GetActiveIndex();
  EXPECT_EQ(active_index, 0);
  EXPECT_EQ(on_input_change_count, 0);

  auto CheckInsertIgnored = [&]() {
    std::optional<std::string> code = test_api.GetCode();
    EXPECT_FALSE(code.has_value());
    EXPECT_EQ(test_api.GetActiveIndex(), 0);
    EXPECT_EQ(on_input_change_count, 0);
    EXPECT_EQ(on_enter_count, 0);
    EXPECT_EQ(on_escape_count, 0);
  };

  views::Textfield* textfield = test_api.GetInputTextField(active_index);
  textfield->InsertText(
      u"01",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();

  textfield->InsertText(
      u"12",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();

  textfield->InsertText(
      u"004",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();

  textfield->InsertText(
      u"987",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();
}

// Validates that the FixedLengthCodeInput::ContentsChanged() method handles
// and ignores correctly when the Textfield::InsertText() method is called
// with non numerical strings.
TEST_F(FixedLengthCodeInputTest, ContentsChangedWithNonNumericalStrings) {
  FixedLengthCodeInput::TestApi test_api(input_view_.get());

  int active_index = test_api.GetActiveIndex();
  EXPECT_EQ(active_index, 0);
  EXPECT_EQ(on_input_change_count, 0);

  auto CheckInsertIgnored = [&]() {
    std::optional<std::string> code = test_api.GetCode();
    EXPECT_FALSE(code.has_value());
    EXPECT_EQ(test_api.GetActiveIndex(), 0);
    EXPECT_EQ(on_input_change_count, 0);
    EXPECT_EQ(on_enter_count, 0);
    EXPECT_EQ(on_escape_count, 0);
  };

  views::Textfield* textfield = test_api.GetInputTextField(active_index);
  textfield->InsertText(
      u"a",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();

  textfield->InsertText(
      u"xz",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();

  textfield->InsertText(
      u"/",
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();

  textfield->InsertText(
      u"", ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  CheckInsertIgnored();
}

}  // namespace ash
