chromium/ios/chrome/browser/shared/model/web_state_list/test/web_state_list_builder_from_description.mm

// 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/shared/model/web_state_list/test/web_state_list_builder_from_description.h"

#import <queue>
#import <ranges>
#import <sstream>

#import "base/strings/string_split.h"
#import "base/strings/string_util.h"
#import "components/tab_groups/tab_group_id.h"
#import "components/tab_groups/tab_group_visual_data.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"

namespace {

// Different types of token in a description.
enum class TokenType {
  kInvalid,
  kWebStateIdentifier,
  kWebStateActivation,
  kPinnedWebStatesSeparator,
  kTabGroupOpeningBracket,
  kTabGroupIdentifier,
  kTabGroupClosingBracket,
  kMaxValue = kTabGroupClosingBracket,
};

// Returns the type of token associated with a character.
std::optional<TokenType> GetTokenTypeForCharacter(char character) {
  if (base::IsAsciiAlpha(character)) {
    return TokenType::kWebStateIdentifier;
  }
  if (base::IsAsciiDigit(character)) {
    return TokenType::kTabGroupIdentifier;
  }
  switch (character) {
    case ' ':
      return std::nullopt;
    case '|':
      return TokenType::kPinnedWebStatesSeparator;
    case '*':
      return TokenType::kWebStateActivation;
    case '[':
      return TokenType::kTabGroupOpeningBracket;
    case ']':
      return TokenType::kTabGroupClosingBracket;
    default:
      return TokenType::kInvalid;
  }
}

// A WebStateList description token.
struct Token {
  TokenType type;
  char character;
};

// Returns a list of tokens from `description`. Characters which cannot be
// associated with a token are simply ignored.
std::vector<Token> TokenizeWebStateListDescription(
    std::string_view description) {
  std::vector<Token> tokens;
  tokens.reserve(description.size());
  for (char character : description) {
    std::optional<TokenType> token_type = GetTokenTypeForCharacter(character);
    if (token_type) {
      tokens.push_back({.type = *token_type, .character = character});
    }
  }
  return tokens;
}

// Creates a fake WebState with navigation items.
std::unique_ptr<web::WebState> CreateWebState(
    ChromeBrowserState* browser_state) {
  const GURL url = GURL(kChromeUIVersionURL);
  auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
  navigation_manager->AddItem(url, ui::PAGE_TRANSITION_TYPED);

  auto web_state =
      std::make_unique<web::FakeWebState>(web::WebStateID::NewUnique());
  web_state->SetNavigationManager(std::move(navigation_manager));
  web_state->SetNavigationItemCount(1);
  web_state->SetVisibleURL(url);
  web_state->SetBrowserState(browser_state);
  web_state->SetWebFramesManager(web::ContentWorld::kAllContentWorlds,
                                 std::make_unique<web::FakeWebFramesManager>());
  web_state->SetWebFramesManager(web::ContentWorld::kPageContentWorld,
                                 std::make_unique<web::FakeWebFramesManager>());
  web_state->SetWebFramesManager(web::ContentWorld::kIsolatedWorld,
                                 std::make_unique<web::FakeWebFramesManager>());
  return web_state;
}

}  // namespace

WebStateListBuilderFromDescription::WebStateListBuilderFromDescription(
    WebStateList* web_state_list)
    : web_state_list_(web_state_list) {
  DCHECK(web_state_list_);
  DCHECK(web_state_list_->empty());
  web_state_list_->AddObserver(this);
}

WebStateListBuilderFromDescription::~WebStateListBuilderFromDescription() {
  web_state_list_->RemoveObserver(this);
}

bool WebStateListBuilderFromDescription::BuildWebStateListFromDescription(
    std::string_view description,
    ChromeBrowserState* browser_state) {
  return BuildWebStateListFromDescription(
      description,
      base::BindRepeating(CreateWebState, base::Unretained(browser_state)));
}

bool WebStateListBuilderFromDescription::BuildWebStateListFromDescription(
    std::string_view description,
    base::RepeatingCallback<std::unique_ptr<web::WebState>()>
        create_web_state) {
  if (!web_state_list_->empty()) {
    return false;
  }

  bool parsing_pinned_web_states = true;
  char identifier_for_current_tab_group = 0;
  std::set<int> indices_for_current_tab_group;

  const std::vector<Token> tokens =
      TokenizeWebStateListDescription(description);
  for (int i = 0; i < static_cast<int>(tokens.size()); ++i) {
    const Token token = tokens[i];
    const std::optional<Token> prev_token =
        i - 1 >= 0 ? std::optional(tokens[i - 1]) : std::nullopt;
    const std::optional<Token> next_token =
        i + 1 < static_cast<int>(tokens.size()) ? std::optional(tokens[i + 1])
                                                : std::nullopt;
    switch (token.type) {
      case TokenType::kInvalid:
        return false;

      case TokenType::kWebStateIdentifier: {
        if (GetWebStateForIdentifier(token.character)) {
          return false;
        }
        auto web_state = create_web_state.Run();
        SetWebStateIdentifier(web_state.get(), token.character);
        web_state_list_->InsertWebState(
            std::move(web_state),
            WebStateList::InsertionParams::AtIndex(web_state_list_->count())
                .Pinned(parsing_pinned_web_states));
        if (identifier_for_current_tab_group) {
          indices_for_current_tab_group.insert(web_state_list_->count() - 1);
        }
        break;
      }

      case TokenType::kWebStateActivation: {
        if (web_state_list_->GetActiveWebState() != nullptr ||
            web_state_list_->count() == 0 || !prev_token ||
            prev_token->type != TokenType::kWebStateIdentifier) {
          return false;
        }
        web_state_list_->ActivateWebStateAt(web_state_list_->count() - 1);
        break;
      }

      case TokenType::kPinnedWebStatesSeparator: {
        if (!parsing_pinned_web_states) {
          return false;
        }
        parsing_pinned_web_states = false;
        break;
      }

      case TokenType::kTabGroupOpeningBracket: {
        if (identifier_for_current_tab_group || parsing_pinned_web_states ||
            !next_token || next_token->type != TokenType::kTabGroupIdentifier ||
            GetTabGroupForIdentifier(next_token->character)) {
          return false;
        }
        identifier_for_current_tab_group = next_token->character;
        indices_for_current_tab_group.clear();
        break;
      }

      case TokenType::kTabGroupIdentifier: {
        break;
      }

      case TokenType::kTabGroupClosingBracket: {
        if (!identifier_for_current_tab_group ||
            indices_for_current_tab_group.empty()) {
          return false;
        }
        const TabGroup* created_group = web_state_list_->CreateGroup(
            std::move(indices_for_current_tab_group),
            tab_groups::TabGroupVisualData(),
            tab_groups::TabGroupId::GenerateNew());
        SetTabGroupIdentifier(created_group, identifier_for_current_tab_group);
        identifier_for_current_tab_group = 0;
        break;
      }
    }
  }

  if (parsing_pinned_web_states || identifier_for_current_tab_group) {
    return false;
  }

  return true;
}

std::string WebStateListBuilderFromDescription::GetWebStateListDescription()
    const {
  if (web_state_list_->empty()) {
    return "|";
  }

  std::ostringstream oss;
  bool pinned_web_states_separator_added = false;
  for (int i = 0; i < web_state_list_->count(); ++i) {
    WebState* web_state = web_state_list_->GetWebStateAt(i);
    const TabGroup* tab_group = web_state_list_->GetGroupOfWebStateAt(i);
    WebState* prev_web_state = web_state_list_->ContainsIndex(i - 1)
                                   ? web_state_list_->GetWebStateAt(i - 1)
                                   : nullptr;
    const TabGroup* prev_tab_group =
        prev_web_state ? web_state_list_->GetGroupOfWebStateAt(i - 1) : nullptr;
    WebState* next_web_state = web_state_list_->ContainsIndex(i + 1)
                                   ? web_state_list_->GetWebStateAt(i + 1)
                                   : nullptr;
    const TabGroup* next_tab_group =
        next_web_state ? web_state_list_->GetGroupOfWebStateAt(i + 1) : nullptr;

    if (!pinned_web_states_separator_added &&
        !web_state_list_->IsWebStatePinnedAt(i) &&
        (!prev_web_state || web_state_list_->IsWebStatePinnedAt(i - 1))) {
      pinned_web_states_separator_added = true;
      oss << "| ";
    }

    if (tab_group != prev_tab_group && tab_group != nullptr) {
      oss << "[ " << GetTabGroupIdentifier(tab_group) << " ";
    }
    oss << GetWebStateIdentifier(web_state)
        << ((web_state_list_->active_index() == i) ? "* " : " ");
    if (tab_group != next_tab_group && tab_group != nullptr) {
      oss << "] ";
    }

    if (!pinned_web_states_separator_added &&
        web_state_list_->IsWebStatePinnedAt(i) &&
        (!next_web_state || !web_state_list_->IsWebStatePinnedAt(i + 1))) {
      pinned_web_states_separator_added = true;
      oss << "| ";
    }
  }

  std::string result = oss.str();
  if (!result.empty()) {
    // Remove trailing space if any.
    CHECK_EQ(' ', result.back());
    result.pop_back();
  }
  return result;
}

std::string WebStateListBuilderFromDescription::FormatWebStateListDescription(
    std::string_view description) const {
  std::string result = base::CollapseWhitespaceASCII(description, false);
  for (int i = result.size() - 2; i >= 0; --i) {
    // Inserting missing spaces.
    const char curr = result[i];
    const char next = result[i + 1];
    if (curr == ' ' || next == ' ' ||
        (base::IsAsciiAlpha(curr) && next == '*')) {
      continue;
    }
    result.insert(i + 1, " ");
  }
  return result;
}

web::WebState* WebStateListBuilderFromDescription::GetWebStateForIdentifier(
    char identifier) const {
  auto found_web_state_it = web_state_for_identifier_.find(identifier);
  return found_web_state_it != end(web_state_for_identifier_)
             ? found_web_state_it->second.get()
             : nullptr;
}

const TabGroup* WebStateListBuilderFromDescription::GetTabGroupForIdentifier(
    char identifier) const {
  auto found_tab_group_it = tab_group_for_identifier_.find(identifier);
  return found_tab_group_it != end(tab_group_for_identifier_)
             ? found_tab_group_it->second.get()
             : nullptr;
}

char WebStateListBuilderFromDescription::GetWebStateIdentifier(
    WebState* web_state) const {
  auto found_identifier_it = identifier_for_web_state_.find(web_state);
  return found_identifier_it != end(identifier_for_web_state_)
             ? found_identifier_it->second
             : '_';
}

char WebStateListBuilderFromDescription::GetTabGroupIdentifier(
    const TabGroup* tab_group) const {
  auto found_identifier_it = identifier_for_tab_group_.find(tab_group);
  return found_identifier_it != end(identifier_for_tab_group_)
             ? found_identifier_it->second
             : '_';
}

void WebStateListBuilderFromDescription::SetWebStateIdentifier(
    WebState* web_state,
    char new_identifier) {
  CHECK(web_state);
  CHECK(base::IsAsciiAlpha(new_identifier));
  CHECK(!GetWebStateForIdentifier(new_identifier));
  const char old_identifier = GetWebStateIdentifier(web_state);
  if (old_identifier != '_') {
    web_state_for_identifier_.erase(old_identifier);
  }
  web_state_for_identifier_[new_identifier] = web_state;
  identifier_for_web_state_[web_state] = new_identifier;
}

void WebStateListBuilderFromDescription::SetTabGroupIdentifier(
    const TabGroup* tab_group,
    char new_identifier) {
  CHECK(tab_group);
  CHECK(base::IsAsciiDigit(new_identifier));
  CHECK(!GetTabGroupForIdentifier(new_identifier));
  const char old_identifier = GetTabGroupIdentifier(tab_group);
  if (old_identifier != '_') {
    tab_group_for_identifier_.erase(old_identifier);
  }
  identifier_for_tab_group_[tab_group] = new_identifier;
  tab_group_for_identifier_[new_identifier] = tab_group;
}

void WebStateListBuilderFromDescription::GenerateIdentifiersForWebStateList() {
  std::queue<char> available_web_state_identifiers;
  for (char identifier = 'a'; identifier <= 'z'; ++identifier) {
    if (!GetWebStateForIdentifier(identifier)) {
      available_web_state_identifiers.push(identifier);
    }
  }
  for (char identifier = 'A'; identifier <= 'Z'; ++identifier) {
    if (!GetWebStateForIdentifier(identifier)) {
      available_web_state_identifiers.push(identifier);
    }
  }

  std::queue<char> available_tab_group_identifiers;
  for (char identifier = '0'; identifier <= '9'; ++identifier) {
    if (!GetTabGroupForIdentifier(identifier)) {
      available_tab_group_identifiers.push(identifier);
    }
  }

  for (int i = 0; i < web_state_list_->count(); ++i) {
    WebState* web_state = web_state_list_->GetWebStateAt(i);
    if (GetWebStateIdentifier(web_state) == '_') {
      CHECK(!available_web_state_identifiers.empty());
      char identifier = available_web_state_identifiers.front();
      available_web_state_identifiers.pop();
      SetWebStateIdentifier(web_state, identifier);
    }

    const TabGroup* tab_group = web_state_list_->GetGroupOfWebStateAt(i);
    if (tab_group && GetTabGroupIdentifier(tab_group) == '_') {
      CHECK(!available_tab_group_identifiers.empty());
      char identifier = available_tab_group_identifiers.front();
      available_tab_group_identifiers.pop();
      SetTabGroupIdentifier(tab_group, identifier);
    }
  }
}

#pragma mark - WebStateListObserver

void WebStateListBuilderFromDescription::WebStateListDidChange(
    WebStateList* web_state_list,
    const WebStateListChange& change,
    const WebStateListStatus& status) {
  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // Nothing to do.
      break;
    case WebStateListChange::Type::kDetach: {
      const WebStateListChangeDetach& detach_change =
          change.As<WebStateListChangeDetach>();
      const auto web_state = detach_change.detached_web_state();
      const char identifier = GetWebStateIdentifier(web_state);
      if (identifier != '_') {
        web_state_for_identifier_.erase(identifier);
      }
      const auto iter = identifier_for_web_state_.find(web_state);
      if (iter != end(identifier_for_web_state_)) {
        identifier_for_web_state_.erase(web_state);
      }
      break;
    }
    case WebStateListChange::Type::kMove:
      // Nothing to do.
      break;
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replace_change =
          change.As<WebStateListChangeReplace>();
      const auto replaced_web_state = replace_change.replaced_web_state();
      const char identifier = GetWebStateIdentifier(replaced_web_state);
      // Remove the replaced WebState.
      if (identifier != '_') {
        web_state_for_identifier_.erase(identifier);
      }
      const auto iter = identifier_for_web_state_.find(replaced_web_state);
      if (iter != end(identifier_for_web_state_)) {
        identifier_for_web_state_.erase(replaced_web_state);
      }
      // Add the inserted WebState.
      const auto inserted_web_state = replace_change.inserted_web_state();
      SetWebStateIdentifier(inserted_web_state, identifier);
      break;
    }
    case WebStateListChange::Type::kInsert:
      // Nothing to do.
      break;
    case WebStateListChange::Type::kGroupCreate:
      // Nothing to do.
      break;
    case WebStateListChange::Type::kGroupVisualDataUpdate:
      // Nothing to do.
      break;
    case WebStateListChange::Type::kGroupMove:
      // Nothing to do.
      break;
    case WebStateListChange::Type::kGroupDelete: {
      const WebStateListChangeGroupDelete& group_delete_change =
          change.As<WebStateListChangeGroupDelete>();
      const auto group = group_delete_change.deleted_group();
      const char identifier = GetTabGroupIdentifier(group);
      if (identifier != '_') {
        tab_group_for_identifier_.erase(identifier);
      }
      const auto iter = identifier_for_tab_group_.find(group);
      if (iter != end(identifier_for_tab_group_)) {
        identifier_for_tab_group_.erase(group);
      }
      break;
    }
  }
}

void WebStateListBuilderFromDescription::WebStateListDestroyed(
    WebStateList* web_state_list) {
  NOTREACHED_IN_MIGRATION()
      << "WebStateListBuilderFromDescription shouldn’t outlive its "
         "WebStateList";
}