/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree.
 */

#import "FBXPath.h"

#import "FBConfiguration.h"
#import "FBExceptions.h"
#import "FBElementUtils.h"
#import "FBLogger.h"
#import "FBMacros.h"
#import "FBXMLGenerationOptions.h"
#import "FBXPathExtensions.h"
#import "FBXCElementSnapshotWrapper+Helpers.h"
#import "NSString+FBXMLSafeString.h"
#import "XCUIApplication.h"
#import "XCUIElement.h"
#import "XCUIElement+FBCaching.h"
#import "XCUIElement+FBUtilities.h"
#import "XCUIElement+FBWebDriverAttributes.h"
#import "XCTestPrivateSymbols.h"
#import "FBElementHelpers.h"
#import "FBXCAXClientProxy.h"
#import "FBXCAccessibilityElement.h"


@interface FBElementAttribute : NSObject

@property (nonatomic, readonly) id<FBElement> element;

+ (nonnull NSString *)name;
+ (nullable NSString *)valueForElement:(id<FBElement>)element;

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element;
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value;

+ (NSArray<Class> *)supportedAttributes;

@end

@interface FBTypeAttribute : FBElementAttribute

@end

@interface FBValueAttribute : FBElementAttribute

@end

@interface FBNameAttribute : FBElementAttribute

@end

@interface FBLabelAttribute : FBElementAttribute

@end

@interface FBEnabledAttribute : FBElementAttribute

@end

@interface FBVisibleAttribute : FBElementAttribute

@end

@interface FBAccessibleAttribute : FBElementAttribute

@end

@interface FBDimensionAttribute : FBElementAttribute

@end

@interface FBXAttribute : FBDimensionAttribute

@end

@interface FBYAttribute : FBDimensionAttribute

@end

@interface FBWidthAttribute : FBDimensionAttribute

@end

@interface FBHeightAttribute : FBDimensionAttribute

@end

@interface FBIndexAttribute : FBElementAttribute

@end

@interface FBHittableAttribute : FBElementAttribute

@end

@interface FBInternalIndexAttribute : FBElementAttribute

@property (nonatomic, nonnull, readonly) NSString* indexValue;

@end

@interface FBApplicationBundleIdAttribute : FBElementAttribute

@end

@interface FBApplicationPidAttribute : FBElementAttribute

@end

@interface FBPlaceholderValueAttribute : FBElementAttribute

@end

@interface FBNativeFrameAttribute : FBElementAttribute

@end

@interface FBTraitsAttribute : FBElementAttribute

@end

@interface FBMinValueAttribute : FBElementAttribute

@end

@interface FBMaxValueAttribute : FBElementAttribute

@end

@interface FBCustomActionsAttribute : FBElementAttribute

@end

#if TARGET_OS_TV

@interface FBFocusedAttribute : FBElementAttribute

@end

#endif

const static char *_UTF8Encoding = "UTF-8";

static NSString *const kXMLIndexPathKey = @"private_indexPath";
static NSString *const topNodeIndexPath = @"top";

@implementation FBXPath

+ (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery
{
  return [self throwException:name forQuery:xpathQuery detail:nil];
}

+ (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery detail:(nullable NSString *)detail
{
  NSString *reason;
  if (nil != detail) {
    reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\": %@", xpathQuery, detail];
  } else {
    reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
  }
  @throw [NSException exceptionWithName:name reason:reason userInfo:@{}];
  return nil;
}

+ (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
                                        options:(nullable FBXMLGenerationOptions *)options
{
  xmlDocPtr doc;
  xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
  int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
  if (rc < 0) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
  } else {
    BOOL hasScope = nil != options.scope && [options.scope length] > 0;
    if (hasScope) {
      rc = xmlTextWriterStartElement(writer,
                                     (xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
      if (rc < 0) {
        [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
      }
    }

    if (rc >= 0) {
      [self waitUntilStableWithElement:root];
      // If 'includeHittableInPageSource' setting is enabled, then use native snapshots
      // to calculate a more accurate value for the 'hittable' attribute.
      rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root
                                                        useNative:FBConfiguration.includeHittableInPageSource]
                                           writer:writer
                                     elementStore:nil
                                            query:nil
                              excludingAttributes:options.excludedAttributes];
    }

    if (rc >= 0 && hasScope) {
      rc = xmlTextWriterEndElement(writer);
      if (rc < 0) {
        [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
      }
    }

    if (rc >= 0) {
      rc = xmlTextWriterEndDocument(writer);
      if (rc < 0) {
        [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc];
      }
    }
  }
  if (rc < 0) {
    xmlFreeTextWriter(writer);
    xmlFreeDoc(doc);
    return nil;
  }
  int buffersize;
  xmlChar *xmlbuff;
  xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
  xmlFreeTextWriter(writer);
  xmlFreeDoc(doc);
  NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding];
  xmlFree(xmlbuff);
  return result;
}

+ (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
                                                    forQuery:(NSString *)xpathQuery
{
  xmlDocPtr doc;

  xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
  if (NULL == writer) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery];
    return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  }
  NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
  int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
  id<FBXCElementSnapshot> lookupScopeSnapshot = nil;
  id<FBXCElementSnapshot> contextRootSnapshot = nil;
  BOOL useNativeSnapshot = nil == xpathQuery
    ? NO
    : [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class];
  if (rc < 0) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
  } else {
    [self waitUntilStableWithElement:root];
    if (FBConfiguration.limitXpathContextScope) {
      lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot];
    } else {
      if ([root isKindOfClass:XCUIElement.class]) {
        lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application]
                                           useNative:useNativeSnapshot];
        contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
          ? nil
          : ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root
                                                                useNative:useNativeSnapshot]);
      } else {
        lookupScopeSnapshot = (id<FBXCElementSnapshot>)root;
        contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id<FBXCElementSnapshot>)root;
        while (nil != lookupScopeSnapshot.parent) {
          lookupScopeSnapshot = lookupScopeSnapshot.parent;
        }
      }
    }

    rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot
                                         writer:writer
                                   elementStore:elementStore
                                          query:xpathQuery
                            excludingAttributes:nil];
    if (rc >= 0) {
      rc = xmlTextWriterEndDocument(writer);
      if (rc < 0) {
        [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
      }
    }
  }
  if (rc < 0) {
    xmlFreeTextWriter(writer);
    xmlFreeDoc(doc);
    return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  }

  xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc
                                                          elementStore:elementStore.copy
                                                           forSnapshot:contextRootSnapshot];
  xmlNodePtr contextNode = NULL;
  if (NULL != contextNodeQueryResult) {
    xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval;
    if (!xmlXPathNodeSetIsEmpty(nodeSet)) {
      contextNode = nodeSet->nodeTab[0];
    }
  }
  NSString *evaluationError = nil;
  xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
                                        document:doc
                                     contextNode:contextNode
                                    errorMessage:&evaluationError];
  if (NULL != contextNodeQueryResult) {
    xmlXPathFreeObject(contextNodeQueryResult);
  }
  if (NULL == queryResult) {
    xmlFreeTextWriter(writer);
    xmlFreeDoc(doc);
    return [self throwException:FBInvalidXPathException forQuery:xpathQuery detail:evaluationError];
  }

  NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
                                                 elementStore:elementStore];
  xmlXPathFreeObject(queryResult);
  xmlFreeTextWriter(writer);
  xmlFreeDoc(doc);
  if (nil == matchingSnapshots) {
    return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  }
  return matchingSnapshots;
}

+ (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
                         elementStore:(NSMutableDictionary *)elementStore
{
  if (xmlXPathNodeSetIsEmpty(nodeSet)) {
    return @[];
  }
  NSMutableArray *matchingSnapshots = [NSMutableArray array];
  const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String];
  for (NSInteger i = 0; i < nodeSet->nodeNr; i++) {
    xmlNodePtr currentNode = nodeSet->nodeTab[i];
    xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName);
    if (NULL == attrValue) {
      [FBLogger log:@"Failed to invoke libxml2>xmlGetProp"];
      return nil;
    }
    id<FBXCElementSnapshot> element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]];
    if (element) {
      [matchingSnapshots addObject:element];
    }
    xmlFree(attrValue);
  }
  return matchingSnapshots.copy;
}

+ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc
                                     elementStore:(NSDictionary<NSString *, id<FBXCElementSnapshot>> *)elementStore
                                      forSnapshot:(nullable id<FBXCElementSnapshot>)snapshot
{
  if (nil == snapshot) {
    return NULL;
  }

  NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]];
  if (nil == contextRootUid) {
    return NULL;
  }

  for (NSString *key in elementStore) {
    id<FBXCElementSnapshot> value = [elementStore objectForKey:key];
    NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]];
    if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) {
      continue;
    }
    NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key];
    xmlXPathObjectPtr queryResult = [self evaluate:indexQuery
                                          document:doc
                                       contextNode:NULL];
    if (NULL != queryResult) {
      return queryResult;
    }
  }
  return NULL;
}

+ (NSSet<Class> *)elementAttributesWithXPathQuery:(NSString *)query
{
  if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) {
    // read all element attributes if 'star' attribute name pattern is used in xpath query
    return [NSSet setWithArray:FBElementAttribute.supportedAttributes];
  }
  NSMutableSet<Class> *result = [NSMutableSet set];
  for (Class attributeCls in FBElementAttribute.supportedAttributes) {
    if ([query rangeOfString:[NSString stringWithFormat:@"[^\\w@]@%@[^\\w]", [attributeCls name]] options:NSRegularExpressionSearch].location != NSNotFound) {
      [result addObject:attributeCls];
    }
  }
  return result.copy;
}

+ (int)xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
                                 writer:(xmlTextWriterPtr)writer
                           elementStore:(nullable NSMutableDictionary *)elementStore
                                  query:(nullable NSString*)query
                    excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
{
  // Trying to be smart here and only including attributes, that were asked in the query, to the resulting document.
  // This may speed up the lookup significantly in some cases
  NSMutableSet<Class> *includedAttributes;
  if (nil == query) {
    includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes];
    if (!FBConfiguration.includeHittableInPageSource) {
      // The hittable attribute is expensive to calculate for each snapshot item
      // thus we only include it when requested explicitly
      [includedAttributes removeObject:FBHittableAttribute.class];
    }
    if (!FBConfiguration.includeNativeFrameInPageSource) {
      // Include nativeFrame only when requested
      [includedAttributes removeObject:FBNativeFrameAttribute.class];
    }
    if (!FBConfiguration.includeMinMaxValueInPageSource) {
      // minValue/maxValue are retrieved from private APIs and may be slow on deep trees
      [includedAttributes removeObject:FBMinValueAttribute.class];
      [includedAttributes removeObject:FBMaxValueAttribute.class];
    }
    if (!FBConfiguration.includeCustomActionsInPageSource) {
      // customActions are retrieved from accessibility attributes and may be slow on deep trees
      [includedAttributes removeObject:FBCustomActionsAttribute.class];
    }
    if (nil != excludedAttributes) {
      for (NSString *excludedAttributeName in excludedAttributes) {
        for (Class supportedAttribute in FBElementAttribute.supportedAttributes) {
          if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) {
            [includedAttributes removeObject:supportedAttribute];
            break;
          }
        }
      }
    }
  } else {
    includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy;
  }
  [FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes];

  int rc = [self writeXmlWithRootElement:root
                               indexPath:(elementStore != nil ? topNodeIndexPath : nil)
                            elementStore:elementStore
                      includedAttributes:includedAttributes.copy
                                  writer:writer];
  if (rc < 0) {
    [FBLogger log:@"Failed to generate XML presentation of a screen element"];
    return rc;
  }
  return 0;
}

+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
                     document:(xmlDocPtr)doc
                  contextNode:(nullable xmlNodePtr)contextNode
{
  return [self evaluate:xpathQuery document:doc contextNode:contextNode errorMessage:nil];
}

+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
                     document:(xmlDocPtr)doc
                  contextNode:(nullable xmlNodePtr)contextNode
                 errorMessage:(NSString * _Nullable * _Nullable)errorMessage
{
  xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
  if (NULL == xpathCtx) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
    return NULL;
  }
  xpathCtx->node = NULL == contextNode ? doc->children : contextNode;

  FBXPathExtensions *extensions = [FBXPathExtensions new];
  [extensions registerFunctionsWithContext:xpathCtx];

  xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
  if (NULL == xpathObj) {
    NSString *detail = extensions.lastEvaluationError;
    if (NULL != errorMessage) {
      *errorMessage = detail;
    }
    if (nil != detail) {
      [FBLogger logFmt:@"Failed to evaluate XPath query \"%@\": %@", xpathQuery, detail];
    } else {
      [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
    }
    xmlXPathFreeContext(xpathCtx);
    return NULL;
  }
  xmlXPathFreeContext(xpathCtx);
  return xpathObj;
}

+ (nullable NSString *)safeXmlStringWithString:(NSString *)str
{
  return [str fb_xmlSafeStringWithReplacement:@""];
}

+ (int)recordElementAttributes:(xmlTextWriterPtr)writer
                    forElement:(id<FBXCElementSnapshot>)element
                     indexPath:(nullable NSString *)indexPath
            includedAttributes:(nullable NSSet<Class> *)includedAttributes
{
  for (Class attributeCls in FBElementAttribute.supportedAttributes) {
    // include all supported attributes by default unless enumerated explicitly
    if (includedAttributes && ![includedAttributes containsObject:attributeCls]) {
      continue;
    }
    // Text-input placeholder (only for elements that support inner text)
    if ((attributeCls == FBPlaceholderValueAttribute.class) &&
        !FBDoesElementSupportInnerText(element.elementType)) {
      continue;
    }
    // Only for elements that support min/max value
    if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) &&
        !FBDoesElementSupportMinMaxValue(element.elementType)) {
      continue;
    }
    int rc = [attributeCls recordWithWriter:writer
                                 forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]];
    if (rc < 0) {
      return rc;
    }
  }

  if (nil != indexPath) {
    // index path is the special case
    return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath];
  }
  if (element.elementType == XCUIElementTypeApplication) {
    // only record process identifier and bundle identifier for the application element
    int pid = [element.accessibilityElement processIdentifier];
    if (pid > 0) {
      int rc = [FBApplicationPidAttribute recordWithWriter:writer
                                                  forValue:[NSString stringWithFormat:@"%d", pid]];
      if (rc < 0) {
        return rc;
      }
      XCUIApplication *app = [[FBXCAXClientProxy sharedClient]
                              monitoredApplicationWithProcessIdentifier:pid];
      NSString *bundleID = [app bundleID];
      if (nil != bundleID) {
        rc = [FBApplicationBundleIdAttribute recordWithWriter:writer
                                                     forValue:bundleID];
        if (rc < 0) {
          return rc;
        }
      }
    }
  }
  return 0;
}

+ (int)writeXmlWithRootElement:(id<FBXCElementSnapshot>)root
                     indexPath:(nullable NSString *)indexPath
                  elementStore:(nullable NSMutableDictionary *)elementStore
            includedAttributes:(nullable NSSet<Class> *)includedAttributes
                        writer:(xmlTextWriterPtr)writer
{
  NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil);

  NSArray<id<FBXCElementSnapshot>> *children = root.children;

  if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
    [elementStore setObject:root forKey:topNodeIndexPath];
  }

  FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root];
  int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]);
  if (rc < 0) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc];
    return rc;
  }

  rc = [self recordElementAttributes:writer
                          forElement:root
                           indexPath:indexPath
                  includedAttributes:includedAttributes];
  if (rc < 0) {
    return rc;
  }

  for (NSUInteger i = 0; i < [children count]; i++) {
    @autoreleasepool {
      id<FBXCElementSnapshot> childSnapshot = [children objectAtIndex:i];
      NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil;
      if (elementStore != nil && newIndexPath != nil) {
        [elementStore setObject:childSnapshot forKey:(id)newIndexPath];
      }
      rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot]
                               indexPath:newIndexPath
                            elementStore:elementStore
                      includedAttributes:includedAttributes
                                  writer:writer];
      if (rc < 0) {
        return rc;
      }
    }
  }

  rc = xmlTextWriterEndElement(writer);
  if (rc < 0) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
    return rc;
  }
  return 0;
}

+ (id<FBXCElementSnapshot>)snapshotWithRoot:(id<FBElement>)root
                                  useNative:(BOOL)useNative
{
  if (![root isKindOfClass:XCUIElement.class]) {
    return (id<FBXCElementSnapshot>)root;
  }

  // https://github.com/appium/appium-xcuitest-driver/pull/2565
  if (useNative) {
    return [(XCUIElement *)root fb_nativeSnapshot];
  }
  // https://github.com/appium/WebDriverAgent/issues/1085
  return [root isKindOfClass:XCUIApplication.class] && !FBConfiguration.enforceCustomSnapshots
    ? [(XCUIElement *)root fb_standardSnapshot]
    : [(XCUIElement *)root fb_customSnapshot];
}

+ (void)waitUntilStableWithElement:(id<FBElement>)root
{
  if ([root isKindOfClass:XCUIElement.class]) {
    // If the app is not idle state while we retrieve the visiblity state
    // then the snapshot retrieval operation might freeze and time out
    [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
  }
}

@end


static NSString *const FBAbstractMethodInvocationException = @"AbstractMethodInvocationException";

@implementation FBElementAttribute

- (instancetype)initWithElement:(id<FBElement>)element
{
  self = [super init];
  if (self) {
    _element = element;
  }
  return self;
}

+ (NSString *)name
{
  NSString *errMsg = [NSString stringWithFormat:@"The abstract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)];
  @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  NSString *errMsg = [NSString stringWithFormat:@"The abstract method -(NSString *)value is expected to be overriden by %@", NSStringFromClass(self.class)];
  @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
}

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element
{
  NSString *value = [self valueForElement:element];
  return [self recordWithWriter:writer forValue:value];
}

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value
{
  if (nil == value) {
    // Skip the attribute if the value equals to nil
    return 0;
  }
  int rc = xmlTextWriterWriteAttribute(writer,
                                       (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
                                       (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
  if (rc < 0) {
    [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
  }
  return rc;
}

+ (NSArray<Class> *)supportedAttributes
{
  // The list of attributes to be written for each XML node
  // The enumeration order does matter here
  return @[FBTypeAttribute.class,
           FBValueAttribute.class,
           FBNameAttribute.class,
           FBLabelAttribute.class,
           FBEnabledAttribute.class,
           FBVisibleAttribute.class,
           FBAccessibleAttribute.class,
#if TARGET_OS_TV
           FBFocusedAttribute.class,
#endif
           FBXAttribute.class,
           FBYAttribute.class,
           FBWidthAttribute.class,
           FBHeightAttribute.class,
           FBIndexAttribute.class,
           FBHittableAttribute.class,
           FBPlaceholderValueAttribute.class,
           FBTraitsAttribute.class,
           FBNativeFrameAttribute.class,
           FBMinValueAttribute.class,
           FBMaxValueAttribute.class,
           FBCustomActionsAttribute.class,
          ];
}

@end

@implementation FBTypeAttribute

+ (NSString *)name
{
  return @"type";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return element.wdType;
}

@end

@implementation FBValueAttribute

+ (NSString *)name
{
  return @"value";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  id idValue = element.wdValue;
  if ([idValue isKindOfClass:[NSValue class]]) {
    return [idValue stringValue];
  } else if ([idValue isKindOfClass:[NSString class]]) {
    return idValue;
  }
  return [idValue description];
}

@end

@implementation FBNameAttribute

+ (NSString *)name
{
  return @"name";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return element.wdName;
}

@end

@implementation FBLabelAttribute

+ (NSString *)name
{
  return @"label";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return element.wdLabel;
}

@end

@implementation FBEnabledAttribute

+ (NSString *)name
{
  return @"enabled";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return FBBoolToString(element.wdEnabled);
}

@end

@implementation FBVisibleAttribute

+ (NSString *)name
{
  return @"visible";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return FBBoolToString(element.wdVisible);
}

@end

@implementation FBAccessibleAttribute

+ (NSString *)name
{
  return @"accessible";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return FBBoolToString(element.wdAccessible);
}

@end

#if TARGET_OS_TV

@implementation FBFocusedAttribute

+ (NSString *)name
{
  return @"focused";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return FBBoolToString(element.wdFocused);
}

@end

#endif

@implementation FBDimensionAttribute

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return [NSString stringWithFormat:@"%@", [element.wdRect objectForKey:[self name]]];
}

@end

@implementation FBXAttribute

+ (NSString *)name
{
  return @"x";
}

@end

@implementation FBYAttribute

+ (NSString *)name
{
  return @"y";
}

@end

@implementation FBWidthAttribute

+ (NSString *)name
{
  return @"width";
}

@end

@implementation FBHeightAttribute

+ (NSString *)name
{
  return @"height";
}

@end

@implementation FBIndexAttribute

+ (NSString *)name
{
  return @"index";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return [NSString stringWithFormat:@"%lu", element.wdIndex];
}

@end

@implementation FBHittableAttribute

+ (NSString *)name
{
  return @"hittable";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return FBBoolToString(element.wdHittable);
}

@end

@implementation FBInternalIndexAttribute

+ (NSString *)name
{
  return kXMLIndexPathKey;
}

@end

@implementation FBApplicationBundleIdAttribute : FBElementAttribute

+ (NSString *)name
{
  return @"bundleId";
}

@end

@implementation FBApplicationPidAttribute : FBElementAttribute

+ (NSString *)name
{
  return @"processId";
}

@end

@implementation FBPlaceholderValueAttribute

+ (NSString *)name
{
  return @"placeholderValue";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return element.wdPlaceholderValue;
}
@end

@implementation FBNativeFrameAttribute

+ (NSString *)name
{
  return @"nativeFrame";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return NSStringFromCGRect(element.wdNativeFrame);
}
@end

@implementation FBTraitsAttribute

+ (NSString *)name
{
  return @"traits";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return element.wdTraits;
}

@end

@implementation FBMinValueAttribute

+ (NSString *)name
{
  return @"minValue";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return [element.wdMinValue stringValue];
}

@end

@implementation FBMaxValueAttribute

+ (NSString *)name
{
  return @"maxValue";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return [element.wdMaxValue stringValue];
}

@end

@implementation FBCustomActionsAttribute

+ (NSString *)name
{
  return @"customActions";
}

+ (NSString *)valueForElement:(id<FBElement>)element
{
  return element.wdCustomActions;
}

@end
