// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QApplication>
#include <QtWidgets>
#include <QTest>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qregularexpression.h>

// some versions of CALayer.h use 'slots' as an identifier
#define QT_NO_KEYWORDS

#include <QtWidgets/qapplication.h>
#include <QtWidgets/qlineedit.h>
#include <QtWidgets/qpushbutton.h>
#include <QtWidgets>
#include <QTest>
#include <unistd.h>

#import <AppKit/AppKit.h>
#import <ApplicationServices/ApplicationServices.h>

QT_USE_NAMESPACE

struct AXErrorTag {
    AXError err;
    explicit AXErrorTag(AXError theErr) : err(theErr) {}
};

QDebug operator<<(QDebug dbg, AXErrorTag err)
{
    QDebugStateSaver saver(dbg);

    const char *errDesc = 0;
    const char *errName = 0;
    switch (err.err) {
#define HANDLE_ERR(error, desc) case kAXError##error: errName = "kAXError" #error; errDesc = desc; break
        HANDLE_ERR(Success, "Success");
        HANDLE_ERR(Failure, "A system error occurred, such as the failure to allocate an object.");
        HANDLE_ERR(IllegalArgument, "An illegal argument was passed to the function.");
        HANDLE_ERR(InvalidUIElement, "The AXUIElementRef passed to the function is invalid.");
        HANDLE_ERR(InvalidUIElementObserver, "The AXObserverRef passed to the function is not a valid observer.");
        HANDLE_ERR(CannotComplete, "The function cannot complete because messaging failed in some way or because the application with which the function is communicating is busy or unresponsive.");
        HANDLE_ERR(AttributeUnsupported, "The attribute is not supported by the AXUIElementRef.");
        HANDLE_ERR(ActionUnsupported, "The action is not supported by the AXUIElementRef.");
        HANDLE_ERR(NotificationUnsupported, "The notification is not supported by the AXUIElementRef.");
        HANDLE_ERR(NotImplemented, "Indicates that the function or method is not implemented (this can be returned if a process does not support the accessibility API).");
        HANDLE_ERR(NotificationAlreadyRegistered, "This notification has already been registered for.");
        HANDLE_ERR(NotificationNotRegistered, "Indicates that a notification is not registered yet.");
        HANDLE_ERR(APIDisabled, "The accessibility API is disabled (as when, for example, the user deselects \"Enable access for assistive devices\" in Universal Access Preferences).");
        HANDLE_ERR(NoValue, "The requested value or AXUIElementRef does not exist.");
        HANDLE_ERR(ParameterizedAttributeUnsupported, "The parameterized attribute is not supported by the AXUIElementRef.");
        HANDLE_ERR(NotEnoughPrecision, "Not enough precision.");
        default: errName = "<unknown error>"; errDesc = "UNKNOWN ERROR"; break;
    }
#undef HANDLE_ERR

    dbg.nospace() << "AXError(value=" << err.err << ", name=" << errName << ", description=\"" << errDesc << "\")";

    return dbg;
}

@interface TestAXObject : NSObject
{
    AXUIElementRef reference;
    bool axError;
}
    @property (readonly) NSString *role;
    @property (readonly) NSString *roleDescription;
    @property (readonly) NSString *title;
    @property (readonly) NSString *description;
    @property (readonly) NSString *value;
    @property (readonly) CGRect rect;
    @property (readonly) NSArray *actions;
    @property (readonly) bool valid;
@end

@implementation TestAXObject

- (instancetype)initWithAXUIElementRef:(AXUIElementRef)ref {

    if ((self = [super init])) {
        reference = ref;
        if (!reference) {
            qCritical() << "Null reference!";
            axError = true;
        } else {
            axError = false;
        }
    }
    return self;
}

- (AXUIElementRef) ref { return reference; }
- (bool)errorOccurred { return axError; }
- (void) print {
    NSLog(@"Accessible Object role: '%@', title: '%@', description: '%@', value: '%@', rect: '%@'", self.role, self.title, self.description, self.value, NSStringFromRect(NSRectFromCGRect(self.rect)));
    NSLog(@"    Children: %ld", [[self childList] count]);
}

- (NSArray*) windowList
{
    NSArray *list;
    AXUIElementCopyAttributeValues(
                reference,
                kAXWindowsAttribute,
                0, 100, /*min, max*/
                (CFArrayRef *) &list);
    return list;
}

- (NSArray*) childList
{
    NSArray *list;
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValues(reference, kAXChildrenAttribute,
                                                                 0, 100, /*min, max*/
                                                                 (CFArrayRef *) &list))) {
        axError = true;
        qDebug() << "AXUIElementCopyAttributeValue(kAXChildrenAttribute) returned error = "
                 << AXErrorTag(err) << "with reference" << reference;
    }
    return list;
}

- (NSArray *)tableRows
{
    NSArray *arr;
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValues(reference, kAXRowsAttribute,
                                                                    0, 100, /*min, max*/
                                                                    (CFArrayRef *) &arr))) {
        axError = true;
        qDebug() << "AXUIElementCopyAttributeValue(kAXRowsAttribute) returned error = "
                 << AXErrorTag(err) << "with reference" << reference;
    }
    return arr;
}

- (NSArray *)tableColumns
{
    NSArray *arr;
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValues(reference, kAXColumnsAttribute,
                                                                    0, 100, /*min, max*/
                                                                    (CFArrayRef *) &arr))) {
        axError = true;
        qDebug() << "AXUIElementCopyAttributeValue(kAXColumnsAttribute) returned error = "
                 << AXErrorTag(err) << "with reference" << reference;
    }
    return arr;
}

- (NSArray *)tabs
{
    NSArray *arr;
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValues(reference, kAXTabsAttribute,
                                                                    0, 100, /*min, max*/
                                                                    (CFArrayRef *) &arr))) {
        axError = true;
        qDebug() << "AXUIElementCopyAttributeValue(kAXTabsAttribute) returned error = "
                 << AXErrorTag(err) << "with reference" << reference;
    }
    return arr;
}

- (AXUIElementRef) findDirectChildByRole: (CFStringRef) role
{
    TestAXObject *result = nil;
    NSArray *childList = [self childList];
    for (id child in childList) {
        TestAXObject *childObject = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)child];
        if ([childObject.role isEqualToString:(NSString*)role]) {
            result = childObject;
            break;
        }
    }
    AXUIElementRef ret = [result ref];
    [result release];
    return ret;
}

+ (TestAXObject *) getApplicationAXObject
{
    pid_t pid = getpid();
    AXUIElementRef appRef = AXUIElementCreateApplication(pid);
    TestAXObject *appObject = [[TestAXObject alloc] initWithAXUIElementRef: appRef];
    return appObject;
}

+ (NSInteger)_numberFromValue:(CFTypeRef)value
{
    NSInteger number = -1;
    if (!CFNumberGetValue((CFNumberRef)value, kCFNumberNSIntegerType, &number))
    {
        qDebug() << "Could not get NSInteger value out of CFNumberRef";
    }
    return number;
}

+ (BOOL)_boolFromValue:(CFTypeRef)value
{
    return CFBooleanGetValue((CFBooleanRef)value);
}

+ (NSRange)_rangeFromValue:(CFTypeRef)value
{
    CFRange cfRange;
    NSRange range = NSMakeRange(0, 0);

    if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCFRangeType), &cfRange))
        qDebug() << "Could not get CFRange value out of AXValueRef";
    else if (cfRange.location < 0 || cfRange.length < 0)
        qDebug() << "Cannot convert CFRange with negative location or length to NSRange";
    else if (static_cast<uintmax_t>(cfRange.location) > NSUIntegerMax || static_cast<uintmax_t>(cfRange.length) > NSUIntegerMax)
        qDebug() << "Cannot convert CFRange with location or length out of bounds for NSUInteger";
    else
    {
        range.length = static_cast<NSUInteger>(cfRange.length);
        range.location = static_cast<NSUInteger>(cfRange.location);
    }

    return range;
}

+ (NSRect)_rectFromValue:(CFTypeRef)value
{
    NSRect rect = NSMakeRect(0, 0, 0, 0);
    if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCGRectType), reinterpret_cast<CGRect*>(&rect)))
    {
        qDebug() << "Could not get CGRect value out of AXValueRef";
    }
    return rect;
}

+ (NSPoint)_pointFromValue:(CFTypeRef)value
{
    NSPoint point = NSMakePoint(0, 0);
    if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCGPointType), reinterpret_cast<CGPoint*>(&point)))
    {
        qDebug() << "Could not get CGPoint value out of AXValueRef";
    }
    return point;
}

+ (NSSize)_sizeFromValue:(CFTypeRef)value
{
    NSSize size = NSMakeSize(0, 0);
    if (!AXValueGetValue(AXValueRef(value), AXValueType(kAXValueCGSizeType), reinterpret_cast<CGSize*>(&size)))
    {
        qDebug() << "Could not get CGSize value out of AXValueRef";
    }
    return size;
}

- (CFTypeRef)_attributeValue:(CFStringRef)attribute
{
    CFTypeRef value = NULL;
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementCopyAttributeValue(reference, attribute, &value))) {
        axError = true;
        qDebug() << "AXUIElementCopyAttributeValue(" << QString::fromCFString(attribute) << ") returned error = " << AXErrorTag(err);
    }
    return value;
}

- (NSString*)_stringAttributeValue:(CFStringRef)attribute
{
    return (NSString*)[self _attributeValue:attribute];
}

- (NSInteger)_numberAttributeValue:(CFStringRef)attribute
{
    return [[self class] _numberFromValue:[self _attributeValue:attribute]];
}

- (BOOL)_boolAttributeValue:(CFStringRef)attribute
{
    return [[self class] _boolFromValue:[self _attributeValue:attribute]];
}

- (NSRange)_rangeAttributeValue:(CFStringRef)attribute
{
    return [[self class] _rangeFromValue:[self _attributeValue:attribute]];
}

- (NSRect)_rectAttributeValue:(CFStringRef)attribute
{
    return [[self class] _rectFromValue:[self _attributeValue:attribute]];
}

- (NSPoint)_pointAttributeValue:(CFStringRef)attribute
{
    return [[self class] _pointFromValue:[self _attributeValue:attribute]];
}

- (NSSize)_sizeAttributeValue:(CFStringRef)attribute
{
    return [[self class] _sizeFromValue:[self _attributeValue:attribute]];
}

- (CFTypeRef)_attributeValue:(CFStringRef)attribute forParameter:(CFTypeRef)parameter
{
    CFTypeRef value = NULL;
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementCopyParameterizedAttributeValue(reference, attribute, parameter, &value))) {
        axError = true;
        CFStringRef description = CFCopyDescription(parameter);
        qDebug() << "AXUIElementCopyParameterizedAttributeValue(" << QString::fromCFString(attribute) << ", parameter=" << QString::fromCFString(description) << ") returned error = " << AXErrorTag(err);
        CFRelease(description);
    }
    return value;
}

- (CFTypeRef)_attributeValue:(CFStringRef)attribute forRange:(NSRange)aRange
{
    CFRange cfRange = CFRangeMake(aRange.location, aRange.length);
    AXValueRef range = AXValueCreate(AXValueType(kAXValueCFRangeType), &cfRange);
    CFTypeRef value =  [self _attributeValue:attribute forParameter:range];
    CFRelease(range);
    return value;
}

- (CFTypeRef)_attributeValue:(CFStringRef)attribute forNumber:(NSInteger)aNumber
{
    CFNumberRef number = CFNumberCreate(NULL, kCFNumberNSIntegerType, &aNumber);
    CFTypeRef value = [self _attributeValue:attribute forParameter:number];
    CFRelease(number);
    return value;
}

- (CFTypeRef)_attributeValue:(CFStringRef)attribute forPoint:(CGPoint)aPoint
{
    AXValueRef point = AXValueCreate(AXValueType(kAXValueCGPointType), &aPoint);
    CFTypeRef value = [self _attributeValue:attribute forParameter:point];
    CFRelease(point);
    return value;
}

- (NSArray*)actions
{
    AXError err;
    CFArrayRef actions;

    if (kAXErrorSuccess != (err = AXUIElementCopyActionNames(reference, &actions))) {
        axError = true;
        qDebug() << "AXUIElementCopyActionNames(...) returned error = " << AXErrorTag(err);
    }

    return (NSArray*)actions;
}

- (void)performAction:(CFStringRef)action
{
    AXError err;

    if (kAXErrorSuccess != (err = AXUIElementPerformAction(reference, action))) {
        axError = true;
        qDebug() << "AXUIElementPerformAction("  << QString::fromCFString(action) << ") returned error = " << AXErrorTag(err);
    }
}

- (bool)                valid { return reference != nil; }
- (NSString*)           role { return [self _stringAttributeValue:kAXRoleAttribute]; }
- (NSString*)           roleDescription { return [self _stringAttributeValue:kAXRoleDescriptionAttribute]; }
- (NSString*)           title { return [self _stringAttributeValue:kAXTitleAttribute]; }
- (NSString*)           description { return [self _stringAttributeValue:kAXDescriptionAttribute]; }
- (NSString*)           value { return [self _stringAttributeValue:kAXValueAttribute]; }
- (NSInteger)           valueNumber { return [self _numberAttributeValue:kAXValueAttribute]; }
- (NSRect)              rect
{
    NSRect rect;
    rect.origin = [self _pointAttributeValue:kAXPositionAttribute];
    rect.size = [self _sizeAttributeValue:kAXSizeAttribute];
    return rect;
}
- (AXUIElementRef)      parent { return (AXUIElementRef)[self _attributeValue:kAXParentAttribute]; }
- (AXUIElementRef)      window { return (AXUIElementRef)[self _attributeValue:kAXWindowAttribute]; }
- (BOOL)                focused { return [self _boolAttributeValue:kAXFocusedAttribute]; }
- (NSInteger)           numberOfCharacters { return [self _numberAttributeValue:kAXNumberOfCharactersAttribute]; }
- (NSString*)           selectedText { return [self _stringAttributeValue:kAXSelectedTextAttribute]; }
- (NSRange)             selectedTextRange { return [self _rangeAttributeValue:kAXSelectedTextRangeAttribute]; }
- (NSRange)             visibleCharacterRange  { return [self _rangeAttributeValue:kAXVisibleCharacterRangeAttribute]; }
- (NSString*)           help { return [self _stringAttributeValue:kAXHelpAttribute]; }
- (NSInteger)           insertionPointLineNumber { return [self _numberAttributeValue:kAXInsertionPointLineNumberAttribute]; }

- (NSInteger)           lineForIndex:(NSInteger)index { return [[self class] _numberFromValue:[self _attributeValue:kAXLineForIndexParameterizedAttribute forNumber:index]]; }
- (NSRange)             rangeForLine:(NSInteger)line { return [[self class] _rangeFromValue:[self _attributeValue:kAXRangeForLineParameterizedAttribute forNumber:line]]; }
- (NSString*)           stringForRange:(NSRange)range { return (NSString*)[self _attributeValue:kAXStringForRangeParameterizedAttribute forRange:range]; }
- (NSAttributedString*) attributedStringForRange:(NSRange)range { return (NSAttributedString*)[self _attributeValue:kAXAttributedStringForRangeParameterizedAttribute forRange:range]; }
- (NSRect)              boundsForRange:(NSRange)range { return [[self class] _rectFromValue:[self _attributeValue:kAXBoundsForRangeParameterizedAttribute forRange:range]]; }
- (NSRange)             styleRangeForIndex:(NSInteger)index { return [[self class] _rangeFromValue:[self _attributeValue:kAXStyleRangeForIndexParameterizedAttribute forNumber:index]]; }

@end

QVector<int> notificationList;

void observerCallback(AXObserverRef /*observer*/, AXUIElementRef /*element*/, CFStringRef notification, void *)
{
    if ([(NSString*)notification isEqualToString: NSAccessibilityFocusedUIElementChangedNotification])
        notificationList.append(QAccessible::Focus);
    else if ([(NSString*)notification isEqualToString: NSAccessibilityValueChangedNotification])
        notificationList.append(QAccessible::ValueChanged);
    else
        notificationList.append(-1);
}

class AccessibleTestWindow : public QWidget
{
    Q_OBJECT
public:
    AccessibleTestWindow()
    {
        new QHBoxLayout(this);
    }

    void addWidget(QWidget* widget)
    {
        layout()->addWidget(widget);
        widget->show();
        QVERIFY(QTest::qWaitForWindowExposed(widget));
    }

    void clearChildren()
    {
        qDeleteAll(children());
        new QHBoxLayout(this);
    }
};

class tst_QAccessibilityMac : public QObject
{
Q_OBJECT
private Q_SLOTS:
    void init();
    void cleanup();

    void singleWidgetTest();
    void lineEditTest();
    void hierarchyTest();
    void notificationsTest();
    void checkBoxTest();
    void tableViewTest();
    void treeViewTest();
    void tabBarTest();
    void windowTest();

private:
    AccessibleTestWindow *m_window;
};


void tst_QAccessibilityMac::init()
{
    m_window = new AccessibleTestWindow();
    m_window->setWindowTitle(QString("Test window - %1").arg(QTest::currentTestFunction()));
    m_window->show();
    m_window->resize(400, 400);

    QVERIFY(QTest::qWaitForWindowExposed(m_window));
}

void tst_QAccessibilityMac::cleanup()
{
    delete m_window;
}

void tst_QAccessibilityMac::singleWidgetTest()
{
    delete m_window;
    m_window = 0;

    QLineEdit *le = new QLineEdit();
    le->setText("button");
    le->show();
    QVERIFY(QTest::qWaitForWindowExposed(le));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    QTRY_VERIFY(appObject.windowList.count == 1);

    AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];

    AXUIElementRef lineEditRef = [window findDirectChildByRole: kAXTextFieldRole];
    QVERIFY(lineEditRef != nil);
    TestAXObject *lineEdit = [[TestAXObject alloc] initWithAXUIElementRef: lineEditRef];
    QVERIFY([[lineEdit value] isEqualToString:@"button"]);

    // Access invalid reference, should return empty value
    delete le;
    QCoreApplication::processEvents();
    TestAXObject *lineEditInvalid = [[TestAXObject alloc] initWithAXUIElementRef: lineEditRef];
    QTest::ignoreMessage(QtDebugMsg, QRegularExpression("kAXErrorInvalidUIElement"));
    QVERIFY([[lineEditInvalid value] length] == 0);
}

void tst_QAccessibilityMac::lineEditTest()
{
    QLineEdit *lineEdit = new QLineEdit(m_window);
    lineEdit->setText("a11y test QLineEdit");
    m_window->addWidget(lineEdit);
    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    // one window
    QTRY_VERIFY(appObject.windowList.count == 1);
    AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];

    QVERIFY([window rect].size.width == 400);
    // height of window includes title bar
    QVERIFY([window rect].size.height >= 400);

    QVERIFY([window.title isEqualToString:@"Test window - lineEditTest"]);

    // children of window:
    AXUIElementRef lineEditElement = [window findDirectChildByRole: kAXTextFieldRole];
    QVERIFY(lineEditElement != nil);

    TestAXObject *le = [[TestAXObject alloc] initWithAXUIElementRef: lineEditElement];
    NSString *value = @"a11y test QLineEdit";
    QVERIFY([le.value isEqualToString:value]);
    QVERIFY(value.length <= NSIntegerMax);
    QVERIFY(le.numberOfCharacters == static_cast<NSInteger>(value.length));
    const NSRange ranges[] = {
        { 0, 0},
        { 0, 1},
        { 0, 5},
        { 5, 0},
        { 5, 1},
        { 0, value.length},
        { value.length, 0},
    };
    for (size_t i = 0; i < sizeof(ranges)/sizeof(ranges[0]); ++i) {
        NSRange range = ranges[i];
        NSString *expectedSubstring = [value substringWithRange:range];
        NSString *actualSubstring = [le stringForRange:range];
        NSString *actualAttributedSubstring = [le attributedStringForRange:range].string;
        QVERIFY([actualSubstring isEqualTo:expectedSubstring]);
        QVERIFY([actualAttributedSubstring isEqualTo:expectedSubstring]);
    }
}

void tst_QAccessibilityMac::hierarchyTest()
{
    QWidget *w = new QWidget(m_window);
    m_window->addWidget(w);

    w->setLayout(new QVBoxLayout());
    QPushButton *b = new QPushButton(w);
    w->layout()->addWidget(b);
    b->setText("I am a button");

    QPushButton *b2 = new QPushButton(w);
    w->layout()->addWidget(b2);
    b2->setText("Button 2");

    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    // one window
    QTRY_VERIFY(appObject.windowList.count == 1);
    AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];

    // Because the plain widget is filtered out of the hierarchy, we expect the button
    // to be a direct child of the window
    AXUIElementRef buttonRef = [window findDirectChildByRole: kAXButtonRole];
    QVERIFY(buttonRef != nil);

    TestAXObject *buttonObject = [[TestAXObject alloc] initWithAXUIElementRef: buttonRef];
    TestAXObject *parentObject = [[TestAXObject alloc] initWithAXUIElementRef: [buttonObject parent]];

    // check that the parent is a window
    QVERIFY([[parentObject role] isEqualToString: NSAccessibilityWindowRole]);

    // test the focus
    // child 0 is the layout, then button1 and 2
    QPushButton *button1 = qobject_cast<QPushButton*>(w->children().at(1));
    QVERIFY(button1);
    QPushButton *button2 = qobject_cast<QPushButton*>(w->children().at(2));
    QVERIFY(button2);
    button2->setFocus();

    AXUIElementRef systemWideElement = AXUIElementCreateSystemWide();
    AXUIElementRef focussedElement = NULL;
    AXError error = AXUIElementCopyAttributeValue(systemWideElement,
        (CFStringRef)NSAccessibilityFocusedUIElementAttribute, (CFTypeRef*)&focussedElement);
    QVERIFY(!error);
    QVERIFY(focussedElement);
    TestAXObject *focusButton2 = [[TestAXObject alloc] initWithAXUIElementRef: focussedElement];

    QVERIFY([[focusButton2 role] isEqualToString: NSAccessibilityButtonRole]);
    QVERIFY([[focusButton2 title] isEqualToString: @"Button 2"]);


    button1->setFocus();
    error = AXUIElementCopyAttributeValue(systemWideElement,
        (CFStringRef)NSAccessibilityFocusedUIElementAttribute, (CFTypeRef*)&focussedElement);
    QVERIFY(!error);
    QVERIFY(focussedElement);
    TestAXObject *focusButton1 = [[TestAXObject alloc] initWithAXUIElementRef: focussedElement];
    QVERIFY([[focusButton1 role] isEqualToString: NSAccessibilityButtonRole]);
    QVERIFY([[focusButton1 title] isEqualToString: @"I am a button"]);
}

void tst_QAccessibilityMac::notificationsTest()
{
    auto *w = m_window;
    QLineEdit *le1 = new QLineEdit(w);
    QLineEdit *le2 = new QLineEdit(w);
    w->layout()->addWidget(le1);
    w->layout()->addWidget(le2);

    QCoreApplication::processEvents();
    QTest::qWait(100);

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    // one window
    QTRY_VERIFY(appObject.windowList.count == 1);
    AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];

    AXUIElementRef lineEdit1 = [window findDirectChildByRole: kAXTextFieldRole];
    QVERIFY(lineEdit1 != nil);

    AXObserverRef observer = 0;
    AXError err = AXObserverCreate(getpid(), observerCallback, &observer);
    QVERIFY(!err);
    AXObserverAddNotification(observer, appObject.ref, kAXFocusedUIElementChangedNotification, 0);
    AXObserverAddNotification(observer, lineEdit1, kAXValueChangedNotification, 0);

    CFRunLoopAddSource( [[NSRunLoop currentRunLoop] getCFRunLoop], AXObserverGetRunLoopSource(observer), kCFRunLoopDefaultMode);

    QVERIFY(notificationList.length() == 0);
    le2->setFocus();
    QTRY_VERIFY(notificationList.length() == 1);
    QTRY_VERIFY(notificationList.at(0) == QAccessible::Focus);
    le1->setFocus();
    QTRY_VERIFY(notificationList.length() == 2);
    QTRY_VERIFY(notificationList.at(1) == QAccessible::Focus);
    le1->setText("hello");
    QTRY_VERIFY(notificationList.length() == 3);
    QTRY_VERIFY(notificationList.at(2) == QAccessible::ValueChanged);
    le1->setText("foo");
    QTRY_VERIFY(notificationList.length() == 4);
    QTRY_VERIFY(notificationList.at(3) == QAccessible::ValueChanged);
}

void tst_QAccessibilityMac::checkBoxTest()
{
    QCheckBox *ckBox = new QCheckBox(m_window);
    ckBox->setText("Great option");
    m_window->addWidget(ckBox);
    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    // one window
    QTRY_VERIFY(appObject.windowList.count == 1);
    AXUIElementRef windowRef = (AXUIElementRef) [appObject.windowList objectAtIndex: 0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef: windowRef];

    // children of window:
    AXUIElementRef checkBox = [window findDirectChildByRole: kAXCheckBoxRole];
    QVERIFY(checkBox != nil);

    TestAXObject *cb = [[TestAXObject alloc] initWithAXUIElementRef: checkBox];

    // here start actual checkbox tests
    QVERIFY([cb valueNumber] == 0);
    QVERIFY([cb.title isEqualToString:@"Great option"]);
    // EXPECT(cb.description == nil); // currently returns "" instead of nil

    QVERIFY([cb.actions containsObject:(NSString*)kAXPressAction]);

    [cb performAction:kAXPressAction];
    QVERIFY([cb valueNumber] == 1);

    [cb performAction:kAXPressAction];
    QVERIFY([cb valueNumber] == 0);

    ckBox->setCheckState(Qt::PartiallyChecked);
    QVERIFY([cb valueNumber] == 2);
}

void tst_QAccessibilityMac::tableViewTest()
{
    QTableWidget *tw = new QTableWidget(3, 2, m_window);
    struct Person
    {
        const char *name;
        const char *address;
    };
    const Person contents[] = { { "Socrates", "Greece" },
                                { "Confucius", "China" },
                                { "Kant", "Preussia" }
                              };
    for (int i = 0; i < int(sizeof(contents) / sizeof(Person)); ++i) {
        Person p = contents[i];
        QTableWidgetItem *name = new QTableWidgetItem(QString::fromLatin1(p.name));
        tw->setItem(i, 0, name);
        QTableWidgetItem *address = new QTableWidgetItem(QString::fromLatin1(p.address));
        tw->setItem(i, 1, address);
    }
    m_window->addWidget(tw);
    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    NSArray *windowList = [appObject windowList];
    // one window
    QVERIFY([windowList count] == 1);
    AXUIElementRef windowRef = (AXUIElementRef)[windowList objectAtIndex:0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef:windowRef];

    // children of window:
    AXUIElementRef tableView = [window findDirectChildByRole:kAXTableRole];
    QVERIFY(tableView != nil);

    TestAXObject *tv = [[TestAXObject alloc] initWithAXUIElementRef:tableView];

    // here start actual tableview tests
    // Should have 2 columns
    const unsigned int columnCount = 2;
    NSArray *columnArray = [tv tableColumns];
    QCOMPARE([columnArray count], columnCount);

    // should have 3 rows
    const unsigned int rowCount = 3;
    NSArray *rowArray = [tv tableRows];
    QCOMPARE([rowArray count], rowCount);

    // The individual cells are children of the rows
    TestAXObject *row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[0]];
    TestAXObject *cell = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)[row childList][0]];
    QVERIFY([cell.title isEqualToString:@"Socrates"]);
    row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[2]];
    cell = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)[row childList][1]];
    QVERIFY([cell.title isEqualToString:@"Preussia"]);

    // both rows and columns are direct children of the table
    NSArray *childList = [tv childList];
    QCOMPARE([childList count], columnCount + rowCount);
    for (id child in childList) {
        TestAXObject *childObject = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)child];
        QVERIFY([childObject.role isEqualToString:NSAccessibilityRowRole] ||
               [childObject.role isEqualToString:NSAccessibilityColumnRole]);
    }
}

void tst_QAccessibilityMac::treeViewTest()
{
    QTreeWidget *tw = new QTreeWidget;
    tw->setColumnCount(2);
    QTreeWidgetItem *root = new QTreeWidgetItem(tw, {"/", "0"});
    root->setExpanded(false);
    QTreeWidgetItem *users = new QTreeWidgetItem(root,{ "Users", "1"});
    (void)new QTreeWidgetItem(root, {"Applications", "2"});
    QTreeWidgetItem *lastChild = new QTreeWidgetItem(root, {"Libraries", "3"});

    m_window->addWidget(tw);
    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    NSArray *windowList = [appObject windowList];
    // one window
    QVERIFY([windowList count] == 1);
    AXUIElementRef windowRef = (AXUIElementRef)[windowList objectAtIndex:0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef:windowRef];
    QVERIFY(window.valid);

    // children of window

    auto accessibleTreeView = [window]() -> TestAXObject * {
        AXUIElementRef treeView = [window findDirectChildByRole:kAXOutlineRole];
        Q_ASSERT(treeView != nil);
        TestAXObject *tv = [[TestAXObject alloc] initWithAXUIElementRef:treeView];
        return tv;
    };

    TestAXObject *tv = accessibleTreeView();
    QVERIFY(tv.valid);

    // here start actual treeview tests. NSAccessibilityOutline is a specialization
    // of NSAccessibilityTable, and we represent trees as tables.
    const auto cellText = [&tv](int rowIndex, int columnIndex) -> QString {
        NSArray *rowArray = [tv tableRows];
        Q_ASSERT(rowArray.count > uint(rowIndex));
        TestAXObject *row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[rowIndex]];
        Q_ASSERT(row && row.valid);
        TestAXObject *cell = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)row.childList[columnIndex]];
        Q_ASSERT(cell && cell.valid);
        QString result = QString::fromNSString(cell.title);
        [rowArray release];
        return result;
    };

    // Should have 2 columns
    const unsigned int columnCount = 2;
    {
        NSArray *columnArray = tv.tableColumns;
        QCOMPARE(columnArray.count, columnCount);
        [columnArray release];
    }

    // Should have 1 row for now - as long as the root item is not expanded.
    // That row should have 2 columns.
    {
        NSArray *rowArray = tv.tableRows;
        QCOMPARE(rowArray.count, 1u);
        {
            TestAXObject *row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[0]];
            QCOMPARE(row.childList.count, columnCount);
            [row release];
        }
        [rowArray release];
    }
    QCOMPARE(cellText(0, 0), root->text(0));

    root->setExpanded(true);
    // Now that the root node is expanded we should now see all rows.
    // Each row should have the same number of columns as the tree view.
    {
        NSArray *rowArray = tv.tableRows;
        QCOMPARE(rowArray.count, root->childCount() + 1u);

        for (ulong r = 0; r < rowArray.count; ++r) {
            QVERIFY(rowArray[r] != nil);
            TestAXObject *row = [[TestAXObject alloc] initWithAXUIElementRef:(AXUIElementRef)rowArray[r]];
            const uint childCount = row.childList.count;
            QCOMPARE(childCount, columnCount);
            [row release];
        }
        [rowArray release];
    }

    // this should not trigger any assert
    tw->setCurrentItem(lastChild);

    // cache will have been cleared, reinitialize
    [tv release];
    tv = accessibleTreeView();
    QVERIFY(tv.valid);

    QCOMPARE(cellText(0, 0), root->text(0));
    QCOMPARE(cellText(1, 0), users->text(0));
    QCOMPARE(cellText(1, 1), users->text(1));

    [appObject release];
    [window release];
}

void tst_QAccessibilityMac::tabBarTest()
{
    QTabBar *tbar = new QTabBar;
    static const unsigned int nTabs = 20;
    for (unsigned int i = 0; i < nTabs; ++i)
        tbar->addTab(QString::number(i));
    tbar->setUsesScrollButtons(true);

    m_window->addWidget(tbar);
    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    NSArray *windowList = [appObject windowList];
    // one window
    QVERIFY([windowList count] == 1);
    AXUIElementRef windowRef = (AXUIElementRef)[windowList objectAtIndex:0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef:windowRef];
    QVERIFY(window.valid);

    // children of window
    AXUIElementRef axTarBar = [window findDirectChildByRole:kAXTabGroupRole];
    QVERIFY(axTarBar != nil);

    TestAXObject *tb = [[TestAXObject alloc] initWithAXUIElementRef:axTarBar];
    QVERIFY(tb.valid);

    [appObject release];
    [window release];

    NSArray *tbChildList = [tb childList];
    // +2 because of the scroll buttons
    QCOMPARE([tbChildList count], nTabs + 2);

    NSArray *tbTabsList = [tb tabs];
    QCOMPARE([tbTabsList count], nTabs);

    for (unsigned int i = 0; i < nTabs; ++i) {
        AXUIElementRef axTab = (AXUIElementRef)[tbTabsList objectAtIndex:i];
        QVERIFY(axTab != nil);

        TestAXObject *tab = [[TestAXObject alloc] initWithAXUIElementRef:axTab];
        QVERIFY(tab.valid);
        QCOMPARE(QString::fromNSString(tab.role), QString::fromCFString(kAXRadioButtonRole));
        QCOMPARE(QString::fromNSString(tab.title), QString::number(i));
        QCOMPARE(QString::fromNSString(tab.roleDescription), "tab");
    }
}

void tst_QAccessibilityMac::windowTest()
{
    QTextEdit *textEdit = new QTextEdit;
    m_window->addWidget(textEdit);
    QVERIFY(QTest::qWaitForWindowExposed(m_window));
    QCoreApplication::processEvents();

    TestAXObject *appObject = [TestAXObject getApplicationAXObject];
    QVERIFY(appObject);

    NSArray *windowList = [appObject windowList];
    // one window
    QVERIFY([windowList count] == 1);
    AXUIElementRef windowRef = (AXUIElementRef)[windowList objectAtIndex:0];
    QVERIFY(windowRef != nil);
    TestAXObject *window = [[TestAXObject alloc] initWithAXUIElementRef:windowRef];
    QVERIFY(window.valid);

    AXUIElementRef axTextEdit = [window findDirectChildByRole:kAXTextAreaRole];
    QVERIFY(axTextEdit != nil);

    [appObject release];
    [window release];

    TestAXObject *edit = [[TestAXObject alloc] initWithAXUIElementRef:axTextEdit];
    QVERIFY(edit.valid);
    AXUIElementRef axWindow = edit.window;
    QVERIFY(axWindow);
}

QTEST_MAIN(tst_QAccessibilityMac)
#include "tst_qaccessibilitymac.moc"
