/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit

// TODO for see more:
// 1. correctly decode the 'bold' attribute using Lexical rules
// 2. Maybe cache the size of the attributed string?
// 3. test if the 'non-text' truncation (cut off half way through image/table) works [it doesn't seem to]
// 4. handle taps on See More, with a Lexical event
// 5. Component state for collapsed vs not
// 6. Have I got the textContainerInsets calculation wrong? Used plus instead of minus?

public class TextContainer: NSTextContainer {

  internal var readOnlySizeCache: LexicalReadOnlySizeCache?

  override open var isSimpleRectangularTextContainer: Bool {
    get {
      guard let readOnlySizeCache else { return true }

      // if we're limiting height, AND there's a custom truncation string, then we're maybe not rectangular. Otherwise we definitely are.
      if readOnlySizeCache.requiredHeight != nil {
        return (readOnlySizeCache.customTruncationString == nil)
      }
      return true
    }
  }

  override public func lineFragmentRect(
    forProposedRect proposedRect: CGRect,
    at characterIndex: Int,
    writingDirection baseWritingDirection: NSWritingDirection,
    remaining remainingRect: UnsafeMutablePointer<CGRect>?
  ) -> CGRect {
    var lineFragmentRect = super.lineFragmentRect(forProposedRect: proposedRect,
                                                  at: characterIndex,
                                                  writingDirection: baseWritingDirection,
                                                  remaining: remainingRect)

    guard let readOnlySizeCache,
          let characterRange = readOnlySizeCache.characterRangeForLastLineFragmentBeforeTruncation,
          let glyphRange = readOnlySizeCache.glyphRangeForLastLineFragmentBeforeTruncation,
          let cutPoint = readOnlySizeCache.glyphIndexAtTruncationIndicatorCutPoint,
          NSLocationInRange(characterIndex, characterRange),
          let sizeForTruncationString = readOnlySizeCache.sizeForTruncationString
    else {
      return lineFragmentRect
    }

    // can we shrink the line? Or should we display truncation indicator below?
    let glyphsBeforeIndicator = cutPoint - glyphRange.lowerBound
    if glyphsBeforeIndicator < 2 {
      // display indicator below, so don't resize the last line fragment.
      readOnlySizeCache.textContainerDidShrinkLastLine = false
    } else {
      // display indicator inline
      lineFragmentRect.size.width = min(lineFragmentRect.width, self.size.width - (sizeForTruncationString.width + readOnlySizeCache.gapBeforeTruncationString))
      readOnlySizeCache.textContainerDidShrinkLastLine = true
    }

    return lineFragmentRect
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation

extension String {
  public func lengthAsNSString() -> Int {
    let nsString = self as NSString
    return nsString.length
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit

public class TextStorage: NSTextStorage {

  internal typealias CharacterLocation = Int
  @objc internal var decoratorPositionCache: [NodeKey: CharacterLocation] = [:]

  private var backingAttributedString: NSMutableAttributedString
  var mode: TextStorageEditingMode
  weak var editor: Editor?

  override public init() {
    backingAttributedString = NSMutableAttributedString()
    mode = TextStorageEditingMode.none
    super.init()
  }

  convenience init(editor: Editor) {
    self.init()
    self.editor = editor
    self.backingAttributedString = NSMutableAttributedString()
    self.mode = TextStorageEditingMode.none
  }

  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("\(#function) has not been implemented")
  }

  override open var string: String {
    return backingAttributedString.string
  }

  override open func attributes(
    at location: Int,
    effectiveRange range: NSRangePointer?
  ) -> [NSAttributedString.Key: Any] {
    if backingAttributedString.length <= location {
      editor?.log(.NSTextStorage, .error, "Index out of range")
      return [:]
    }
    return backingAttributedString.attributes(at: location, effectiveRange: range)
  }

  override open func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) {
    if mode == .none {
      // If mode is none (i.e. an update that hasn't gone through either controller or non-controlled mode yet),
      // we discard attribute information here. This applies to e.g. autocomplete, but it lets us handle it
      // using Lexical's own attribute persistence logic rather than UIKit's. The reason for doing it this way
      // is to avoid UIKit stomping on our custom attributes.
      editor?.log(.NSTextStorage, .verboseIncludingUserContent, "Replace characters mode=none, string length \(self.backingAttributedString.length), range \(range), replacement \(attrString.string)")
      performControllerModeUpdate(attrString.string, range: range)
      return
    }
    // Since we're in either controller or non-controlled mode, call super -- this will in turn call
    // both replaceCharacters and replaceAttributes.
    super.replaceCharacters(in: range, with: attrString)
  }

  override open func replaceCharacters(in range: NSRange, with str: String) {
    if mode == .none {
      performControllerModeUpdate(str, range: range)
      return
    }

    // Mode is not none, so this change has already passed through Lexical
    beginEditing()
    backingAttributedString.replaceCharacters(in: range, with: str)
    edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
    endEditing()
  }

  private func performControllerModeUpdate(_ str: String, range: NSRange) {
    mode = .controllerMode
    defer {
      mode = .none
    }

    do {
      guard let editor, let frontend = editor.frontend else { return }

      let nativeSelection = NativeSelection(range: range, affinity: .forward)
      try editor.update {
        guard let editorState = getActiveEditorState() else {
          return
        }
        if !(try getSelection() is RangeSelection) {
          guard let newSelection = RangeSelection(nativeSelection: nativeSelection) else {
            return
          }
          editorState.selection = newSelection
        }

        guard let selection = try getSelection() as? RangeSelection else {
          return // we should have a range selection by now, so this is unexpected
        }
        try selection.applyNativeSelection(nativeSelection)
        try selection.insertText(str)
      }
      try editor.read {
        guard let updatedSelection = try getSelection() as? RangeSelection else {
          return
        }
        let updatedNativeSelection = try createNativeSelection(from: updatedSelection, editor: editor)
        frontend.interceptNextSelectionChangeAndReplaceWithRange = updatedNativeSelection.range
      }

      frontend.showPlaceholderText()
    } catch {
      print("\(error)")
    }
    return
  }

  override open func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
    if mode != .controllerMode {
      return
    }

    beginEditing()
    backingAttributedString.setAttributes(attrs, range: range)
    edited(.editedAttributes, range: range, changeInLength: 0)
    endEditing()
  }

  public var extraLineFragmentAttributes: [NSAttributedString.Key: Any]? {
    didSet {
      beginEditing()
      if backingAttributedString.length > 0 {
        edited(.editedAttributes, range: NSRange(location: backingAttributedString.length - 1, length: 1), changeInLength: 0)
      }
      endEditing()
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import UIKit

internal class InputDelegateProxy: NSObject, UITextInputDelegate {
  internal weak var targetInputDelegate: UITextInputDelegate?
  internal var isSuspended: Bool = false

  func selectionWillChange(_ textInput: UITextInput?) {
    if let targetInputDelegate, isSuspended == false {
      targetInputDelegate.selectionWillChange(textInput)
    }
  }

  func selectionDidChange(_ textInput: UITextInput?) {
    if let targetInputDelegate, isSuspended == false {
      targetInputDelegate.selectionDidChange(textInput)
    }
  }

  func textWillChange(_ textInput: UITextInput?) {
    if let targetInputDelegate, isSuspended == false {
      targetInputDelegate.textWillChange(textInput)
    }
  }

  func textDidChange(_ textInput: UITextInput?) {
    if let targetInputDelegate, isSuspended == false {
      targetInputDelegate.textDidChange(textInput)
    }
  }

  // Note that this function only sends a didChange.
  internal func sendSelectionChangedIgnoringSuspended(_ textInput: UITextInput?) {
    if let targetInputDelegate {
      targetInputDelegate.selectionDidChange(textInput)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import UIKit

public class NativeSelection {

  internal init(range: NSRange?, opaqueRange: UITextRange?, affinity: UITextStorageDirection, markedRange: NSRange?, markedOpaqueRange: UITextRange?, selectionIsNodeOrObject: Bool) {
    self.range = range
    self.opaqueRange = opaqueRange
    self.affinity = affinity
    self.markedRange = markedRange
    self.markedOpaqueRange = markedOpaqueRange
    self.selectionIsNodeOrObject = selectionIsNodeOrObject
  }

  internal init() {
    self.range = nil
    self.opaqueRange = nil
    self.affinity = .forward
    self.markedRange = nil
    self.markedOpaqueRange = nil
    self.selectionIsNodeOrObject = false // may reconsider this default later
  }

  public convenience init(range: NSRange, affinity: UITextStorageDirection) {
    self.init(range: range,
              opaqueRange: nil,
              affinity: affinity,
              markedRange: nil,
              markedOpaqueRange: nil,
              selectionIsNodeOrObject: false)
  }

  // if nil, there's no selection at all (i.e. no focus). If there's a location but length 0, then
  // the caret is being displayed.
  let range: NSRange?

  // UITextRange can only be turned into a numerical range in the context of a text field. We're storing
  // both things in this object -- the opaque range is useful for modifying the selection, whereas the
  // numerical range is useful for matching up with our range cache.
  let opaqueRange: UITextRange?

  let affinity: UITextStorageDirection

  // marked text is the iOS term for what Lexical calls `composing`.
  // If these properties are nil, there is no marked text.
  // The opaque range comes straight from the text view; the range (as an NSRange) is calculated by us.
  let markedRange: NSRange?
  let markedOpaqueRange: UITextRange?

  // The selection is something that cannot be represented by a character range. Usually corresponds with
  // NodeSelection or similar within Lexical.
  let selectionIsNodeOrObject: Bool
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import MobileCoreServices
import UIKit

protocol LexicalTextViewDelegate: NSObjectProtocol {
  func textViewDidBeginEditing(textView: TextView)
  func textViewDidEndEditing(textView: TextView)
  func textViewShouldChangeText(_ textView: UITextView, range: NSRange, replacementText text: String)
  func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
}

class TextView: UITextView {
  let editor: Editor

  internal let pasteboard = UIPasteboard.general
  internal let pasteboardIdentifier = "x-lexical-nodes"
  internal var isUpdatingNativeSelection = false
  internal var layoutManagerDelegate: LayoutManagerDelegate

  // This is to work around a UIKit issue where, in situations like autocomplete, UIKit changes our selection via
  // private methods, and the first time we find out is when our delegate method is called. @amyworrall
  internal var interceptNextSelectionChangeAndReplaceWithRange: NSRange?
  weak var lexicalDelegate: LexicalTextViewDelegate?
  private var placeholderLabel: UILabel

  private let useInputDelegateProxy: Bool
  private let inputDelegateProxy: InputDelegateProxy

  // MARK: - Init

  init(editorConfig: EditorConfig, featureFlags: FeatureFlags) {
    let textStorage = TextStorage()
    let layoutManager = LayoutManager()
    layoutManagerDelegate = LayoutManagerDelegate()
    layoutManager.delegate = layoutManagerDelegate

    let textContainer = TextContainer(size: CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude))
    textContainer.widthTracksTextView = true

    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)

    var reconcilerSanityCheck = featureFlags.reconcilerSanityCheck

    #if targetEnvironment(simulator)
    reconcilerSanityCheck = false
    #endif

    editor = Editor(
      featureFlags: FeatureFlags(reconcilerSanityCheck: reconcilerSanityCheck),
      editorConfig: editorConfig)
    textStorage.editor = editor
    placeholderLabel = UILabel(frame: .zero)

    useInputDelegateProxy = featureFlags.proxyTextViewInputDelegate
    inputDelegateProxy = InputDelegateProxy()

    super.init(frame: .zero, textContainer: textContainer)

    if useInputDelegateProxy {
      inputDelegateProxy.targetInputDelegate = self.inputDelegate
      super.inputDelegate = inputDelegateProxy
    }

    delegate = self
    textContainerInset = UIEdgeInsets(top: 8.0, left: 5.0, bottom: 8.0, right: 5.0)

    setUpPlaceholderLabel()
    registerRichText(editor: editor)
  }

  /// This init method is used for unit tests
  convenience init() {
    self.init(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError("\(#function) has not been implemented")
  }

  override func layoutSubviews() {
    super.layoutSubviews()

    placeholderLabel.frame.origin = CGPoint(x: textContainer.lineFragmentPadding * 1.5 + textContainerInset.left, y: textContainerInset.top)
    placeholderLabel.sizeToFit()
  }

  override var inputDelegate: UITextInputDelegate? {
    get {
      if useInputDelegateProxy {
        return inputDelegateProxy.targetInputDelegate
      } else {
        return super.inputDelegate
      }
    }
    set {
      if useInputDelegateProxy {
        inputDelegateProxy.targetInputDelegate = newValue
      } else {
        super.inputDelegate = newValue
      }
    }
  }

  // MARK: - Incoming events

  override func deleteBackward() {
    editor.log(.UITextView, .verbose, "deleteBackward()")

    let previousSelectedRange = selectedRange

    inputDelegateProxy.isSuspended = true // do not send selection changes during deleteBackwards, to not confuse third party keyboards
    defer {
      inputDelegateProxy.isSuspended = false
    }

    editor.dispatchCommand(type: .deleteCharacter, payload: true)

    if previousSelectedRange.length > 0 {
      // Expect new selection to be on the start of selection
      if selectedRange.location != previousSelectedRange.location || selectedRange.length != 0 {
        inputDelegateProxy.sendSelectionChangedIgnoringSuspended(self)
      }
    } else {
      // Expect new selection to be somewhere before selection -- we could calculate this by considering
      // unicode characters, but it would be complex. Let's do a best effort, since this situation is rare anyway.
      if selectedRange.length != 0 || selectedRange.location >= previousSelectedRange.location {
        inputDelegateProxy.sendSelectionChangedIgnoringSuspended(self)
      }
    }
  }

  override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    if action == #selector(paste(_:)) {
      if pasteboard.hasStrings {
        return true
      } else if !(pasteboard.data(forPasteboardType: LexicalConstants.pasteboardIdentifier)?.isEmpty ?? true) {
        return true
      } else if !(pasteboard.data(forPasteboardType: (kUTTypeUTF8PlainText as String))?.isEmpty ?? true) {
        return true
      } else {
        return super.canPerformAction(action, withSender: sender)
      }
    } else {
      return super.canPerformAction(action, withSender: sender)
    }
  }

  override func copy(_ sender: Any?) {
    editor.dispatchCommand(type: .copy, payload: pasteboard)
  }

  override func cut(_ sender: Any?) {
    editor.dispatchCommand(type: .cut, payload: pasteboard)
  }

  override func paste(_ sender: Any?) {
    editor.dispatchCommand(type: .paste, payload: pasteboard)
  }

  override func insertText(_ text: String) {
    editor.log(.UITextView, .verbose, "Text view selected range \(String(describing: self.selectedRange))")

    let expectedSelectionLocation = selectedRange.location + text.lengthAsNSString()

    inputDelegateProxy.isSuspended = true // do not send selection changes during insertText, to not confuse third party keyboards
    defer {
      inputDelegateProxy.isSuspended = false
    }

    guard let textStorage = textStorage as? TextStorage else {
      // This should never happen, we will always have a custom text storage.
      editor.log(.TextView, .error, "Missing custom text storage")
      return
    }

    textStorage.mode = TextStorageEditingMode.controllerMode
    editor.dispatchCommand(type: .insertText, payload: text)
    textStorage.mode = TextStorageEditingMode.none

    // check if we need to send a selectionChanged (i.e. something unexpected happened)
    if selectedRange.length != 0 || selectedRange.location != expectedSelectionLocation {
      inputDelegateProxy.sendSelectionChangedIgnoringSuspended(self)
    }
  }

  // MARK: Marked text

  override func setAttributedMarkedText(_ markedText: NSAttributedString?, selectedRange: NSRange) {
    editor.log(.UITextView, .verbose)
    if let markedText {
      setMarkedTextInternal(markedText.string, selectedRange: selectedRange)
    } else {
      unmarkText()
    }
  }

  override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
    editor.log(.UITextView, .verbose)
    if let markedText {
      setMarkedTextInternal(markedText, selectedRange: selectedRange)
    } else {
      unmarkText()
    }
  }

  private func setMarkedTextInternal(_ markedText: String, selectedRange: NSRange) {
    editor.log(.TextView, .verbose)
    guard let textStorage = textStorage as? TextStorage else {
      // This should never happen, we will always have a custom text storage.
      editor.log(.TextView, .error, "Missing custom text storage")
      super.setMarkedText(markedText, selectedRange: selectedRange)
      return
    }

    if markedText.isEmpty, let markedRange = editor.getNativeSelection().markedRange {
      textStorage.replaceCharacters(in: markedRange, with: "")
      return
    }

    let markedTextOperation = MarkedTextOperation(createMarkedText: true,
                                                  selectionRangeToReplace: editor.getNativeSelection().markedRange ?? self.selectedRange,
                                                  markedTextString: markedText,
                                                  markedTextInternalSelection: selectedRange)

    let behaviourModificationMode = UpdateBehaviourModificationMode(suppressReconcilingSelection: true, suppressSanityCheck: true, markedTextOperation: markedTextOperation)

    textStorage.mode = TextStorageEditingMode.controllerMode
    defer {
      textStorage.mode = TextStorageEditingMode.none
    }
    do {
      // set composition key
      try editor.read {
        guard let selection = try getSelection() as? RangeSelection else {
          editor.log(.TextView, .error, "Could not get selection in setMarkedTextInternal()")
          throw LexicalError.invariantViolation("should have selection when starting marked text")
        }

        editor.compositionKey = selection.anchor.key
      }

      // insert text
      try onInsertTextFromUITextView(text: markedText, editor: editor, updateMode: behaviourModificationMode)
    } catch {
      let language = textInputMode?.primaryLanguage
      editor.log(.TextView, .error, "exception thrown, lang \(String(describing: language)): \(String(describing: error))")
      unmarkTextWithoutUpdate()
      return
    }
  }

  internal func setMarkedTextFromReconciler(_ markedText: NSAttributedString, selectedRange: NSRange) {
    editor.log(.TextView, .verbose)
    isUpdatingNativeSelection = true
    super.setAttributedMarkedText(markedText, selectedRange: selectedRange)
    interceptNextSelectionChangeAndReplaceWithRange = nil
    onSelectionChange(editor: editor)
    isUpdatingNativeSelection = false
    editor.compositionKey = nil
    showPlaceholderText()
  }

  override func unmarkText() {
    editor.log(.UITextView, .verbose)
    let previousMarkedRange = editor.getNativeSelection().markedRange
    let oldIsUpdatingNative = isUpdatingNativeSelection
    isUpdatingNativeSelection = true
    super.unmarkText()
    isUpdatingNativeSelection = oldIsUpdatingNative
    if let previousMarkedRange {
      // find all nodes in selection. Mark dirty. Reconcile. This should correct all the attributes to be what we expect.
      do {
        try editor.update {
          guard let anchor = try pointAtStringLocation(previousMarkedRange.location, searchDirection: .forward, rangeCache: editor.rangeCache),
                let focus = try pointAtStringLocation(previousMarkedRange.location + previousMarkedRange.length, searchDirection: .forward, rangeCache: editor.rangeCache) else {
            return
          }

          let markedRangeSelection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())
          _ = try markedRangeSelection.getNodes().map { node in
            internallyMarkNodeAsDirty(node: node, cause: .userInitiated)
          }

          editor.compositionKey = nil
        }
      } catch {}
    }
  }

  internal func unmarkTextWithoutUpdate() {
    editor.log(.TextView, .verbose)
    super.unmarkText()
  }

  // MARK: - Lexical internal

  internal func presentDeveloperFacingError(message: String) {
    let alert = UIAlertController(title: "Lexical Error", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil))
    if let rootViewController = self.window?.rootViewController {
      rootViewController.present(alert, animated: true, completion: nil)
    }
  }

  internal func updateNativeSelection(from selection: RangeSelection) throws {
    isUpdatingNativeSelection = true
    defer { isUpdatingNativeSelection = false }
    let nativeSelection = try createNativeSelection(from: selection, editor: editor)

    if let range = nativeSelection.range {
      selectedRange = range
    }
  }

  internal func resetSelectedRange() {
    selectedRange = NSRange(location: 0, length: 0)
  }

  func defaultClearEditor() throws {
    editor.resetEditor(pendingEditorState: nil)
    editor.dispatchCommand(type: .clearEditor)
  }

  func setPlaceholderText(_ text: String, textColor: UIColor, font: UIFont) {
    placeholderLabel.text = text
    placeholderLabel.textColor = textColor
    placeholderLabel.font = font
    self.font = font

    showPlaceholderText()
  }

  func showPlaceholderText() {
    var shouldShow = false
    do {
      try editor.read {
        guard let root = getRoot() else { return }
        shouldShow = root.getTextContentSize() == 0
      }
      if !shouldShow {
        hidePlaceholderLabel()
        return
      }
      try editor.read {
        if canShowPlaceholder(isComposing: editor.isComposing()) {
          placeholderLabel.isHidden = false
          layoutIfNeeded()
        }
      }
    } catch {}
  }

  // MARK: - Private

  private func setUpPlaceholderLabel() {
    placeholderLabel.backgroundColor = .clear
    placeholderLabel.isHidden = true
    placeholderLabel.isAccessibilityElement = false
    placeholderLabel.numberOfLines = 1
    addSubview(placeholderLabel)
  }

  private func hidePlaceholderLabel() {
    placeholderLabel.isHidden = true
  }
}

extension TextView: UITextViewDelegate {
  public func textViewDidChangeSelection(_ textView: UITextView) {

    if isUpdatingNativeSelection {
      return
    }

    if let interception = interceptNextSelectionChangeAndReplaceWithRange {
      interceptNextSelectionChangeAndReplaceWithRange = nil
      selectedRange = interception
      return
    }

    onSelectionChange(editor: editor)
  }

  public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    hidePlaceholderLabel()
    lexicalDelegate?.textViewShouldChangeText(self, range: range, replacementText: text)

    return true
  }

  public func textViewDidBeginEditing(_ textView: UITextView) {
    lexicalDelegate?.textViewDidBeginEditing(textView: self)
  }

  public func textViewDidEndEditing(_ textView: UITextView) {
    lexicalDelegate?.textViewDidEndEditing(textView: self)
  }

  public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    if !isEditable {
      return true
    }

    return lexicalDelegate?.textView(self, shouldInteractWith: URL, in: characterRange, interaction: interaction) ?? false
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
@testable import Lexical
import XCTest

// swiftlint:disable force_try

/* Produces text output:
 *
 * hello world\n
 * Paragraph 2 contains another text node\n
 * \n
 * Third para.
 */
func createExampleNodeTree() {
  guard let editorState = getActiveEditorState(), let rootNode = editorState.getRootNode() else {
    XCTFail("should have editor state")
    return
  }

  guard let paragraphNode = rootNode.getFirstChild() as? ParagraphNode else { // key 0
    XCTFail("Didn't find pre-prepared paragraph node")
    return
  }

  let textNode = TextNode() // key 1
  try! textNode.setText("hello ")

  let textNode2 = TextNode() // key 2
  try! textNode2.setBold(true)
  try! textNode2.setText("world")

  try! paragraphNode.append([textNode])
  try! paragraphNode.append([textNode2])

  let textNode3 = TextNode() // key 3
  try! textNode3.setText("Paragraph 2 contains another text node")
  let paragraphNode2 = ParagraphNode() // key 4
  try! paragraphNode2.append([textNode3])

  let emptyPara = ParagraphNode() // key 5

  let textNode4 = TextNode() // key 6
  try! textNode4.setText("Third para.")
  let paragraphNode3 = ParagraphNode() // key 7
  try! paragraphNode3.append([textNode4])

  try! rootNode.append([paragraphNode2])
  try! rootNode.append([emptyPara])
  try! rootNode.append([paragraphNode3])
}

// swiftlint:enable force_try


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import Foundation
import Lexical

/// TestAttributesNode class has been added for unit tests
class TestAttributesNode: ElementNode {
  override required init() {
    super.init()
    self.type = NodeType(rawValue: "TestNode")
  }

  public required init(from decoder: Decoder) throws {
    try super.init(from: decoder)
    self.type = NodeType(rawValue: "TestNode")
  }

  override open func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] {
    var attributeDictionary = super.getAttributedStringAttributes(theme: theme)
    attributeDictionary[.fontFamily] = "Arial"
    attributeDictionary[.fontSize] = 10 as Float
    attributeDictionary[.bold] = true

    return attributeDictionary
  }

  override public func clone() -> Self {
    Self()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class AttributesUtilsTests: XCTestCase {
  func testGetLexicalAttributes() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])

      let attributes = AttributeUtils.getLexicalAttributes(
        from: textNode,
        state: editorState,
        theme: editor.getTheme()
      )

      for attributeDict in attributes {
        if let font = attributeDict[.font] as? UIFont {
          XCTAssertTrue(font.familyName == "Helvetica", "Node font attribute is incorrect")
        }
      }
    }
  }

  private func firstFontInAttributedString(attrStr: NSAttributedString) -> UIFont {
    let font = attrStr.attribute(.font, at: 0, effectiveRange: nil)
    return font as? UIFont ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
  }

  func testattributedStringByAddingStyles() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])

      let attributedString = NSMutableAttributedString(string: textNode.getTextPart())

      let styledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let font = firstFontInAttributedString(attrStr: styledAttrStr)

      XCTAssertTrue(font.familyName == "Helvetica", "TextNode's attributes should take priority over all parent node's attributes")
    }
  }

  func testApplyBoldStyles() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello world")
      textNode.format.bold = true

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])
      let attributedString = NSMutableAttributedString(string: textNode.getTextPart())
      let styledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let font = firstFontInAttributedString(attrStr: styledAttrStr)

      XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should contain the bold trait")

      textNode.format.bold = false
      // let attributedString = NSMutableAttributedString(string: textNode.getTextPart())
      let newStyledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let newFont = firstFontInAttributedString(attrStr: newStyledAttrStr)

      XCTAssertFalse(newFont.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should not contain the bold trait")
    }
  }

  func testApplyItalicStyles() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello world")
      textNode.format.italic = true

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])
      let attributedString = NSMutableAttributedString(string: textNode.getTextPart())
      let styledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let font = firstFontInAttributedString(attrStr: styledAttrStr)

      XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitItalic), "Font should contain the italic trait")

      textNode.format.italic = false
      let newStyledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let newFont = firstFontInAttributedString(attrStr: newStyledAttrStr)

      XCTAssertFalse(newFont.fontDescriptor.symbolicTraits.contains(.traitItalic), "Font should not contain the italic trait")
    }
  }

  func testApplyBoldAndItalicStyles() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello world")
      textNode.format.bold = true
      textNode.format.italic = true

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])
      let attributedString = NSMutableAttributedString(string: textNode.getTextPart())
      let styledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let font = firstFontInAttributedString(attrStr: styledAttrStr)

      XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should contain the bold trait")
      XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitItalic), "Font should contain the italic trait")

      textNode.format.bold = false
      let newStyledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: textNode, state: editorState, theme: editor.getTheme())
      let newFont = firstFontInAttributedString(attrStr: newStyledAttrStr)

      XCTAssertFalse(newFont.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should not contain the bold trait")
      XCTAssertTrue(newFont.fontDescriptor.symbolicTraits.contains(.traitItalic), "Font should contain the italic trait")
    }
  }

  func testFontUpdate() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor
    try editor.update {
      let testAttributeNode = TestAttributesNode()

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([testAttributeNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])

      let attributedString = NSMutableAttributedString(string: "Hello World")
      let styledAttrStr = AttributeUtils.attributedStringByAddingStyles(attributedString, from: testAttributeNode, state: editorState, theme: editor.getTheme())
      let font = firstFontInAttributedString(attrStr: styledAttrStr)

      XCTAssertEqual(font.familyName, "Arial", "Font attribute is incorrect")
      XCTAssertTrue(font.pointSize == 10, "Font size is incorrect")
      XCTAssertTrue(font.fontDescriptor.symbolicTraits.contains(.traitBold), "Font should not contain the bold trait")
    }
  }

  func testThemeForRootNode() throws {
    let rootAttributes: [NSAttributedString.Key: Any] = [
      .fontFamily: "Arial",
      .fontSize: 18 as Float
    ]

    let theme = Theme()
    theme.root = rootAttributes
    let view = LexicalView(editorConfig: EditorConfig(theme: theme, plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let paragraphNode = ParagraphNode()
      let textNode = TextNode()
      try textNode.setText("Testing Theme!")

      try paragraphNode.append([textNode])

      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([paragraphNode])
    }

    let attributedString = view.textView.attributedText
    if let attribute = attributedString?.attribute(.font, at: 1, effectiveRange: nil) as? UIFont {
      XCTAssertEqual(attribute.familyName, "Arial")
      XCTAssertEqual(attribute.pointSize, 18)
    }
  }

  func testThemeForHeadingNode() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let headingNode = HeadingNode(tag: .h1)
      let textNode = TextNode()
      try textNode.setText("Testing HeadingNode!")

      try headingNode.append([textNode])
      guard let editorState = getActiveEditorState(),
            let rootNode: RootNode = try editorState.getRootNode()?.getWritable()
      else {
        XCTFail("should have editor state")
        return
      }

      try rootNode.append([headingNode])
    }

    let attributedString = view.textView.attributedText
    if let attribute = attributedString?.attribute(.font, at: 1, effectiveRange: nil) as? UIFont {
      XCTAssertEqual(attribute.familyName, "Helvetica")
      XCTAssertEqual(attribute.pointSize, 36)
    }
  }

  private func themeForBlockTests() -> Theme {
    let theme = Theme()
    theme.setBlockLevelAttributes(.code, value: BlockLevelAttributes(marginTop: 5, marginBottom: 3, paddingTop: 6, paddingBottom: 4))
    return theme
  }

  func testBlockLevelAttributesSimpleCase() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: themeForBlockTests(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let paragraphNode = ParagraphNode()
      let textNode = TextNode()
      try textNode.setText("Para1")
      try paragraphNode.append([textNode])

      let codeLine1 = TextNode(text: "line1")
      let lineBreak = LineBreakNode()
      let codeLine2 = TextNode(text: "line2")
      let codeNode = CodeNode()
      try codeNode.append([codeLine1, lineBreak, codeLine2])

      guard let rootNode = getRoot() else {
        XCTFail("should have root node")
        return
      }

      try rootNode.getChildren().forEach { node in
        try node.remove()
      }

      try rootNode.append([paragraphNode, codeNode])
    }

    guard let attributedString = view.textView.attributedText else {
      XCTFail("No attr string")
      return
    }
    XCTAssertEqual(attributedString.string, "Para1\nline1\nline2")

    // test that line1 gets the spacing before
    if let attribute = attributedString.attribute(.paragraphStyle, at: 6, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 11, "Expected combination of paddingtop and margintop")
      XCTAssertEqual(attribute.paragraphSpacing, 0, "Expected no spacing after")
    } else {
      XCTFail("no para style")
    }
    // this is the first character of line2
    if let attribute = attributedString.attribute(.paragraphStyle, at: 12, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 0, "Expected no spacing before")
      XCTAssertEqual(attribute.paragraphSpacing, 7, "Expected spacing after")
    } else {
      XCTFail("no para style")
    }
  }

  func testBlockLevelAttributesEmptyLineEndOfBlock() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: themeForBlockTests(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let paragraphNode = ParagraphNode()
      let textNode = TextNode()
      try textNode.setText("Para1")
      try paragraphNode.append([textNode])

      let codeLine1 = TextNode(text: "line1")
      let lineBreak = LineBreakNode()
      let codeNode = CodeNode()
      try codeNode.append([codeLine1, lineBreak])

      let paraNode2 = ParagraphNode()
      let textNode2 = TextNode(text: "text2")
      try paraNode2.append([textNode2])

      guard let rootNode = getRoot() else {
        XCTFail("should have root node")
        return
      }

      try rootNode.getChildren().forEach { node in
        try node.remove()
      }

      try rootNode.append([paragraphNode, codeNode, paraNode2])
    }

    guard let attributedString = view.textView.attributedText else {
      XCTFail("No attr string")
      return
    }
    XCTAssertEqual(attributedString.string, "Para1\nline1\n\ntext2")

    // test that line1 gets the spacing before
    if let attribute = attributedString.attribute(.paragraphStyle, at: 6, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 11, "Expected combination of paddingtop and margintop")
      XCTAssertEqual(attribute.paragraphSpacing, 0, "Expected no spacing after")
    } else {
      XCTFail("no para style")
    }
    // this is a newline character that should contain the styling for the end of the code block
    if let attribute = attributedString.attribute(.paragraphStyle, at: 12, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 0, "Expected no spacing before")
      XCTAssertEqual(attribute.paragraphSpacing, 7, "Expected spacing after")
    } else {
      XCTFail("no para style")
    }

    XCTAssertNil(view.textStorage.extraLineFragmentAttributes, "Should have no extra line fragment attributes due to not ending in newline")
  }

  func testBlockLevelAttributesEmptyLineEndOfDocument() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: themeForBlockTests(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let paragraphNode = ParagraphNode()
      let textNode = TextNode()
      try textNode.setText("Para1")
      try paragraphNode.append([textNode])

      let codeLine1 = TextNode(text: "line1")
      let lineBreak = LineBreakNode()
      let codeNode = CodeNode()
      try codeNode.append([codeLine1, lineBreak])

      guard let rootNode = getRoot() else {
        XCTFail("should have root node")
        return
      }

      try rootNode.getChildren().forEach { node in
        try node.remove()
      }

      try rootNode.append([paragraphNode, codeNode])
    }

    guard let attributedString = view.textView.attributedText else {
      XCTFail("No attr string")
      return
    }
    XCTAssertEqual(attributedString.string, "Para1\nline1\n")

    // test that line1 gets the spacing before
    if let attribute = attributedString.attribute(.paragraphStyle, at: 6, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 11, "Expected combination of paddingtop and margintop")
      XCTAssertEqual(attribute.paragraphSpacing, 0, "Expected no spacing after")
    } else {
      XCTFail("no para style")
    }
    // this is a newline character that the LineBreakNode corresponds to.
    if let attribute = attributedString.attribute(.paragraphStyle, at: 11, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 11, "Expected spacing before, since this line break is the same paragraph as line1")
      XCTAssertEqual(attribute.paragraphSpacing, 0, "Expected no spacing after, since that's handled by the extra line fragment attributes")
    } else {
      XCTFail("no para style")
    }

    XCTAssertNotNil(view.textStorage.extraLineFragmentAttributes, "Should have extra line fragment attributes")
    if let attribute = view.textStorage.extraLineFragmentAttributes?[.paragraphStyle] as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 0, "Expected no spacing before")
      XCTAssertEqual(attribute.paragraphSpacing, 7, "Expected spacing after")
    } else {
      XCTFail("no para style in extra line fragment attribs")
    }
  }

  func testBlockLevelAttributesEmptyParagraphEndOfDocument() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: themeForBlockTests(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let paragraphNode = ParagraphNode()
      let textNode = TextNode()
      try textNode.setText("Para1")
      try paragraphNode.append([textNode])

      let codeLine1 = TextNode(text: "line1")
      let lineBreak = LineBreakNode()
      let codeNode = CodeNode()
      try codeNode.append([codeLine1, lineBreak])

      let paraNode2 = ParagraphNode()
      let textNode2 = TextNode(text: "")
      try paraNode2.append([textNode2])

      guard let rootNode = getRoot() else {
        XCTFail("should have root node")
        return
      }

      try rootNode.getChildren().forEach { node in
        try node.remove()
      }

      try rootNode.append([paragraphNode, codeNode, paraNode2])
    }

    guard let attributedString = view.textView.attributedText else {
      XCTFail("No attr string")
      return
    }
    XCTAssertEqual(attributedString.string, "Para1\nline1\n\n")

    // test that line1 gets the spacing before
    if let attribute = attributedString.attribute(.paragraphStyle, at: 6, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 11, "Expected combination of paddingtop and margintop")
      XCTAssertEqual(attribute.paragraphSpacing, 0, "Expected no spacing after")
    } else {
      XCTFail("no para style")
    }
    // this is a newline character that should contain the styling for the end of the code block
    if let attribute = attributedString.attribute(.paragraphStyle, at: 12, effectiveRange: nil) as? NSParagraphStyle {
      XCTAssertEqual(attribute.paragraphSpacingBefore, 0, "Expected no spacing before")
      XCTAssertEqual(attribute.paragraphSpacing, 7, "Expected spacing after")
    } else {
      XCTFail("no para style")
    }

    XCTAssertNotNil(view.textStorage.extraLineFragmentAttributes, "Should have extra line fragment attributes")
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class CodeNodeTests: XCTestCase {
  var view: LexicalView?
  var editor: Editor? {
    get {
      return view?.editor
    }
  }

  override func setUp() {
    view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  override func tearDown() {
    view = nil
  }

  func testinsertNewAfter() throws {
    guard let editor else {
      XCTFail("Editor unexpectedly nil")
      return
    }

    try editor.update {
      guard
        let editorState = getActiveEditorState(),
        let rootNode = editorState.getRootNode()
      else {
        XCTFail("should have editor state")
        return
      }

      let codeNode = CodeNode()
      let firstCode = CodeHighlightNode()
      try firstCode.setText("Test1")

      let firstLinebreak = LineBreakNode()
      let secondLinebreak = LineBreakNode()

      try codeNode.append([firstCode, firstLinebreak, secondLinebreak])

      try rootNode.append([codeNode])

      editorState.selection = RangeSelection(
        anchor: Point(key: codeNode.key, offset: 3, type: .element),
        focus: Point(key: codeNode.key, offset: 3, type: .element),
        format: TextFormat())
    }

    try editor.update {
      guard let editorState = getActiveEditorState() else {
        XCTFail("should have editor state")
        return
      }

      guard let codeNode = getNodeByKey(key: "1") as? CodeNode else {
        XCTFail("Code node not found")
        return
      }

      guard let selection = editorState.selection as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }

      let newNode = try codeNode.insertNewAfter(selection: selection)
      XCTAssertNotNil(newNode)
      XCTAssertEqual(newNode?.parent, codeNode.parent)
      XCTAssertEqual(newNode?.type, NodeType.paragraph)
      XCTAssertNotEqual(newNode?.key, codeNode.key)
      XCTAssertEqual(newNode, codeNode.getNextSibling())
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

extension NodeType {
  static let testNode = NodeType(rawValue: "testNode")
}

class TestDecoratorNode: DecoratorNode {
  var numberOfTimesDecorateHasBeenCalled = 0

  public required init(numTimes: Int, key: NodeKey? = nil) {
    super.init(key)
    self.numberOfTimesDecorateHasBeenCalled = numTimes
  }

  override init() {
    super.init(nil)
  }

  public required init(_ key: NodeKey?) {
    super.init(key)
  }

  required init(from decoder: Decoder) throws {
    fatalError("init(from:) has not been implemented")
  }

  override public func clone() -> Self {
    Self(numTimes: numberOfTimesDecorateHasBeenCalled, key: key)
  }

  override public func createView() -> UIImageView {
    return UIImageView()
  }

  override public func decorate(view: UIView) {
    getLatest().numberOfTimesDecorateHasBeenCalled += 1
  }

  override public func sizeForDecoratorView(textViewWidth: CGFloat, attributes: [NSAttributedString.Key: Any]) -> CGSize {
    return CGSize(width: 100, height: 100)
  }
}

class DecoratorNodeTests: XCTestCase {
  func createLexicalView() -> LexicalView {
    return LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  func testIsDecoratorNode() throws {
    let view = createLexicalView()
    let editor = view.editor

    try editor.update {
      let decoratorNode = DecoratorNode()
      let textNode = TextNode()

      XCTAssert(isDecoratorNode(decoratorNode))
      XCTAssert(!isDecoratorNode(textNode))
    }
  }

  func testDecoratorNodeAddsSubViewOnceOnNodeCreation() throws {
    let view = createLexicalView()
    let editor = view.editor

    try editor.registerNode(nodeType: NodeType.testNode, class: TestDecoratorNode.self)

    guard let viewForDecoratorSubviews = view.viewForDecoratorSubviews else {
      XCTFail()
      return
    }

    let initialSubViewCount = viewForDecoratorSubviews.subviews.count

    XCTAssertFalse(viewForDecoratorSubviews.subviews.last is UIImageView)

    try editor.update {
      guard let rootNode = getActiveEditorState()?.getRootNode() else {
        XCTFail("No root node")
        return
      }

      let paragraphNode = ParagraphNode()

      let textNode = TextNode()
      try textNode.setText("Hello")

      let decoratorNode = TestDecoratorNode()

      try paragraphNode.append([textNode])
      try paragraphNode.append([decoratorNode])

      try rootNode.append([paragraphNode])
    }

    try editor.update {}
    try editor.update {}
    try editor.update {}

    XCTAssertEqual(viewForDecoratorSubviews.subviews.count, initialSubViewCount + 1)
    XCTAssertTrue(viewForDecoratorSubviews.subviews.last is UIImageView)
  }

  func testDecorateCalledOnlyWhenDirty() throws {
    let view = createLexicalView()
    let editor = view.editor

    try editor.registerNode(nodeType: NodeType.testNode, class: TestDecoratorNode.self)

    var nodeKey: NodeKey?

    try editor.update {
      guard let rootNode = getActiveEditorState()?.getRootNode() else {
        XCTFail("No root node")
        return
      }

      let paragraphNode = ParagraphNode()

      let decoratorNode = TestDecoratorNode()
      try paragraphNode.append([decoratorNode])
      nodeKey = decoratorNode.getKey()

      try rootNode.append([paragraphNode])
    }

    try editor.read {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      XCTAssertEqual(decoratorNode.numberOfTimesDecorateHasBeenCalled, 1)
    }

    try editor.update {}

    try editor.read {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      XCTAssertEqual(decoratorNode.numberOfTimesDecorateHasBeenCalled, 1, "should still be 1 after an update where nothing changed")
    }

    try editor.update {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      internallyMarkNodeAsDirty(node: decoratorNode, cause: .userInitiated)
    }

    try editor.read {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      XCTAssertEqual(decoratorNode.numberOfTimesDecorateHasBeenCalled, 2, "should be 2 after a dirty update")
    }
  }

  func testDecorateCalledWhenMountingFrontendView() throws {
    let textKitContext = LexicalReadOnlyTextKitContext(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = textKitContext.editor

    try editor.registerNode(nodeType: NodeType.testNode, class: TestDecoratorNode.self)

    var nodeKey: NodeKey?

    try editor.update {
      guard let rootNode = getActiveEditorState()?.getRootNode() else {
        XCTFail("No root node")
        return
      }

      let paragraphNode = ParagraphNode()

      let decoratorNode = TestDecoratorNode()
      try paragraphNode.append([decoratorNode])
      nodeKey = decoratorNode.getKey()

      try rootNode.append([paragraphNode])
    }

    try editor.read {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      XCTAssertEqual(decoratorNode.numberOfTimesDecorateHasBeenCalled, 0)
    }

    try editor.update {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      internallyMarkNodeAsDirty(node: decoratorNode, cause: .userInitiated)
    }

    try editor.read {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      XCTAssertEqual(decoratorNode.numberOfTimesDecorateHasBeenCalled, 0, "should not have decorated as no view created")
    }

    let view = LexicalReadOnlyView()
    view.textKitContext = textKitContext

    try editor.read {
      guard let nodeKey, let decoratorNode = getNodeByKey(key: nodeKey) as? TestDecoratorNode else { XCTFail(); return }
      XCTAssertEqual(decoratorNode.numberOfTimesDecorateHasBeenCalled, 1, "should have automatically called decorate when attaching text kit context")
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class EditorStateTests: XCTestCase {

  func testReadReturnsCorrectState() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()

      let textNode = TextNode()
      try textNode.setText("hello world")
    }

    try editor.getEditorState().read {
      guard let activeEditorState = getActiveEditorState() else {
        XCTFail("Editor State is unexpectedly nil")
        return
      }

      guard let node = activeEditorState.getRootNode()?.getFirstChild() as? ParagraphNode else {
        XCTFail("Node is unexpectedly nil")
        return
      }

      guard let textNode = node.getFirstChild() as? TextNode else {
        XCTFail("Text node is unexpectedly nil")
        return
      }

      XCTAssertEqual(textNode.getTextPart(), "hello ")
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class ElementNodeTests: XCTestCase {
  var view: LexicalView?
  var editor: Editor {
    get {
      guard let editor = view?.editor else {
        XCTFail("Editor unexpectedly nil")
        fatalError()
      }
      return editor
    }
  }

  override func setUp() {
    view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  override func tearDown() {
    view = nil
  }

  func testIsElementNode() throws {
    try editor.update {
      let node = ElementNode()
      let textNode = TextNode()
      try textNode.setText("hello")

      XCTAssert(isElementNode(node: node))
      XCTAssert(!isElementNode(node: textNode))
    }
  }

  func testElementNodeProperties() throws {
    try editor.update {
      let node = ElementNode()
      let extraNode = ElementNode()

      XCTAssertTrue(node.canExtractContents())
      XCTAssertTrue(node.canReplaceWith(replacement: extraNode))
      XCTAssertTrue(node.canInsertAfter(node: extraNode))
      XCTAssertTrue(node.canBeEmpty())
      XCTAssertTrue(node.canInsertTextBefore())
      XCTAssertTrue(node.canInsertTextAfter())
      XCTAssertTrue(node.canSelectionRemove())

      XCTAssertFalse(node.canInsertTab())
      XCTAssertFalse(node.excludeFromCopy())
      XCTAssertFalse(node.isInline())
      XCTAssertFalse(node.canMergeWith(node: extraNode))
    }
  }

  func testSelectWithNilSelection() throws {
    try editor.update {
      createExampleNodeTree()

      if let elementNode = getNodeByKey(key: "0") as? ElementNode {
        let selection = try elementNode.select(anchorOffset: nil, focusOffset: nil)
        XCTAssertEqual(selection.anchor.key, "0")
        XCTAssertEqual(selection.focus.key, "0")
        XCTAssertEqual(selection.anchor.offset, 2)
        XCTAssertEqual(selection.focus.offset, 2)
        XCTAssertEqual(selection.anchor.type, SelectionType.element)
        XCTAssertEqual(selection.focus.type, SelectionType.element)
        XCTAssertTrue(selection.dirty)
      }
    }
  }

  func testSelectWithSelection() throws {
    try editor.update {
      createExampleNodeTree()

      view?.textView.selectedRange = NSRange(location: 2, length: 4)
      if let elementNode = getNodeByKey(key: "4") as? ElementNode {
        let selection = try elementNode.select(anchorOffset: 20, focusOffset: 10)
        XCTAssertEqual(selection.anchor.key, "4")
        XCTAssertEqual(selection.focus.key, "4")
        XCTAssertEqual(selection.anchor.offset, 20)
        XCTAssertEqual(selection.focus.offset, 10)
        XCTAssertEqual(selection.anchor.type, SelectionType.element)
        XCTAssertEqual(selection.focus.type, SelectionType.element)
        XCTAssertTrue(selection.dirty)
      }
    }
  }

  func testSelectStart() throws {
    try editor.update {

      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      guard let rootNode = getActiveEditorState()?.getRootNode() else {
        XCTFail("No root node")
        return
      }
      try rootNode.append([paragraphNode])

      let selection = try paragraphNode.selectStart()
      XCTAssert(selection.anchor.offset == 0)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class GarbageCollectionTests: XCTestCase {

  var view: LexicalView?
  var editor: Editor {
    get {
      guard let editor = view?.editor else {
        XCTFail("Editor unexpectedly nil")
        fatalError()
      }
      return editor
    }
  }

  override func setUp() {
    view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  override func tearDown() {
    view = nil
  }

  func testGarbageCollectDetachedNodes() throws {
    try editor.update {
      guard let editorState = getActiveEditorState(),
            let pendingEditorState = editor.testing_getPendingEditorState(),
            let rootNode = editorState.getRootNode() else {
        XCTFail("should have editor state")
        return
      }

      XCTAssertNotNil(editorState)
      XCTAssertEqual(editor.getEditorState().nodeMap.count, 2)

      let textNode = TextNode()
      try textNode.setText("A")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try rootNode.append([paragraphNode])
      let dirtyLeaves: DirtyNodeMap = [textNode.key: .userInitiated]

      XCTAssertEqual(editor.dirtyNodes.count, 4)

      try textNode.remove()

      XCTAssertEqual(pendingEditorState.nodeMap.count, 4)

      garbageCollectDetachedNodes(prevEditorState: editorState, editorState: editorState, dirtyLeaves: dirtyLeaves)
    }

    XCTAssertEqual(editor.getEditorState().nodeMap.count, 3)
  }

  func testGarbageCollectDetachedDeepChildNodes() throws {
    try editor.update {

      guard let editorState = getActiveEditorState(),
            let pendingEditorState = editor.testing_getPendingEditorState(),
            let rootNode = editorState.getRootNode() else {
        XCTFail("should have editor state")
        return
      }

      XCTAssertNotNil(editorState)

      let textNode = TextNode()
      try textNode.setText("A")
      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try rootNode.append([paragraphNode])

      XCTAssertNotNil(paragraphNode)
      XCTAssertEqual(paragraphNode.key, "2")
      XCTAssertEqual(textNode.getParent()?.key, "2")
      XCTAssertEqual(editorState.nodeMap.count, 4)
      XCTAssertEqual(editor.dirtyNodes.count, 4)

      guard let parentKey = textNode.getParent()?.key else {
        XCTFail("Failed to get parent key")
        return
      }

      try textNode.remove()

      XCTAssertEqual(pendingEditorState.nodeMap.count, 4)

      garbageCollectDetachedDeepChildNodes(
        node: paragraphNode,
        parentKey: parentKey,
        prevNodeMap: editor.getEditorState().nodeMap,
        nodeMap: editor.getEditorState().nodeMap,
        dirtyNodes: editor.dirtyNodes
      )
    }

    XCTAssertEqual(editor.getEditorState().nodeMap.count, 3)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class HistoryTests: XCTestCase {

  var view: LexicalView?
  var editor: Editor {
    get {
      guard let editor = view?.editor else {
        XCTFail("Editor unexpectedly nil")
        fatalError()
      }
      return editor
    }
  }

  override func setUp() {
    view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    addListeners()
  }

  private func addListeners() {
    guard let view else { XCTFail(); return }

    _ = view.editor.registerUpdateListener(listener: { (activeEditorState, previousEditorState, dirtyNodes) in
      view.editorHistory.applyChange(
        editorState: activeEditorState,
        prevEditorState: previousEditorState,
        dirtyNodes: dirtyNodes)
    })

    _ = view.editor.registerCommand(type: .undo, listener: { payload in
      view.editorHistory.applyCommand(type: .undo)
      return true
    })

    _ = view.editor.registerCommand(type: .redo, listener: { payload in
      view.editorHistory.applyCommand(type: .redo)
      return true
    })
  }

  override func tearDown() {
    view = nil
  }

  func testGetDirtyNodes() throws {
    try editor.update {
      guard let editorState = getActiveEditorState(), let rootNode = editorState.getRootNode() else {
        XCTFail("should have editor state")
        return
      }
      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      try rootNode.append([paragraphNode])

      XCTAssertEqual(editor.dirtyNodes.count, 5)
      XCTAssert(editor.dirtyNodes[textNode.key] != nil)
    }
  }

  func testGetChangeType() throws {
    try editor.update {
      guard let editorState = getActiveEditorState(), let rootNode = editorState.getRootNode() else {
        XCTFail("should have editor state")
        return
      }
      guard let pendingEditorState = editor.testing_getPendingEditorState() else {
        XCTFail("should have editor state")
        return
      }

      let changeType = try getChangeType(prevEditorState: editorState, nextEditorState: pendingEditorState, dirtyLeavesSet: editor.dirtyNodes, isComposing: false)

      XCTAssertNotNil(editorState)
      XCTAssertEqual(changeType, .other)

      let textNode = TextNode()
      try textNode.setText("Ã©")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try rootNode.append([paragraphNode])

      let changeType1 = try getChangeType(prevEditorState: editorState, nextEditorState: pendingEditorState, dirtyLeavesSet: editor.dirtyNodes, isComposing: true)
      XCTAssertEqual(changeType1, .composingCharacter)
    }
  }

  func testApplyHistory() throws {
    guard let view else { XCTFail(); return }

    XCTAssertEqual(view.textStorage.string, "", "Text storage should be empty")
    view.textView.insertText("A")
    XCTAssertEqual(view.textStorage.string, "A", "Text storage should contain A")
    view.editor.dispatchCommand(type: .undo)
    XCTAssertEqual(view.textStorage.string, "", "Text storage should be empty")
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

@testable import Lexical
import XCTest

class MutationsTests: XCTestCase {

  func testMutationIsControlledWhenDeletingAcrossNodes() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    XCTAssertEqual(view.textView.text, "hello world\nParagraph 2 contains another text node\n\nThird para.")

    view.textView.selectedRange = NSRange(location: 0, length: view.textView.textStorage.length)
    view.textView.insertText("1")

    try editor.getEditorState().read {
      guard let node = getNodeByKey(key: "1") else {
        XCTFail("Can't find node")
        return
      }

      XCTAssertEqual(node.getTextPart(), "1")
      XCTAssertNil(node.getParent()?.getPreviousSibling())
      XCTAssertNil(node.getParent()?.getNextSibling())
    }
  }
}