// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/testing/testing.h"

#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"

#include "flutter/shell/platform/common/accessibility_bridge.h"
#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
#include "flutter/third_party/accessibility/ax/ax_action_data.h"

namespace flutter::testing {

namespace {
// Returns a view controller configured for the text fixture resource configuration.
FlutterViewController* CreateTestViewController() {
  NSString* fixtures = @(testing::GetFixturesPath());
  FlutterDartProject* project = [[FlutterDartProject alloc]
      initWithAssetsPath:fixtures
             ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
  return [[FlutterViewController alloc] initWithProject:project];
}
}  // namespace

TEST(FlutterPlatformNodeDelegateMac, Basics) {
  FlutterViewController* viewController = CreateTestViewController();
  FlutterEngine* engine = viewController.engine;
  engine.semanticsEnabled = YES;
  auto bridge = viewController.accessibilityBridge.lock();
  // Initialize ax node data.
  FlutterSemanticsNode2 root;
  root.id = 0;
  root.flags = static_cast<FlutterSemanticsFlag>(0);
  ;
  root.actions = static_cast<FlutterSemanticsAction>(0);
  root.text_selection_base = -1;
  root.text_selection_extent = -1;
  root.label = "accessibility";
  root.hint = "";
  root.value = "";
  root.increased_value = "";
  root.decreased_value = "";
  root.tooltip = "";
  root.child_count = 0;
  root.custom_accessibility_actions_count = 0;
  bridge->AddFlutterSemanticsNodeUpdate(root);

  bridge->CommitUpdates();

  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
  // Verify the accessibility attribute matches.
  NSAccessibilityElement* native_accessibility =
      root_platform_node_delegate->GetNativeViewAccessible();
  std::string value = [native_accessibility.accessibilityValue UTF8String];
  EXPECT_TRUE(value == "accessibility");
  EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole);
  EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u);
  [engine shutDownEngine];
}

TEST(FlutterPlatformNodeDelegateMac, SelectableTextHasCorrectSemantics) {
  FlutterViewController* viewController = CreateTestViewController();
  FlutterEngine* engine = viewController.engine;
  engine.semanticsEnabled = YES;
  auto bridge = viewController.accessibilityBridge.lock();
  // Initialize ax node data.
  FlutterSemanticsNode2 root;
  root.id = 0;
  root.flags =
      static_cast<FlutterSemanticsFlag>(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
                                        FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
  root.actions = static_cast<FlutterSemanticsAction>(0);
  root.text_selection_base = 1;
  root.text_selection_extent = 3;
  root.label = "";
  root.hint = "";
  // Selectable text store its text in value
  root.value = "selectable text";
  root.increased_value = "";
  root.decreased_value = "";
  root.tooltip = "";
  root.child_count = 0;
  root.custom_accessibility_actions_count = 0;
  bridge->AddFlutterSemanticsNodeUpdate(root);

  bridge->CommitUpdates();

  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
  // Verify the accessibility attribute matches.
  NSAccessibilityElement* native_accessibility =
      root_platform_node_delegate->GetNativeViewAccessible();
  std::string value = [native_accessibility.accessibilityValue UTF8String];
  EXPECT_EQ(value, "selectable text");
  EXPECT_EQ(native_accessibility.accessibilityRole, NSAccessibilityStaticTextRole);
  EXPECT_EQ([native_accessibility.accessibilityChildren count], 0u);
  NSRange selection = native_accessibility.accessibilitySelectedTextRange;
  EXPECT_EQ(selection.location, 1u);
  EXPECT_EQ(selection.length, 2u);
  std::string selected_text = [native_accessibility.accessibilitySelectedText UTF8String];
  EXPECT_EQ(selected_text, "el");
}

TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRange) {
  FlutterViewController* viewController = CreateTestViewController();
  FlutterEngine* engine = viewController.engine;
  engine.semanticsEnabled = YES;
  auto bridge = viewController.accessibilityBridge.lock();
  // Initialize ax node data.
  FlutterSemanticsNode2 root;
  root.id = 0;
  root.flags =
      static_cast<FlutterSemanticsFlag>(FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
                                        FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly);
  root.actions = static_cast<FlutterSemanticsAction>(0);
  root.text_selection_base = -1;
  root.text_selection_extent = -1;
  root.label = "";
  root.hint = "";
  // Selectable text store its text in value
  root.value = "selectable text";
  root.increased_value = "";
  root.decreased_value = "";
  root.tooltip = "";
  root.child_count = 0;
  root.custom_accessibility_actions_count = 0;
  bridge->AddFlutterSemanticsNodeUpdate(root);

  bridge->CommitUpdates();

  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
  // Verify the accessibility attribute matches.
  NSAccessibilityElement* native_accessibility =
      root_platform_node_delegate->GetNativeViewAccessible();
  NSRange selection = native_accessibility.accessibilitySelectedTextRange;
  EXPECT_TRUE(selection.location == NSNotFound);
  EXPECT_EQ(selection.length, 0u);
}

// MOCK_ENGINE_PROC is leaky by design
// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)

TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) {
  FlutterViewController* viewController = CreateTestViewController();
  FlutterEngine* engine = viewController.engine;

  // Attach the view to a NSWindow.
  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
                                                 styleMask:NSBorderlessWindowMask
                                                   backing:NSBackingStoreBuffered
                                                     defer:NO];
  window.contentView = viewController.view;

  engine.semanticsEnabled = YES;
  auto bridge = viewController.accessibilityBridge.lock();
  // Initialize ax node data.
  FlutterSemanticsNode2 root;
  root.id = 0;
  root.label = "root";
  root.hint = "";
  root.value = "";
  root.increased_value = "";
  root.decreased_value = "";
  root.tooltip = "";
  root.child_count = 1;
  int32_t children[] = {1};
  root.children_in_traversal_order = children;
  root.custom_accessibility_actions_count = 0;
  bridge->AddFlutterSemanticsNodeUpdate(root);

  FlutterSemanticsNode2 child1;
  child1.id = 1;
  child1.label = "child 1";
  child1.hint = "";
  child1.value = "";
  child1.increased_value = "";
  child1.decreased_value = "";
  child1.tooltip = "";
  child1.child_count = 0;
  child1.custom_accessibility_actions_count = 0;
  bridge->AddFlutterSemanticsNodeUpdate(child1);

  bridge->CommitUpdates();

  auto root_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();

  // Set up embedder API mock.
  FlutterSemanticsAction called_action;
  uint64_t called_id;

  engine.embedderAPI.DispatchSemanticsAction = MOCK_ENGINE_PROC(
      DispatchSemanticsAction,
      ([&called_id, &called_action](auto engine, uint64_t id, FlutterSemanticsAction action,
                                    const uint8_t* data, size_t data_length) {
        called_id = id;
        called_action = action;
        return kSuccess;
      }));

  // Performs an AXAction.
  ui::AXActionData action_data;
  action_data.action = ax::mojom::Action::kDoDefault;
  root_platform_node_delegate->AccessibilityPerformAction(action_data);

  EXPECT_EQ(called_action, FlutterSemanticsAction::kFlutterSemanticsActionTap);
  EXPECT_EQ(called_id, 1u);

  [engine setViewController:nil];
  [engine shutDownEngine];
}

// NOLINTEND(clang-analyzer-core.StackAddressEscape)

TEST(FlutterPlatformNodeDelegateMac, TextFieldUsesFlutterTextField) {
  FlutterViewController* viewController = CreateTestViewController();
  FlutterEngine* engine = viewController.engine;
  [viewController loadView];

  // Unit test localization is unnecessary.
  // NOLINTNEXTLINE(clang-analyzer-optin.osx.cocoa.localizability.NonLocalizedStringChecker)
  viewController.textInputPlugin.string = @"textfield";
  // Creates a NSWindow so that the native text field can become first responder.
  NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
                                                 styleMask:NSBorderlessWindowMask
                                                   backing:NSBackingStoreBuffered
                                                     defer:NO];
  window.contentView = viewController.view;
  engine.semanticsEnabled = YES;

  auto bridge = viewController.accessibilityBridge.lock();
  // Initialize ax node data.
  FlutterSemanticsNode2 root;
  root.id = 0;
  root.flags = static_cast<FlutterSemanticsFlag>(0);
  root.actions = static_cast<FlutterSemanticsAction>(0);
  root.label = "root";
  root.hint = "";
  root.value = "";
  root.increased_value = "";
  root.decreased_value = "";
  root.tooltip = "";
  root.child_count = 1;
  int32_t children[] = {1};
  root.children_in_traversal_order = children;
  root.custom_accessibility_actions_count = 0;
  root.rect = {0, 0, 100, 100};  // LTRB
  root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
  bridge->AddFlutterSemanticsNodeUpdate(root);

  double rectSize = 50;
  double transformFactor = 0.5;

  FlutterSemanticsNode2 child1;
  child1.id = 1;
  child1.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField;
  child1.actions = static_cast<FlutterSemanticsAction>(0);
  child1.label = "";
  child1.hint = "";
  child1.value = "textfield";
  child1.increased_value = "";
  child1.decreased_value = "";
  child1.tooltip = "";
  child1.text_selection_base = -1;
  child1.text_selection_extent = -1;
  child1.child_count = 0;
  child1.custom_accessibility_actions_count = 0;
  child1.rect = {0, 0, rectSize, rectSize};  // LTRB
  child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1};
  bridge->AddFlutterSemanticsNodeUpdate(child1);

  bridge->CommitUpdates();

  auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
  // Verify the accessibility attribute matches.
  id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
  EXPECT_EQ([native_accessibility isKindOfClass:[FlutterTextField class]], YES);
  FlutterTextField* native_text_field = (FlutterTextField*)native_accessibility;

  NSView* view = viewController.flutterView;
  CGRect scaledBounds = [view convertRectToBacking:view.bounds];
  CGSize scaledSize = scaledBounds.size;
  double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width;

  double expectedFrameSize = rectSize * transformFactor / pixelRatio;
  EXPECT_EQ(NSEqualRects(native_text_field.frame, NSMakeRect(0, 600 - expectedFrameSize,
                                                             expectedFrameSize, expectedFrameSize)),
            YES);

  [native_text_field startEditing];
  EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES);
}

}  // namespace flutter::testing
