// 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.

#include "third_party/blink/renderer/core/dom/focusgroup_flags.h"

#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_table_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"

namespace blink::focusgroup {

class FocusgroupFlagsTest : public SimTest {
 protected:
  void SetUp() override { SimTest::SetUp(); }

  Vector<String>& RawConsoleMessages() { return ConsoleMessages(); }

  void ClearConsoleMessages() { RawConsoleMessages().clear(); }

  Vector<String> CopyConsoleMessages() { return RawConsoleMessages(); }
};

TEST_F(FocusgroupFlagsTest, EmptyAttributeGeneratesError) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString(""));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kNoBehavior);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);

  auto messages = CopyConsoleMessages();
  ASSERT_EQ(messages.size(), 1u);
  EXPECT_TRUE(
      messages[0].contains("focusgroup requires a recognized behavior token"));
}

TEST_F(FocusgroupFlagsTest, InvalidFirstTokenGeneratesError) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("invalid"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kNoBehavior);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);

  auto messages = CopyConsoleMessages();
  ASSERT_EQ(messages.size(), 1u);
  EXPECT_TRUE(
      messages[0].contains("focusgroup requires a recognized behavior token"));
}

TEST_F(FocusgroupFlagsTest, MultipleBehaviorTokensGenerateWarning) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar tablist"));

  // Toolbar is the first recognized behavior token. Tablist is a second
  // behavior token and is reported as unrecognized.
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline);

  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("Unrecognized focusgroup attribute values"));
  EXPECT_TRUE(messages[0].contains("tablist"));
}

TEST_F(FocusgroupFlagsTest, UnknownTokenGeneratesWarning) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar unknown"));
  // Toolbar defaults to inline-only axis.
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline);
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("Unrecognized focusgroup attribute values"));
  EXPECT_TRUE(messages[0].contains("unknown"));
  EXPECT_TRUE(messages[0].contains("Valid tokens are"));
}

TEST_F(FocusgroupFlagsTest, NoneWithOtherTokensSilentlyOptsOut) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("none inline"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kOptOut);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);

  auto messages = CopyConsoleMessages();
  EXPECT_EQ(messages.size(), 0u);
}

TEST_F(FocusgroupFlagsTest, RedundantInlineBlockGeneratesWarning) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Redundancy warning only fires for behaviors without a default axis.
  // Radiogroup has no default axis, so explicit inline+block is redundant.
  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("radiogroup inline block"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kRadiogroup);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline | FocusgroupFlags::kBlock |
                              FocusgroupFlags::kWrapInline |
                              FocusgroupFlags::kWrapBlock);
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("redundant"));
  EXPECT_TRUE(messages[0].contains("default behavior for linear focusgroups"));

  // Toolbar has a default axis (inline), so inline+block is NOT redundant;
  // it is an explicit override adding the block axis.
  ClearConsoleMessages();
  FocusgroupData toolbar_result =
      ParseFocusgroup(element, AtomicString("toolbar inline block"));

  EXPECT_EQ(toolbar_result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(toolbar_result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kBlock);
  auto toolbar_messages = CopyConsoleMessages();
  EXPECT_EQ(toolbar_messages.size(), 0u);
}

TEST_F(FocusgroupFlagsTest, InvalidAxisForGridGeneratesError) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLTableElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("grid inline"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kGrid);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);

  auto messages = CopyConsoleMessages();
  ASSERT_EQ(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("not valid for grid focusgroups"));
  EXPECT_TRUE(messages[0].contains("row-wrap/col-wrap or flow modifiers"));
}

TEST_F(FocusgroupFlagsTest, GridBehaviorRequiresFocusgroupV2) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(false);

  auto* element = MakeGarbageCollected<HTMLTableElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  FocusgroupData result = ParseFocusgroup(element, AtomicString("grid"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kNoBehavior);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);
}

TEST_F(FocusgroupFlagsTest, GridTokensOnLinearGenerateError) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar row-wrap"));

  // Linear focusgroup uses toolbar's default axis (inline); row-wrap ignored
  // with warning.
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline);
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("only valid for grid focusgroups"));
  EXPECT_TRUE(messages[0].contains("use 'wrap' for linear focusgroups"));
}

TEST_F(FocusgroupFlagsTest, GridTokenSupportRequiresFocusgroupV2) {
  ScopedFocusgroupV2ForTest v2_scope(false);

  EXPECT_FALSE(IsValidFocusgroupToken(AtomicString("grid")));
  EXPECT_FALSE(IsValidFocusgroupToken(AtomicString("row-wrap")));
  EXPECT_FALSE(IsValidFocusgroupToken(AtomicString("flow")));

  {
    ScopedFocusgroupV2ForTest v2_enabled(true);
    EXPECT_TRUE(IsValidFocusgroupToken(AtomicString("grid")));
    EXPECT_TRUE(IsValidFocusgroupToken(AtomicString("row-wrap")));
    EXPECT_TRUE(IsValidFocusgroupToken(AtomicString("flow")));
  }
}

TEST_F(FocusgroupFlagsTest, NowrapAlone) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar nowrap"));

  // Toolbar defaults to inline axis; nowrap suppresses any wrap.
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline);
  EXPECT_EQ(CopyConsoleMessages().size(), 0u);
}

TEST_F(FocusgroupFlagsTest, WrapAndNowrapConflict) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar wrap nowrap"));

  // wrap + nowrap is an author error; both are ignored.
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline);
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("author error"));
}

TEST_F(FocusgroupFlagsTest, NowrapOnGrid) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLTableElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("grid nowrap"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kGrid);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("nowrap"));
  EXPECT_TRUE(messages[0].contains("not valid for grid"));
}

TEST_F(FocusgroupFlagsTest, NowrapOverridesDefaultWrap) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Tablist defaults to inline + wrap.  nowrap suppresses the default wrap.
  ClearConsoleMessages();
  FocusgroupData tablist_result =
      ParseFocusgroup(element, AtomicString("tablist nowrap"));
  EXPECT_EQ(tablist_result.behavior, FocusgroupBehavior::kTablist);
  EXPECT_EQ(tablist_result.flags, FocusgroupFlags::kInline);

  // Menu defaults to block + wrap.  nowrap suppresses the default wrap.
  ClearConsoleMessages();
  FocusgroupData menu_result =
      ParseFocusgroup(element, AtomicString("menu nowrap"));
  EXPECT_EQ(menu_result.behavior, FocusgroupBehavior::kMenu);
  EXPECT_EQ(menu_result.flags, FocusgroupFlags::kBlock);
}

TEST_F(FocusgroupFlagsTest, ToolbarDefaults) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Bare toolbar defaults to inline only, no wrap.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("toolbar"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline);

  // Explicit block overrides default inline.
  ClearConsoleMessages();
  FocusgroupData block_result =
      ParseFocusgroup(element, AtomicString("toolbar block"));
  EXPECT_EQ(block_result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(block_result.flags, FocusgroupFlags::kBlock);
}

TEST_F(FocusgroupFlagsTest, TablistDefaults) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Bare tablist defaults to inline + wrap-inline.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("tablist"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kTablist);
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kWrapInline);

  // Explicit block overrides default inline; default wrap applies in block.
  ClearConsoleMessages();
  FocusgroupData block_result =
      ParseFocusgroup(element, AtomicString("tablist block"));
  EXPECT_EQ(block_result.behavior, FocusgroupBehavior::kTablist);
  EXPECT_EQ(block_result.flags,
            FocusgroupFlags::kBlock | FocusgroupFlags::kWrapBlock);

  // nowrap suppresses default wrap.
  ClearConsoleMessages();
  FocusgroupData nowrap_result =
      ParseFocusgroup(element, AtomicString("tablist nowrap"));
  EXPECT_EQ(nowrap_result.behavior, FocusgroupBehavior::kTablist);
  EXPECT_EQ(nowrap_result.flags, FocusgroupFlags::kInline);
}

TEST_F(FocusgroupFlagsTest, MenuDefaults) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Bare menu defaults to block + wrap-block.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("menu"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kMenu);
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kBlock | FocusgroupFlags::kWrapBlock);

  // Explicit inline overrides default block; default wrap applies in inline.
  ClearConsoleMessages();
  FocusgroupData inline_result =
      ParseFocusgroup(element, AtomicString("menu inline"));
  EXPECT_EQ(inline_result.behavior, FocusgroupBehavior::kMenu);
  EXPECT_EQ(inline_result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kWrapInline);
}

TEST_F(FocusgroupFlagsTest, MenubarDefaults) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Bare menubar defaults to inline + wrap-inline.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("menubar"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kMenubar);
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kWrapInline);

  // Explicit block overrides default inline; default wrap applies in block.
  ClearConsoleMessages();
  FocusgroupData block_result =
      ParseFocusgroup(element, AtomicString("menubar block"));
  EXPECT_EQ(block_result.behavior, FocusgroupBehavior::kMenubar);
  EXPECT_EQ(block_result.flags,
            FocusgroupFlags::kBlock | FocusgroupFlags::kWrapBlock);
}

TEST_F(FocusgroupFlagsTest, RadiogroupDefaultsWrap) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Radiogroup defaults to wrap (both axes, wrap in both).
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("radiogroup"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kRadiogroup);
  EXPECT_EQ(result.flags, FocusgroupFlags::kInline | FocusgroupFlags::kBlock |
                              FocusgroupFlags::kWrapInline |
                              FocusgroupFlags::kWrapBlock);
}

TEST_F(FocusgroupFlagsTest, ListboxDefaultsBlock) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Listbox defaults to block axis only, no wrap.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("listbox"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kListbox);
  EXPECT_EQ(result.flags, FocusgroupFlags::kBlock);
}

TEST_F(FocusgroupFlagsTest, ValidTokenListStringIncludesNowrap) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(false);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Trigger an unrecognized-token warning so the console message includes the
  // full valid token list produced by ValidTokenListString().
  ClearConsoleMessages();
  ParseFocusgroup(element, AtomicString("toolbar bogus"));
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);

  // Without the grid feature, grid-only tokens are filtered out. The expected
  // list is: inline, block, wrap, nomemory, nowrap.
  EXPECT_TRUE(messages[0].contains("inline"));
  EXPECT_TRUE(messages[0].contains("block"));
  EXPECT_TRUE(messages[0].contains("wrap"));
  EXPECT_TRUE(messages[0].contains("nomemory"));
  EXPECT_TRUE(messages[0].contains("nowrap"));

  // Grid-only tokens must not appear.
  EXPECT_FALSE(messages[0].contains("row-wrap"));
  EXPECT_FALSE(messages[0].contains("col-wrap"));
  EXPECT_FALSE(messages[0].contains("row-flow"));
  EXPECT_FALSE(messages[0].contains("col-flow"));
}

TEST_F(FocusgroupFlagsTest, ValidTokenListStringIncludesGridTokens) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // With grid enabled, use a grid behavior so grid-only tokens are not warned
  // individually. Trigger unrecognized-token warning for the full list.
  ClearConsoleMessages();
  ParseFocusgroup(element, AtomicString("toolbar bogus"));
  auto messages = CopyConsoleMessages();
  ASSERT_GE(messages.size(), 1u);

  // All tokens including grid-only should appear.
  EXPECT_TRUE(messages[0].contains("row-wrap"));
  EXPECT_TRUE(messages[0].contains("col-wrap"));
  EXPECT_TRUE(messages[0].contains("flow"));
  EXPECT_TRUE(messages[0].contains("row-flow"));
  EXPECT_TRUE(messages[0].contains("col-flow"));
  EXPECT_TRUE(messages[0].contains("nowrap"));
}

TEST_F(FocusgroupFlagsTest, NomemoryModifierSetsFlag) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar nomemory"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_TRUE(result.flags & FocusgroupFlags::kNoMemory);
  // Toolbar defaults to inline-only axis.
  EXPECT_TRUE(result.flags & FocusgroupFlags::kInline);
  EXPECT_FALSE(result.flags & FocusgroupFlags::kBlock);

  auto messages = CopyConsoleMessages();
  EXPECT_EQ(messages.size(), 0u);
}

TEST_F(FocusgroupFlagsTest, NoneAnywhereOptsOut) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // "none" after a behavior token still opts out.
  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar none"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kOptOut);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);
  auto messages = CopyConsoleMessages();
  EXPECT_EQ(messages.size(), 0u);

  // "none" before a behavior token also opts out.
  ClearConsoleMessages();
  FocusgroupData result2 =
      ParseFocusgroup(element, AtomicString("none toolbar"));
  EXPECT_EQ(result2.behavior, FocusgroupBehavior::kOptOut);
  EXPECT_EQ(result2.flags, FocusgroupFlags::kNone);

  // "none" between modifiers opts out.
  ClearConsoleMessages();
  FocusgroupData result3 =
      ParseFocusgroup(element, AtomicString("toolbar wrap none block"));
  EXPECT_EQ(result3.behavior, FocusgroupBehavior::kOptOut);
  EXPECT_EQ(result3.flags, FocusgroupFlags::kNone);
}

TEST_F(FocusgroupFlagsTest, NoneAnywhereWithGridDisabled) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(false);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // "grid none" should opt out even though grid is disabled. The none check
  // must short-circuit before grid validation.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("grid none"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kOptOut);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);
}

TEST_F(FocusgroupFlagsTest, BehaviorTokenAnywhere) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Behavior token not at position 0: modifier before behavior.
  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("wrap toolbar"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  // Toolbar default is inline; explicit wrap applies to inline axis.
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kWrapInline);
  EXPECT_EQ(CopyConsoleMessages().size(), 0u);

  // Unrecognized token before behavior: behavior still found.
  ClearConsoleMessages();
  FocusgroupData result2 =
      ParseFocusgroup(element, AtomicString("invalid toolbar"));
  EXPECT_EQ(result2.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result2.flags, FocusgroupFlags::kInline);
  auto messages2 = CopyConsoleMessages();
  ASSERT_GE(messages2.size(), 1u);
  EXPECT_TRUE(messages2[0].contains("Unrecognized"));
  EXPECT_TRUE(messages2[0].contains("invalid"));

  // Multiple modifiers around a behavior token.
  ClearConsoleMessages();
  FocusgroupData result3 =
      ParseFocusgroup(element, AtomicString("wrap tablist nomemory"));
  EXPECT_EQ(result3.behavior, FocusgroupBehavior::kTablist);
  // Tablist default is inline+wrap; explicit wrap still applies, nomemory set.
  EXPECT_TRUE(result3.flags & FocusgroupFlags::kInline);
  EXPECT_TRUE(result3.flags & FocusgroupFlags::kWrapInline);
  EXPECT_TRUE(result3.flags & FocusgroupFlags::kNoMemory);
}

TEST_F(FocusgroupFlagsTest, NoBehaviorTokenIsError) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Only modifiers, no behavior token: error, no focusgroup.
  ClearConsoleMessages();
  FocusgroupData result = ParseFocusgroup(element, AtomicString("wrap block"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kNoBehavior);
  EXPECT_EQ(result.flags, FocusgroupFlags::kNone);
  auto messages = CopyConsoleMessages();
  ASSERT_EQ(messages.size(), 1u);
  EXPECT_TRUE(
      messages[0].contains("focusgroup requires a recognized behavior token"));

  // Single modifier, no behavior.
  ClearConsoleMessages();
  FocusgroupData result2 = ParseFocusgroup(element, AtomicString("nomemory"));
  EXPECT_EQ(result2.behavior, FocusgroupBehavior::kNoBehavior);
  EXPECT_EQ(result2.flags, FocusgroupFlags::kNone);
  auto messages2 = CopyConsoleMessages();
  ASSERT_EQ(messages2.size(), 1u);
  EXPECT_TRUE(
      messages2[0].contains("focusgroup requires a recognized behavior token"));
}

TEST_F(FocusgroupFlagsTest, CaseInsensitiveParsing) {
  ScopedFocusgroupForTest focusgroup_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  // Mixed-case tokens should be recognized.
  ClearConsoleMessages();
  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("Toolbar Wrap"));
  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kWrapInline);
  EXPECT_EQ(CopyConsoleMessages().size(), 0u);

  // ALL CAPS.
  ClearConsoleMessages();
  FocusgroupData result2 = ParseFocusgroup(element, AtomicString("NONE"));
  EXPECT_EQ(result2.behavior, FocusgroupBehavior::kOptOut);
  EXPECT_EQ(result2.flags, FocusgroupFlags::kNone);
}

TEST_F(FocusgroupFlagsTest, FeedDefaultsToBlockItemControls) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  FocusgroupData result = ParseFocusgroup(element, AtomicString("feed"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kFeed);
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kBlock | FocusgroupFlags::kItemControls);
}

TEST_F(FocusgroupFlagsTest, FeedNoItemControlsSuppressesDefault) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("feed noitemcontrols"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kFeed);
  EXPECT_EQ(result.flags, FocusgroupFlags::kBlock);
}

TEST_F(FocusgroupFlagsTest, ItemControlsAppliesToLinearBehavior) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("toolbar itemcontrols"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kToolbar);
  EXPECT_EQ(result.flags,
            FocusgroupFlags::kInline | FocusgroupFlags::kItemControls);
}

TEST_F(FocusgroupFlagsTest, NoItemControlsTakesPrecedence) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);
  ClearConsoleMessages();

  FocusgroupData result = ParseFocusgroup(
      element, AtomicString("feed itemcontrols noitemcontrols"));

  EXPECT_EQ(result.behavior, FocusgroupBehavior::kFeed);
  EXPECT_EQ(result.flags, FocusgroupFlags::kBlock);
  auto messages = CopyConsoleMessages();
  ASSERT_EQ(messages.size(), 1u);
  EXPECT_TRUE(messages[0].contains("author error"));
  EXPECT_TRUE(messages[0].contains("noitemcontrols takes precedence"));
}

TEST_F(FocusgroupFlagsTest, V2TokensAreUnsupportedWhenDisabled) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(false);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  EXPECT_EQ(ParseFocusgroup(element, AtomicString("feed")), FocusgroupData());
  EXPECT_EQ(
      ParseFocusgroup(element,
                      AtomicString("toolbar itemcontrols noitemcontrols")),
      FocusgroupData(FocusgroupBehavior::kToolbar, FocusgroupFlags::kInline));
  EXPECT_FALSE(IsValidFocusgroupToken(AtomicString("feed")));
  EXPECT_FALSE(IsValidFocusgroupToken(AtomicString("itemcontrols")));
  EXPECT_FALSE(IsValidFocusgroupToken(AtomicString("noitemcontrols")));
}

TEST_F(FocusgroupFlagsTest, DisabledFeedDoesNotMaskValidBehavior) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(false);

  auto* element = MakeGarbageCollected<HTMLDivElement>(GetDocument());
  GetDocument().body()->appendChild(element);

  FocusgroupData result =
      ParseFocusgroup(element, AtomicString("feed toolbar"));

  EXPECT_EQ(result, FocusgroupData(FocusgroupBehavior::kToolbar,
                                   FocusgroupFlags::kInline));
}

TEST_F(FocusgroupFlagsTest, V2TokenSupport) {
  ScopedFocusgroupForTest focusgroup_scope(true);
  ScopedFocusgroupV2ForTest v2_scope(true);

  EXPECT_TRUE(IsValidFocusgroupToken(AtomicString("feed")));
  EXPECT_TRUE(IsValidFocusgroupToken(AtomicString("itemcontrols")));
  EXPECT_TRUE(IsValidFocusgroupToken(AtomicString("noitemcontrols")));
}

}  // namespace blink::focusgroup
