//
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//

#import "DebugUIMessages.h"
#import "DebugUIContacts.h"
#import "DebugUIMessagesAction.h"
#import "DebugUIMessagesAssetLoader.h"
#import "OWSTableViewController.h"
#import "Signal-Swift.h"
#import <Curve25519Kit/Randomness.h>
#import <SignalMessaging/Environment.h>
#import <SignalServiceKit/MIMETypeUtil.h>
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSBatchMessageProcessor.h>
#import <SignalServiceKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
#import <SignalServiceKit/OWSDisappearingMessagesConfiguration.h>
#import <SignalServiceKit/OWSMessageUtils.h>
#import <SignalServiceKit/OWSPrimaryStorage+SessionStore.h>
#import <SignalServiceKit/OWSSyncGroupsRequestMessage.h>
#import <SignalServiceKit/OWSVerificationStateChangeMessage.h>
#import <SignalServiceKit/TSIncomingMessage.h>
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <SignalServiceKit/TSThread.h>

NS_ASSUME_NONNULL_BEGIN

@interface TSIncomingMessage (DebugUI)

@property (nonatomic, getter=wasRead) BOOL read;

@end

#pragma mark -

@interface TSOutgoingMessage (PostDatingDebug)

- (void)setReceivedAtTimestamp:(uint64_t)value;

@end

#pragma mark -

@implementation DebugUIMessages

#pragma mark - Factory Methods

- (NSString *)name
{
    return @"Messages";
}

- (NSArray<OWSTableItem *> *)itemsForActions:(NSArray<DebugUIMessagesAction *> *)actions
{
    NSMutableArray<OWSTableItem *> *items = [NSMutableArray new];

    for (DebugUIMessagesAction *action in actions) {
        [items addObject:[OWSTableItem itemWithTitle:action.label
                                         actionBlock:^{
                                             // For "all in group" actions, do each subaction in the group
                                             // exactly once, in a predictable order.
                                             if ([action isKindOfClass:[DebugUIMessagesGroupAction class]]) {
                                                 DebugUIMessagesGroupAction *groupAction
                                                     = (DebugUIMessagesGroupAction *)action;
                                                 if (groupAction.subactionMode == SubactionMode_Ordered) {
                                                     [action prepareAndPerformNTimes:groupAction.subactions.count];
                                                     return;
                                                 }
                                             }
                                             [DebugUIMessages performActionNTimes:action];
                                         }]];
    }

    return items;
}

- (nullable OWSTableSection *)sectionForThread:(nullable TSThread *)thread
{
    OWSAssert(thread);

    NSMutableArray<OWSTableItem *> *items = [NSMutableArray new];

    [items addObject:[OWSTableItem itemWithTitle:@"Delete all messages in thread"
                                     actionBlock:^{
                                         [DebugUIMessages deleteAllMessagesInThread:thread];
                                     }]];
    [items addObject:[OWSTableItem itemWithTitle:@"👷 Thrash insert/deletes"
                                     actionBlock:^{
                                         [DebugUIMessages thrashInsertAndDeleteForThread:(TSThread *)thread
                                                                                 counter:300];
                                     }]];

    [items addObjectsFromArray:[self itemsForActions:@[
        [DebugUIMessages fakeAllContactShareAction:thread],
        [DebugUIMessages sendMessageVariationsAction:thread],
        // Send Media
        [DebugUIMessages sendAllMediaAction:thread],
        [DebugUIMessages sendRandomMediaAction:thread],
        // Fake Media
        [DebugUIMessages fakeAllMediaAction:thread],
        [DebugUIMessages fakeRandomMediaAction:thread],
        // Fake Text
        [DebugUIMessages fakeAllTextAction:thread],
        [DebugUIMessages fakeRandomTextAction:thread],
        // Sequences
        [DebugUIMessages allFakeSequencesAction:thread],
        // Quoted Replies
        [DebugUIMessages allQuotedReplyAction:thread],
        // Exemplary
        [DebugUIMessages allFakeAction:thread],
        [DebugUIMessages allFakeBackDatedAction:thread],
    ]]];

    [items addObjectsFromArray:@[

#pragma mark - Actions

        [OWSTableItem itemWithTitle:@"Send N text messages (1/sec.)"
                        actionBlock:^{
                            [DebugUIMessages sendNTextMessagesInThread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Select Fake"
                        actionBlock:^{
                            [DebugUIMessages selectFakeAction:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Select Send Media"
                        actionBlock:^{
                            [DebugUIMessages selectSendMediaAction:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Send All Contact Shares"
                        actionBlock:^{
                            [DebugUIMessages sendAllContacts:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Select Quoted Reply"
                        actionBlock:^{
                            [DebugUIMessages selectQuotedReplyAction:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Select Back-Dated"
                        actionBlock:^{
                            [DebugUIMessages selectBackDatedAction:thread];
                        }],


#pragma mark - Misc.

        [OWSTableItem itemWithTitle:@"Perform 100 random actions"
                        actionBlock:^{
                            [DebugUIMessages performRandomActions:100 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Perform 1,000 random actions"
                        actionBlock:^{
                            [DebugUIMessages performRandomActions:1000 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Create 10 fake messages"
                        actionBlock:^{
                            [DebugUIMessages sendFakeMessages:10 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Create 1 fake thread with 1 message"
                        actionBlock:^{
                            [DebugUIMessages createFakeThreads:1 withFakeMessages:1];
                        }],
        [OWSTableItem itemWithTitle:@"Create 100 fake threads with 10 messages"
                        actionBlock:^{
                            [DebugUIMessages createFakeThreads:100 withFakeMessages:10];
                        }],
        [OWSTableItem itemWithTitle:@"Create 10 fake threads with 100 messages"
                        actionBlock:^{
                            [DebugUIMessages createFakeThreads:10 withFakeMessages:100];
                        }],
        [OWSTableItem itemWithTitle:@"Create 10 fake threads with 10 messages"
                        actionBlock:^{
                            [DebugUIMessages createFakeThreads:10 withFakeMessages:10];
                        }],
        [OWSTableItem itemWithTitle:@"Create 100 fake threads with 100 messages"
                        actionBlock:^{
                            [DebugUIMessages createFakeThreads:100 withFakeMessages:100];
                        }],
        [OWSTableItem itemWithTitle:@"Create 1k fake threads with 1 message"
                        actionBlock:^{
                            [DebugUIMessages createFakeThreads:1000 withFakeMessages:1];
                        }],
        [OWSTableItem itemWithTitle:@"Create 1k fake messages"
                        actionBlock:^{
                            [DebugUIMessages sendFakeMessages:1000 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Create 10k fake messages"
                        actionBlock:^{
                            [DebugUIMessages sendFakeMessages:10 * 1000 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Create 100k fake messages"
                        actionBlock:^{
                            [DebugUIMessages sendFakeMessages:100 * 1000 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Create 100k fake text messages"
                        actionBlock:^{
                            [DebugUIMessages sendFakeMessages:100 * 1000 thread:thread isTextOnly:YES];
                        }],
        [OWSTableItem itemWithTitle:@"Send text/x-signal-plain"
                        actionBlock:^{
                            [DebugUIMessages sendOversizeTextMessage:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Send unknown mimetype"
                        actionBlock:^{
                            [DebugUIMessages sendRandomAttachment:thread uti:kUnknownTestAttachmentUTI];
                        }],
        [OWSTableItem itemWithTitle:@"Send pdf"
                        actionBlock:^{
                            [DebugUIMessages sendRandomAttachment:thread uti:(NSString *)kUTTypePDF];
                        }],
        [OWSTableItem itemWithTitle:@"Create all system messages"
                        actionBlock:^{
                            [DebugUIMessages createSystemMessagesInThread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Create messages with variety of timestamps"
                        actionBlock:^{
                            [DebugUIMessages createTimestampMessagesInThread:thread];
                        }],

        [OWSTableItem itemWithTitle:@"Send 10 text and system messages"
                        actionBlock:^{
                            [DebugUIMessages sendTextAndSystemMessages:10 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Send 100 text and system messages"
                        actionBlock:^{
                            [DebugUIMessages sendTextAndSystemMessages:100 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Send 1,000 text and system messages"
                        actionBlock:^{
                            [DebugUIMessages sendTextAndSystemMessages:1000 thread:thread];
                        }],
        [OWSTableItem
            itemWithTitle:@"Request Bogus group info"
              actionBlock:^{
                  DDLogInfo(@"%@ Requesting bogus group info for thread: %@", self.logTag, thread);
                  OWSSyncGroupsRequestMessage *syncGroupsRequestMessage =
                      [[OWSSyncGroupsRequestMessage alloc] initWithThread:thread
                                                                  groupId:[Randomness generateRandomBytes:16]];
                  [[Environment current].messageSender enqueueMessage:syncGroupsRequestMessage
                      success:^{
                          DDLogWarn(@"%@ Successfully sent Request Group Info message.", self.logTag);
                      }
                      failure:^(NSError *error) {
                          DDLogError(
                              @"%@ Failed to send Request Group Info message with error: %@", self.logTag, error);
                      }];
              }],
        [OWSTableItem itemWithTitle:@"Message with stalled timer"
                        actionBlock:^{
                            [DebugUIMessages createDisappearingMessagesWhichFailedToStartInThread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Inject 10 fake incoming messages"
                        actionBlock:^{
                            [DebugUIMessages injectFakeIncomingMessages:10 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Inject 100 fake incoming messages"
                        actionBlock:^{
                            [DebugUIMessages injectFakeIncomingMessages:100 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Inject 1,000 fake incoming messages"
                        actionBlock:^{
                            [DebugUIMessages injectFakeIncomingMessages:1000 thread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Test Indic Scripts"
                        actionBlock:^{
                            [DebugUIMessages testIndicScriptsInThread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Test Zalgo"
                        actionBlock:^{
                            [DebugUIMessages testZalgoTextInThread:thread];
                        }],
        [OWSTableItem itemWithTitle:@"Test Directional Filenames"
                        actionBlock:^{
                            [DebugUIMessages testDirectionalFilenamesInThread:thread];
                        }],
    ]];

    if ([thread isKindOfClass:[TSContactThread class]]) {
        TSContactThread *contactThread = (TSContactThread *)thread;
        NSString *recipientId = contactThread.contactIdentifier;
        [items addObject:[OWSTableItem itemWithTitle:@"Create 10 new groups"
                                         actionBlock:^{
                                             [DebugUIMessages createNewGroups:10 recipientId:recipientId];
                                         }]];
        [items addObject:[OWSTableItem itemWithTitle:@"Create 100 new groups"
                                         actionBlock:^{
                                             [DebugUIMessages createNewGroups:100 recipientId:recipientId];
                                         }]];
        [items addObject:[OWSTableItem itemWithTitle:@"Create 1,000 new groups"
                                         actionBlock:^{
                                             [DebugUIMessages createNewGroups:1000 recipientId:recipientId];
                                         }]];
    }
    if ([thread isKindOfClass:[TSGroupThread class]]) {
        TSGroupThread *groupThread = (TSGroupThread *)thread;
        [items addObject:[OWSTableItem itemWithTitle:@"Send message to all members"
                                         actionBlock:^{
                                             [DebugUIMessages sendMessages:1 toAllMembersOfGroup:groupThread];
                                         }]];
    }
    return [OWSTableSection sectionWithTitle:self.name items:items];
}

+ (void)sendMessages:(NSUInteger)count toAllMembersOfGroup:(TSGroupThread *)groupThread
{
    for (NSString *recipientId in groupThread.groupModel.groupMemberIds) {
        TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
        [[self sendTextMessagesActionInThread:contactThread] prepareAndPerformNTimes:count];
    }
}

+ (void)sendTextMessageInThread:(TSThread *)thread counter:(NSUInteger)counter
{
    DDLogInfo(@"%@ sendTextMessageInThread: %zd", self.logTag, counter);
    [DDLog flushLog];

    NSString *randomText = [self randomText];
    NSString *text = [[[@(counter) description] stringByAppendingString:@" "] stringByAppendingString:randomText];
    OWSMessageSender *messageSender = [Environment current].messageSender;
    TSOutgoingMessage *message =
        [ThreadUtil sendMessageWithText:text inThread:thread quotedReplyModel:nil messageSender:messageSender];
    DDLogError(@"%@ sendTextMessageInThread timestamp: %llu.", self.logTag, message.timestamp);
}

+ (void)sendNTextMessagesInThread:(TSThread *)thread
{
    [self performActionNTimes:[self sendTextMessagesActionInThread:thread]];
}

+ (DebugUIMessagesAction *)sendTextMessagesActionInThread:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction actionWithLabel:@"Send Text Message"
                                   staggeredActionBlock:^(NSUInteger index,
                                       YapDatabaseReadWriteTransaction *transaction,
                                       ActionSuccessBlock success,
                                       ActionFailureBlock failure) {
                                       dispatch_async(dispatch_get_main_queue(), ^{
                                           [self sendTextMessageInThread:thread counter:index];
                                           // TODO:
                                           success();
                                       });
                                   }];
}

+ (void)sendAttachment:(NSString *)filePath
                thread:(TSThread *)thread
                 label:(NSString *)label
            hasCaption:(BOOL)hasCaption
               success:(nullable void (^)(void))success
               failure:(nullable void (^)(void))failure
{
    OWSAssert(filePath);
    OWSAssert(thread);

    OWSMessageSender *messageSender = [Environment current].messageSender;
    NSString *filename = [filePath lastPathComponent];
    NSString *utiType = [MIMETypeUtil utiTypeForFileExtension:filename.pathExtension];
    DataSource *_Nullable dataSource = [DataSourcePath dataSourceWithFilePath:filePath shouldDeleteOnDeallocation:NO];
    [dataSource setSourceFilename:filename];
    SignalAttachment *attachment =
        [SignalAttachment attachmentWithDataSource:dataSource dataUTI:utiType imageQuality:TSImageQualityOriginal];

    NSString *messageBody = nil;
    if (hasCaption) {
        // We want a message body that is "more than one line on all devices,
        // using all dynamic type sizes."
        NSString *sampleText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, "
                               @"consectetur adipiscing elit.";
        messageBody = [[label stringByAppendingString:@" "] stringByAppendingString:sampleText];

        messageBody = [messageBody stringByAppendingString:@" 🔤"];
    }
    attachment.captionText = messageBody;

    OWSAssert(attachment);
    if ([attachment hasError]) {
        DDLogError(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
        [DDLog flushLog];
    }
    OWSAssert(![attachment hasError]);
    [ThreadUtil sendMessageWithAttachment:attachment
                                 inThread:thread
                         quotedReplyModel:nil
                            messageSender:messageSender
                               completion:nil];
    success();
}

#pragma mark - Infrastructure

+ (void)performActionNTimes:(DebugUIMessagesAction *)action
{
    OWSAssertIsOnMainThread();
    OWSAssert(action);

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many?"
                                                                   message:nil
                                                            preferredStyle:UIAlertControllerStyleActionSheet];
    for (NSNumber *countValue in @[
             @(1),
             @(10),
             @(100),
             @(1 * 1000),
             @(10 * 1000),
         ]) {
        [alert addAction:[UIAlertAction actionWithTitle:countValue.stringValue
                                                  style:UIAlertActionStyleDefault
                                                handler:^(UIAlertAction *ignore) {
                                                    [action prepareAndPerformNTimes:countValue.unsignedIntegerValue];
                                                }]];
    }

    [alert addAction:[OWSAlerts cancelAction]];
    UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
    [fromViewController presentViewController:alert animated:YES completion:nil];
}

#pragma mark - Send Media

+ (NSArray<DebugUIMessagesAction *> *)allSendMediaActions:(TSThread *)thread
{
    OWSAssert(thread);

    NSArray<DebugUIMessagesAction *> *actions = @[
        [self sendJpegAction:thread hasCaption:NO],
        [self sendJpegAction:thread hasCaption:YES],
        [self sendGifAction:thread hasCaption:NO],
        [self sendGifAction:thread hasCaption:YES],
        [self sendLargeGifAction:thread hasCaption:NO],
        [self sendLargeGifAction:thread hasCaption:YES],
        [self sendMp3Action:thread hasCaption:NO],
        [self sendMp3Action:thread hasCaption:YES],
        [self sendMp4Action:thread hasCaption:NO],
        [self sendMp4Action:thread hasCaption:YES],
    ];
    return actions;
}

+ (DebugUIMessagesAction *)sendJpegAction:(TSThread *)thread hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self sendMediaAction:@"Send Jpeg"
                      hasCaption:hasCaption
                 fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance]
                          thread:thread];
}

+ (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self sendMediaAction:@"Send Gif"
                      hasCaption:hasCaption
                 fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance]
                          thread:thread];
}

+ (DebugUIMessagesAction *)sendLargeGifAction:(TSThread *)thread hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self sendMediaAction:@"Send Large Gif"
                      hasCaption:hasCaption
                 fakeAssetLoader:[DebugUIMessagesAssetLoader largeGifInstance]
                          thread:thread];
}

+ (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self sendMediaAction:@"Send Mp3"
                      hasCaption:hasCaption
                 fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance]
                          thread:thread];
}

+ (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self sendMediaAction:@"Send Mp4"
                      hasCaption:hasCaption
                 fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance]
                          thread:thread];
}

+ (DebugUIMessagesAction *)sendMediaAction:(NSString *)labelParam
                                hasCaption:(BOOL)hasCaption
                           fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                                    thread:(TSThread *)thread
{
    OWSAssert(labelParam.length > 0);
    OWSAssert(fakeAssetLoader);
    OWSAssert(thread);

    NSString *label = labelParam;
    if (hasCaption) {
        label = [label stringByAppendingString:@" 🔤"];
    }

    return [DebugUIMessagesSingleAction
             actionWithLabel:label
        staggeredActionBlock:^(NSUInteger index,
            YapDatabaseReadWriteTransaction *transaction,
            ActionSuccessBlock success,
            ActionFailureBlock failure) {
            dispatch_async(dispatch_get_main_queue(), ^{
                OWSAssert(fakeAssetLoader.filePath.length > 0);
                [self sendAttachment:fakeAssetLoader.filePath
                              thread:thread
                               label:label
                          hasCaption:hasCaption
                             success:success
                             failure:failure];
            });
        }
                prepareBlock:fakeAssetLoader.prepareBlock];
}

+ (DebugUIMessagesAction *)sendAllMediaAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Send Media"
                                                    subactions:[self allSendMediaActions:thread]];
}

+ (DebugUIMessagesAction *)sendRandomMediaAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Send Media"
                                                       subactions:[self allSendMediaActions:thread]];
}

+ (void)selectSendMediaAction:(TSThread *)thread
{
    OWSAssert(thread);

    [self selectActionUI:[self allSendMediaActions:thread] label:@"Select Send Media"];
}

#pragma mark - Fake Outgoing Media

+ (DebugUIMessagesAction *)fakeOutgoingJpegAction:(TSThread *)thread
                                     messageState:(TSOutgoingMessageState)messageState
                                       hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Jpeg"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingGifAction:(TSThread *)thread
                                    messageState:(TSOutgoingMessageState)messageState
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Gif"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingLargeGifAction:(TSThread *)thread
                                         messageState:(TSOutgoingMessageState)messageState
                                           hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Gif"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader largeGifInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingMp3Action:(TSThread *)thread
                                    messageState:(TSOutgoingMessageState)messageState
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Mp3"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingMp4Action:(TSThread *)thread
                                    messageState:(TSOutgoingMessageState)messageState
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Mp4"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingCompactPortraitPngAction:(TSThread *)thread
                                                   messageState:(TSOutgoingMessageState)messageState
                                                     hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Portrait Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader compactLandscapePngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingCompactLandscapePngAction:(TSThread *)thread
                                                    messageState:(TSOutgoingMessageState)messageState
                                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Landscape Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader compactPortraitPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingTallPortraitPngAction:(TSThread *)thread
                                                messageState:(TSOutgoingMessageState)messageState
                                                  hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Tall Portrait Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingWideLandscapePngAction:(TSThread *)thread
                                                 messageState:(TSOutgoingMessageState)messageState
                                                   hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Wide Landscape Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingLargePngAction:(TSThread *)thread
                                         messageState:(TSOutgoingMessageState)messageState
                                           hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader largePngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingTinyPngAction:(TSThread *)thread
                                        messageState:(TSOutgoingMessageState)messageState
                                          hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Tiny Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingPngAction:(TSThread *)thread
                                     actionLabel:(NSString *)actionLabel
                                       imageSize:(CGSize)imageSize
                                 backgroundColor:(UIColor *)backgroundColor
                                       textColor:(UIColor *)textColor
                                      imageLabel:(NSString *)imageLabel
                                    messageState:(TSOutgoingMessageState)messageState
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:actionLabel
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader pngInstanceWithSize:imageSize
                                                                         backgroundColor:backgroundColor
                                                                               textColor:textColor
                                                                                   label:imageLabel]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingTinyPdfAction:(TSThread *)thread
                                        messageState:(TSOutgoingMessageState)messageState
                                          hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Tiny Pdf"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingLargePdfAction:(TSThread *)thread
                                         messageState:(TSOutgoingMessageState)messageState
                                           hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Pdf"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader largePdfInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingMissingPngAction:(TSThread *)thread
                                           messageState:(TSOutgoingMessageState)messageState
                                             hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Png"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingMissingPdfAction:(TSThread *)thread
                                           messageState:(TSOutgoingMessageState)messageState
                                             hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Pdf"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingOversizeTextAction:(TSThread *)thread
                                             messageState:(TSOutgoingMessageState)messageState
                                               hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeOutgoingMediaAction:@"Fake Outgoing Oversize Text"
                            messageState:messageState
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader oversizeTextInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeOutgoingMediaAction:(NSString *)labelParam
                                      messageState:(TSOutgoingMessageState)messageState
                                        hasCaption:(BOOL)hasCaption
                                   fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                                            thread:(TSThread *)thread
{
    OWSAssert(labelParam.length > 0);
    OWSAssert(fakeAssetLoader);
    OWSAssert(thread);

    NSString *label = [labelParam stringByAppendingString:[self actionLabelForHasCaption:hasCaption
                                                                    outgoingMessageState:messageState
                                                                             isDelivered:NO
                                                                                  isRead:NO]];

    return
        [DebugUIMessagesSingleAction actionWithLabel:label
                              unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
                                  OWSAssert(fakeAssetLoader.filePath.length > 0);
                                  [self createFakeOutgoingMedia:index
                                                   messageState:messageState
                                                     hasCaption:hasCaption
                                                fakeAssetLoader:fakeAssetLoader
                                                         thread:thread
                                                    transaction:transaction];
                              }
                                        prepareBlock:fakeAssetLoader.prepareBlock];
}

+ (void)createFakeOutgoingMedia:(NSUInteger)index
                   messageState:(TSOutgoingMessageState)messageState
                     hasCaption:(BOOL)hasCaption
                fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                         thread:(TSThread *)thread
                    transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(thread);
    OWSAssert(fakeAssetLoader.filePath);
    OWSAssert(transaction);

    // Random time within last n years. Helpful for filling out a media gallery over time.
    //    double yearsMillis = 4.0 * kYearsInMs;
    //    uint64_t millisAgo = (uint64_t)(((double)arc4random() / ((double)0xffffffff)) * yearsMillis);
    //    uint64_t timestamp = [NSDate ows_millisecondTimeStamp] - millisAgo;
    uint64_t timestamp = [NSDate ows_millisecondTimeStamp];

    NSString *messageBody = nil;
    if (hasCaption) {
        // We want a message body that is "more than one line on all devices,
        // using all dynamic type sizes."
        NSString *sampleText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, "
                               @"consectetur adipiscing elit.";
        messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:sampleText];
        messageBody = [messageBody stringByAppendingString:[self actionLabelForHasCaption:hasCaption
                                                                     outgoingMessageState:messageState
                                                                              isDelivered:NO
                                                                                   isRead:NO]];
    }

    TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread
                                                     messageBody:messageBody
                                                 fakeAssetLoader:fakeAssetLoader
                                                    messageState:messageState
                                                     isDelivered:YES
                                                          isRead:NO
                                                   quotedMessage:nil
                                                    contactShare:nil
                                                     transaction:transaction];

    // This is a hack to "back-date" the message.
    [message setReceivedAtTimestamp:timestamp];

    [message saveWithTransaction:transaction];
}

#pragma mark - Fake Incoming Media

+ (DebugUIMessagesAction *)fakeIncomingJpegAction:(TSThread *)thread
                           isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                       hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Jpeg"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingGifAction:(TSThread *)thread
                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Gif"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingLargeGifAction:(TSThread *)thread
                               isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                           hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Large Gif"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader largeGifInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingMp3Action:(TSThread *)thread
                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Mp3"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingMp4Action:(TSThread *)thread
                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Mp4"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingCompactPortraitPngAction:(TSThread *)thread
                                         isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                                     hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Portrait Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader compactPortraitPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingCompactLandscapePngAction:(TSThread *)thread
                                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Landscape Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader compactLandscapePngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingTallPortraitPngAction:(TSThread *)thread
                                      isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                                  hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Tall Portrait Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingWideLandscapePngAction:(TSThread *)thread
                                       isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                                   hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Wide Landscape Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingLargePngAction:(TSThread *)thread
                               isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                           hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Large Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader largePngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingTinyPngAction:(TSThread *)thread
                              isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                          hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Tiny Incoming Large Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingPngAction:(TSThread *)thread
                                     actionLabel:(NSString *)actionLabel
                                       imageSize:(CGSize)imageSize
                                 backgroundColor:(UIColor *)backgroundColor
                                       textColor:(UIColor *)textColor
                                      imageLabel:(NSString *)imageLabel
                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                      hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:actionLabel
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader pngInstanceWithSize:imageSize
                                                                         backgroundColor:backgroundColor
                                                                               textColor:textColor
                                                                                   label:imageLabel]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingTinyPdfAction:(TSThread *)thread
                              isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                          hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Tiny Pdf"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingLargePdfAction:(TSThread *)thread
                               isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                           hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Large Pdf"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader largePdfInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingMissingPngAction:(TSThread *)thread
                                 isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                             hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Missing Png"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingMissingPdfAction:(TSThread *)thread
                                 isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                             hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Missing Pdf"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingOversizeTextAction:(TSThread *)thread
                                   isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                               hasCaption:(BOOL)hasCaption
{
    OWSAssert(thread);

    return [self fakeIncomingMediaAction:@"Fake Incoming Oversize Text"
                  isAttachmentDownloaded:isAttachmentDownloaded
                              hasCaption:hasCaption
                         fakeAssetLoader:[DebugUIMessagesAssetLoader oversizeTextInstance]
                                  thread:thread];
}

+ (DebugUIMessagesAction *)fakeIncomingMediaAction:(NSString *)labelParam
                            isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                        hasCaption:(BOOL)hasCaption
                                   fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                                            thread:(TSThread *)thread
{
    OWSAssert(labelParam.length > 0);
    OWSAssert(fakeAssetLoader);
    OWSAssert(thread);

    NSString *label = labelParam;
    if (hasCaption) {
        label = [label stringByAppendingString:@" 🔤"];
    }

    if (isAttachmentDownloaded) {
        label = [label stringByAppendingString:@" 👍"];
    }

    return
        [DebugUIMessagesSingleAction actionWithLabel:label
                              unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
                                  OWSAssert(fakeAssetLoader.filePath.length > 0);
                                  [self createFakeIncomingMedia:index
                                         isAttachmentDownloaded:isAttachmentDownloaded
                                                     hasCaption:hasCaption
                                                fakeAssetLoader:fakeAssetLoader
                                                         thread:thread
                                                    transaction:transaction];
                              }
                                        prepareBlock:fakeAssetLoader.prepareBlock];
}

+ (TSIncomingMessage *)createFakeIncomingMedia:(NSUInteger)index
                        isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                    hasCaption:(BOOL)hasCaption
                               fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                                        thread:(TSThread *)thread
                                   transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    NSString *_Nullable caption = nil;
    if (hasCaption) {
        // We want a message body that is "more than one line on all devices,
        // using all dynamic type sizes."
        caption = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, "
                  @"consectetur adipiscing elit.";
    }
    return [self createFakeIncomingMedia:index
                  isAttachmentDownloaded:isAttachmentDownloaded
                                 caption:caption
                         fakeAssetLoader:fakeAssetLoader
                                  thread:thread
                             transaction:transaction];
}

+ (TSIncomingMessage *)createFakeIncomingMedia:(NSUInteger)index
                        isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                       caption:(nullable NSString *)caption
                               fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                                        thread:(TSThread *)thread
                                   transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(thread);
    OWSAssert(fakeAssetLoader.filePath);
    OWSAssert(transaction);

    //    // Random time within last n years. Helpful for filling out a media gallery over time.
    //    double yearsMillis = 4.0 * kYearsInMs;
    //    uint64_t millisAgo = (uint64_t)(((double)arc4random() / ((double)0xffffffff)) * yearsMillis);
    //    uint64_t timestamp = [NSDate ows_millisecondTimeStamp] - millisAgo;

    NSString *messageBody = nil;
    if (caption) {
        messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:caption];

        messageBody = [messageBody stringByAppendingString:@" 🔤"];

        if (isAttachmentDownloaded) {
            messageBody = [messageBody stringByAppendingString:@" 👍"];
        }
    }

    return [self createFakeIncomingMessage:thread
                               messageBody:messageBody
                           fakeAssetLoader:fakeAssetLoader
                    isAttachmentDownloaded:isAttachmentDownloaded
                             quotedMessage:nil
                               transaction:transaction];
}

#pragma mark - Fake Media

+ (NSArray<DebugUIMessagesAction *> *)allFakeMediaActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Jpeg ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Gif ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        // Don't bother with multiple GIF states.
        [self fakeOutgoingGifAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingLargeGifAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Mp3 ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Mp4 ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Compact Landscape Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Compact Portrait Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Wide Landscape Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Tall Portrait Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Large Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Tiny Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Reserved Color Png ⚠️"]];
    }
    
    ConversationStyle *conversationStyle = [[ConversationStyle alloc] initWithThread:thread];
    [actions addObjectsFromArray:@[
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing White Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[UIColor whiteColor]
                          textColor:[UIColor ows_signalBrandBlueColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateFailed
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing White Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[UIColor whiteColor]
                          textColor:[UIColor ows_signalBrandBlueColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSending
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing White Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[UIColor whiteColor]
                          textColor:[UIColor ows_signalBrandBlueColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSent
                         hasCaption:YES],

        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingFailed]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateFailed
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingFailed]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSending
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingFailed]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSent
                         hasCaption:YES],

        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Sending' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingSending]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateFailed
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Sending' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingSending]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSending
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Sending' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingSending]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSent
                         hasCaption:YES],

        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Sent' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingSent]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateFailed
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Sent' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingSent]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSending
                         hasCaption:YES],
        [self fakeOutgoingPngAction:thread
                        actionLabel:@"Fake Outgoing 'Outgoing Sent' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle bubbleColorOutgoingSent]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
                       messageState:TSOutgoingMessageStateSent
                         hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Tiny Pdf ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSending hasCaption:YES],
        [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:YES],
        [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
        [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSent hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Large Pdf ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingLargePdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Missing Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingMissingPngAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Large Pdf ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingMissingPdfAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Oversize Text ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateFailed hasCaption:NO],
        [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSending hasCaption:NO],
        [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSent hasCaption:NO],
    ]];

    // Incoming

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Jpg ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingJpegAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingJpegAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingJpegAction:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingJpegAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Gif ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingGifAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingLargeGifAction:thread isAttachmentDownloaded:YES hasCaption:NO],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Mp3 ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingMp3Action:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingMp3Action:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingMp3Action:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingMp3Action:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Mp4 ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingMp4Action:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingMp4Action:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions
            addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Compact Landscape Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions
            addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Compact Portrait Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions
            addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Wide Landscape Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions
            addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Tall Portrait Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Large Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Tiny Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingTinyPngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingTinyPngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions
            addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Reserved Color Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingPngAction:thread
                        actionLabel:@"Fake Incoming White Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[UIColor whiteColor]
                          textColor:[UIColor ows_signalBrandBlueColor]
                         imageLabel:@"W"
             isAttachmentDownloaded:YES
                         hasCaption:YES],
        [self fakeIncomingPngAction:thread
                        actionLabel:@"Fake Incoming White Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[UIColor whiteColor]
                          textColor:[UIColor ows_signalBrandBlueColor]
                         imageLabel:@"W"
             isAttachmentDownloaded:NO
                         hasCaption:YES],
        [self fakeIncomingPngAction:thread
                        actionLabel:@"Fake Incoming 'Incoming' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle primaryColor]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
             isAttachmentDownloaded:YES
                         hasCaption:YES],
        [self fakeIncomingPngAction:thread
                        actionLabel:@"Fake Incoming 'Incoming' Png"
                          imageSize:CGSizeMake(200.f, 200.f)
                    backgroundColor:[conversationStyle primaryColor]
                          textColor:[UIColor whiteColor]
                         imageLabel:@"W"
             isAttachmentDownloaded:NO
                         hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Tiny Pdf ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:NO hasCaption:YES],
        [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Large Pdf ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingLargePdfAction:thread isAttachmentDownloaded:YES hasCaption:NO],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Missing Png ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingMissingPngAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingMissingPngAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Missing Pdf ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO],
        [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:YES],
    ]];
    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Oversize Text ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeIncomingOversizeTextAction:thread isAttachmentDownloaded:NO hasCaption:NO],
        [self fakeIncomingOversizeTextAction:thread isAttachmentDownloaded:YES hasCaption:NO],
    ]];
    return actions;
}

+ (DebugUIMessagesAction *)fakeAllMediaAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Media"
                                                    subactions:[self allFakeMediaActions:thread includeLabels:YES]];
}

+ (DebugUIMessagesAction *)fakeRandomMediaAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Fake Media"
                                                       subactions:[self allFakeMediaActions:thread includeLabels:NO]];
}

#pragma mark - Send Text Messages

+ (DebugUIMessagesAction *)sendShortTextMessageAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction actionWithLabel:@"Send Short Text Message"
                                   staggeredActionBlock:^(NSUInteger index,
                                       YapDatabaseReadWriteTransaction *transaction,
                                       ActionSuccessBlock success,
                                       ActionFailureBlock failure) {
                                       dispatch_async(dispatch_get_main_queue(), ^{
                                           [self sendTextMessageInThread:thread counter:index];
                                       });
                                   }];
}

+ (DebugUIMessagesAction *)sendOversizeTextMessageAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction actionWithLabel:@"Send Oversize Text Message"
                                   staggeredActionBlock:^(NSUInteger index,
                                       YapDatabaseReadWriteTransaction *transaction,
                                       ActionSuccessBlock success,
                                       ActionFailureBlock failure) {
                                       dispatch_async(dispatch_get_main_queue(), ^{
                                           [self sendOversizeTextMessage:thread];
                                       });
                                   }];
}

+ (DebugUIMessagesAction *)sendMessageVariationsAction:(TSThread *)thread
{
    OWSAssert(thread);

    NSArray<DebugUIMessagesAction *> *actions = @[
        [self sendShortTextMessageAction:thread],
        [self sendOversizeTextMessageAction:thread],
    ];

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Send Conversation Cell Variations" subactions:actions];
}

#pragma mark - Fake Text Messages

+ (DebugUIMessagesAction *)fakeShortIncomingTextMessageAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction
               actionWithLabel:@"Fake Short Incoming Text Message"
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            NSString *messageBody =
                [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]];
            [self createFakeIncomingMessage:thread
                                messageBody:messageBody
                            fakeAssetLoader:nil
                     isAttachmentDownloaded:NO
                              quotedMessage:nil
                                transaction:transaction];
        }];
}

+ (SignalAttachment *)signalAttachmentForFilePath:(NSString *)filePath
{
    OWSAssert(filePath);

    NSString *filename = [filePath lastPathComponent];
    NSString *utiType = [MIMETypeUtil utiTypeForFileExtension:filename.pathExtension];
    DataSource *_Nullable dataSource = [DataSourcePath dataSourceWithFilePath:filePath shouldDeleteOnDeallocation:NO];
    [dataSource setSourceFilename:filename];
    SignalAttachment *attachment =
        [SignalAttachment attachmentWithDataSource:dataSource dataUTI:utiType imageQuality:TSImageQualityOriginal];
    if (arc4random_uniform(100) > 50) {
        attachment.captionText = [self randomCaptionText];
    }

    OWSAssert(attachment);
    if ([attachment hasError]) {
        DDLogError(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
        [DDLog flushLog];
    }
    OWSAssert(![attachment hasError]);
    return attachment;
}

+ (void)sendAttachment:(NSString *)filePath
                thread:(TSThread *)thread
               success:(nullable void (^)(void))success
               failure:(nullable void (^)(void))failure
{
    OWSAssert(filePath);
    OWSAssert(thread);

    SignalAttachment *attachment = [self signalAttachmentForFilePath:filePath];
    OWSMessageSender *messageSender = [Environment current].messageSender;
    [ThreadUtil sendMessageWithAttachment:attachment
                                 inThread:thread
                         quotedReplyModel:nil
                            messageSender:messageSender
                               completion:nil];
    success();
}

+ (DebugUIMessagesAction *)fakeIncomingTextMessageAction:(TSThread *)thread text:(NSString *)text
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction
               actionWithLabel:[NSString stringWithFormat:@"Fake Incoming Text Message (%@)", text]
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text];
            [self createFakeIncomingMessage:thread
                                messageBody:messageBody
                            fakeAssetLoader:nil
                     isAttachmentDownloaded:NO
                              quotedMessage:nil
                                transaction:transaction];
        }];
}

+ (DebugUIMessagesAction *)fakeOutgoingTextMessageAction:(TSThread *)thread
                                            messageState:(TSOutgoingMessageState)messageState
                                                    text:(NSString *)text
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction
               actionWithLabel:[NSString stringWithFormat:@"Fake Incoming Text Message (%@)", text]
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text];
            [self createFakeOutgoingMessage:thread
                                messageBody:messageBody
                            fakeAssetLoader:nil
                               messageState:messageState
                                isDelivered:NO
                                     isRead:NO
                              quotedMessage:nil
                               contactShare:nil
                                transaction:transaction];
        }];
}

+ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread
                                                 messageState:(TSOutgoingMessageState)messageState
{
    return [self fakeShortOutgoingTextMessageAction:thread messageState:messageState isDelivered:NO isRead:NO];
}

+ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread
                                                 messageState:(TSOutgoingMessageState)messageState
                                                  isDelivered:(BOOL)isDelivered
                                                       isRead:(BOOL)isRead
{
    return [self fakeShortOutgoingTextMessageAction:(TSThread *)thread
                                               text:[self randomText]
                                       messageState:messageState
                                        isDelivered:isDelivered
                                             isRead:isRead];
}

+ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread
                                                         text:(NSString *)text
                                                 messageState:(TSOutgoingMessageState)messageState
                                                  isDelivered:(BOOL)isDelivered
                                                       isRead:(BOOL)isRead
{
    OWSAssert(thread);

    NSString *label = @"Fake Short Incoming Text Message";
    label = [label stringByAppendingString:[self actionLabelForHasCaption:YES
                                                     outgoingMessageState:messageState
                                                              isDelivered:isDelivered
                                                                   isRead:isRead]];

    return [DebugUIMessagesSingleAction
               actionWithLabel:label
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text];
            [self createFakeOutgoingMessage:thread
                                messageBody:messageBody
                            fakeAssetLoader:nil
                               messageState:messageState
                                isDelivered:isDelivered
                                     isRead:isRead
                              quotedMessage:nil
                               contactShare:nil
                                transaction:transaction];
        }];
}

+ (NSArray<DebugUIMessagesAction *> *)allFakeTextActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSArray<NSString *> *messageBodies = @[
        @"Hi",
        @"1️⃣",
        @"1️⃣2️⃣",
        @"1️⃣2️⃣3️⃣",
        @"落",
        @"﷽",
    ];

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Message Bodies ⚠️"]];
    }
    [actions addObject:[self fakeShortIncomingTextMessageAction:thread]];
    for (NSString *messageBody in messageBodies) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread text:messageBody]];
    }

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Statuses ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateFailed],
        [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSending],
        [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSent],
        [self fakeShortOutgoingTextMessageAction:thread
                                    messageState:TSOutgoingMessageStateSent
                                     isDelivered:YES
                                          isRead:NO],
        [self fakeShortOutgoingTextMessageAction:thread
                                    messageState:TSOutgoingMessageStateSent
                                     isDelivered:YES
                                          isRead:YES],
    ]];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Outgoing Message Bodies ⚠️"]];
    }
    for (NSString *messageBody in messageBodies) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:messageBody]];
    }
    return actions;
}

+ (DebugUIMessagesAction *)fakeAllTextAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Text"
                                                    subactions:[self allFakeTextActions:thread includeLabels:YES]];
}

+ (DebugUIMessagesAction *)fakeRandomTextAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Fake Text"
                                                       subactions:[self allFakeTextActions:thread includeLabels:NO]];
}

#pragma mark - Fake Quoted Replies

+ (DebugUIMessagesAction *)
              fakeQuotedReplyAction:(TSThread *)thread
                 quotedMessageLabel:(NSString *)quotedMessageLabel
            isQuotedMessageIncoming:(BOOL)isQuotedMessageIncoming
                  // Optional. At least one of quotedMessageBody and quotedMessageAssetLoader should be non-nil.
                  quotedMessageBody:(nullable NSString *)quotedMessageBody
           // Optional. At least one of quotedMessageBody and quotedMessageAssetLoader should be non-nil.
           quotedMessageAssetLoader:(nullable DebugUIMessagesAssetLoader *)quotedMessageAssetLoader
                         replyLabel:(NSString *)replyLabel
                    isReplyIncoming:(BOOL)isReplyIncoming
                   replyMessageBody:(nullable NSString *)replyMessageBody
                   replyAssetLoader:(nullable DebugUIMessagesAssetLoader *)replyAssetLoader
                  // Only applies if !isReplyIncoming.
                  replyMessageState:(TSOutgoingMessageState)replyMessageState
{
    OWSAssert(thread);

    // Used fixed values for properties that shouldn't matter much.
    BOOL quotedMessageIsDelivered = NO;
    BOOL quotedMessageIsRead = NO;
    TSOutgoingMessageState quotedMessageMessageState = TSOutgoingMessageStateSent;
    BOOL replyIsDelivered = NO;
    BOOL replyIsRead = NO;

    // Seamlessly convert oversize text messages to oversize text attachments.
    if ([quotedMessageBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
        OWSAssert(!quotedMessageAssetLoader);
        quotedMessageAssetLoader = [DebugUIMessagesAssetLoader oversizeTextInstanceWithText:quotedMessageBody];
        quotedMessageBody = nil;
    }
    if (replyMessageBody &&
        [replyMessageBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
        OWSAssert(!replyAssetLoader);
        replyAssetLoader = [DebugUIMessagesAssetLoader oversizeTextInstanceWithText:replyMessageBody];
        replyMessageBody = nil;
    }

    NSMutableString *label = [NSMutableString new];
    [label appendString:@"Quoted Reply ("];
    [label appendString:replyLabel];
    if (isReplyIncoming) {
    } else {
        [label appendString:[self actionLabelForHasCaption:NO
                                      outgoingMessageState:replyMessageState
                                               isDelivered:replyIsDelivered
                                                    isRead:replyIsRead]];
    }
    [label appendString:@") to ("];
    [label appendString:quotedMessageLabel];
    if (quotedMessageAssetLoader) {
        [label appendFormat:@" %@", quotedMessageAssetLoader.labelEmoji];
    }
    if (isQuotedMessageIncoming) {
    } else {
        [label appendString:[self actionLabelForHasCaption:quotedMessageBody.length > 0
                                      outgoingMessageState:quotedMessageMessageState
                                               isDelivered:quotedMessageIsDelivered
                                                    isRead:quotedMessageIsRead]];
    }
    [label appendString:@")"];

    NSMutableArray<ActionPrepareBlock> *prepareBlocks = [NSMutableArray new];
    if (quotedMessageAssetLoader.prepareBlock) {
        [prepareBlocks addObject:quotedMessageAssetLoader.prepareBlock];
    }
    if (replyAssetLoader.prepareBlock) {
        [prepareBlocks addObject:replyAssetLoader.prepareBlock];
    }

    // We don't need to configure ConversationStyle's view width in this case.
    ConversationStyle *conversationStyle = [[ConversationStyle alloc] initWithThread:thread];

    return [DebugUIMessagesSingleAction
               actionWithLabel:label
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            NSString *_Nullable quotedMessageBodyWIndex
                = (quotedMessageBody ? [NSString stringWithFormat:@"%zd %@", index, quotedMessageBody] : nil);
            TSQuotedMessage *_Nullable quotedMessage = nil;
            if (isQuotedMessageIncoming) {
                TSIncomingMessage *_Nullable messageToQuote = nil;
                messageToQuote = [self createFakeIncomingMessage:thread
                                                     messageBody:quotedMessageBodyWIndex
                                                 fakeAssetLoader:quotedMessageAssetLoader
                                          isAttachmentDownloaded:YES
                                                   quotedMessage:nil
                                                     transaction:transaction];
                OWSAssert(messageToQuote);
                DDLogVerbose(@"%@ %@", self.logTag, label);
                [DDLog flushLog];
                ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote
                                                                                     isGroupThread:thread.isGroupThread
                                                                                       transaction:transaction
                                                                                 conversationStyle:conversationStyle];
                quotedMessage = [[OWSQuotedReplyModel quotedReplyForConversationViewItem:viewItem transaction:transaction] buildQuotedMessage];
            } else {
                TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread
                                                                                  messageBody:quotedMessageBodyWIndex
                                                                              fakeAssetLoader:quotedMessageAssetLoader
                                                                                 messageState:quotedMessageMessageState
                                                                                  isDelivered:quotedMessageIsDelivered
                                                                                       isRead:quotedMessageIsRead
                                                                                quotedMessage:nil
                                                                                 contactShare:nil
                                                                                  transaction:transaction];
                OWSAssert(messageToQuote);

                ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote
                                                                                     isGroupThread:thread.isGroupThread
                                                                                       transaction:transaction
                                                                                 conversationStyle:conversationStyle];
                quotedMessage = [[OWSQuotedReplyModel quotedReplyForConversationViewItem:viewItem transaction:transaction] buildQuotedMessage];
            }
            OWSAssert(quotedMessage);

            NSString *_Nullable replyMessageBodyWIndex
                = (replyMessageBody ? [NSString stringWithFormat:@"%zd %@", index, replyMessageBody] : nil);
            if (isReplyIncoming) {
                [self createFakeIncomingMessage:thread
                                    messageBody:replyMessageBodyWIndex
                                fakeAssetLoader:replyAssetLoader
                         isAttachmentDownloaded:NO
                                  quotedMessage:quotedMessage
                                    transaction:transaction];
            } else {
                [self createFakeOutgoingMessage:thread
                                    messageBody:replyMessageBodyWIndex
                                fakeAssetLoader:replyAssetLoader
                                   messageState:replyMessageState
                                    isDelivered:replyIsDelivered
                                         isRead:replyIsRead
                                  quotedMessage:quotedMessage
                                   contactShare:nil
                                    transaction:transaction];
            }
        }
                  prepareBlock:[self groupPrepareBlockWithPrepareBlocks:prepareBlocks]];
}

// Recursively perform a group of "prepare blocks" in sequence, aborting
// if any fail.
+ (ActionPrepareBlock)groupPrepareBlockWithPrepareBlocks:(NSArray<ActionPrepareBlock> *)prepareBlocks
{
    return ^(ActionSuccessBlock success, ActionFailureBlock failure) {
        [self groupPrepareBlockStepWithPrepareBlocks:[prepareBlocks mutableCopy] success:success failure:failure];
    };
}

+ (void)groupPrepareBlockStepWithPrepareBlocks:(NSMutableArray<ActionPrepareBlock> *)prepareBlocks
                                       success:(ActionSuccessBlock)success
                                       failure:(ActionFailureBlock)failure
{
    if (prepareBlocks.count < 1) {
        success();
        return;
    }
    ActionPrepareBlock nextPrepareBlock = [prepareBlocks lastObject];
    [prepareBlocks removeLastObject];

    nextPrepareBlock(
        ^{
            [self groupPrepareBlockStepWithPrepareBlocks:prepareBlocks success:success failure:failure];
        },
        failure);
}

+ (NSArray<DebugUIMessagesAction *> *)allFakeQuotedReplyActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSString *shortText = @"Lorem ipsum";
    NSString *mediumText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, "
                           @"consectetur adipiscing elit.";
    NSString *longText = [self randomOversizeText];

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread
                                                          text:@"⚠️ Quoted Replies (Message Lengths) ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Medium Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:mediumText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Medium Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:mediumText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Long Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:longText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Long Text"
                     isReplyIncoming:NO
                    replyMessageBody:longText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],
    ]];

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread
                                                          text:@"⚠️ Quoted Replies (Attachment Types) ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Jpg"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader jpegInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Jpg"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader jpegInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Mp3"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader mp3Instance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Mp3"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader mp3Instance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Mp4"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader mp4Instance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Mp4"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader mp4Instance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Gif"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader gifInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Gif"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader gifInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Pdf"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Missing Pdf"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tiny Png"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Missing Png"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],
    ]];

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread
                                                          text:@"⚠️ Quoted Replies (Attachment Layout) ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tall Portrait Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tall Portrait Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                          replyLabel:@"Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tall Portrait Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Wide Landscape Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Wide Landscape Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                          replyLabel:@"Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Wide Landscape Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                          replyLabel:@"Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tiny Png"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tiny Png"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance]
                          replyLabel:@"Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],
    ]];

    void (^directionActions)(BOOL, BOOL) = ^(BOOL isQuotedMessageIncoming, BOOL isReplyIncoming) {
        [actions addObjectsFromArray:@[
            [self fakeQuotedReplyAction:thread
                      quotedMessageLabel:@"Short Text"
                 isQuotedMessageIncoming:isQuotedMessageIncoming
                       quotedMessageBody:shortText
                quotedMessageAssetLoader:nil
                              replyLabel:@"Short Text"
                         isReplyIncoming:isReplyIncoming
                        replyMessageBody:shortText
                        replyAssetLoader:nil
                       replyMessageState:TSOutgoingMessageStateSent],
        ]];
    };

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread
                                                          text:@"⚠️ Quoted Replies (Incoming v. Outgoing) ⚠️"]];
    }
    directionActions(NO, NO);
    directionActions(YES, NO);
    directionActions(NO, YES);
    directionActions(YES, YES);

    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread
                                                          text:@"⚠️ Quoted Replies (Message States) ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Jpg"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader jpegInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Mp3"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader mp3Instance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Mp4"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader mp4Instance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Gif"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader gifInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Pdf"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Missing Pdf"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tiny Png"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Missing Png"
             isQuotedMessageIncoming:YES
                   quotedMessageBody:nil
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance]
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSending],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateFailed],
    ]];


    if (includeLabels) {
        [actions addObject:[self fakeIncomingTextMessageAction:thread
                                                          text:@"⚠️ Quoted Replies (Reply W. Attachment) ⚠️"]];
    }
    [actions addObjectsFromArray:@[
        // Png + Text -> Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Tall Portrait Png"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                          replyLabel:@"Tall Portrait Png"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Text -> Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Tall Portrait Png"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:nil
                   replyMessageState:TSOutgoingMessageStateSent],

        // Text -> Png
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Tall Portrait Png"
                     isReplyIncoming:NO
                    replyMessageBody:nil
                    replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Png -> Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Tall Portrait Png"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Png -> Portrait Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Tall Portrait Png"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Png -> Landscape Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Wide Landscape Png"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],


        // Png -> Landscape Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Wide Landscape Png + Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Png -> Landscape Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Wide Landscape Png + Short Text"
                     isReplyIncoming:NO
                    replyMessageBody:shortText
                    replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Png -> Landscape Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Wide Landscape Png + Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],

        // Png -> Landscape Png + Text
        [self fakeQuotedReplyAction:thread
                  quotedMessageLabel:@"Short Text"
             isQuotedMessageIncoming:NO
                   quotedMessageBody:shortText
            quotedMessageAssetLoader:nil
                          replyLabel:@"Wide Landscape Png + Medium Text"
                     isReplyIncoming:NO
                    replyMessageBody:mediumText
                    replyAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance]
                   replyMessageState:TSOutgoingMessageStateSent],
    ]];

    return actions;
}

+ (DebugUIMessagesAction *)allQuotedReplyAction:(TSThread *)thread
{
    OWSAssert(thread);

    return
        [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Quoted Reply"
                                                 subactions:[self allFakeQuotedReplyActions:thread includeLabels:YES]];
}

+ (void)selectQuotedReplyAction:(TSThread *)thread
{
    OWSAssertIsOnMainThread();
    OWSAssert(thread);

    [self selectActionUI:[self allFakeQuotedReplyActions:thread includeLabels:NO] label:@"Select QuotedReply"];
}

+ (DebugUIMessagesAction *)randomQuotedReplyAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction
        randomGroupActionWithLabel:@"Random Quoted Reply"
                        subactions:[self allFakeQuotedReplyActions:thread includeLabels:NO]];
}

#pragma mark - Exemplary

+ (NSArray<DebugUIMessagesAction *> *)allFakeActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];
    [actions addObjectsFromArray:[self allFakeMediaActions:thread includeLabels:includeLabels]];
    [actions addObjectsFromArray:[self allFakeTextActions:thread includeLabels:includeLabels]];
    [actions addObjectsFromArray:[self allFakeSequenceActions:thread includeLabels:includeLabels]];
    [actions addObjectsFromArray:[self allFakeQuotedReplyActions:thread includeLabels:includeLabels]];
    [actions addObjectsFromArray:[self allFakeBackDatedActions:thread includeLabels:includeLabels]];
    [actions addObjectsFromArray:[self allFakeContactShareActions:thread includeLabels:includeLabels]];
    return actions;
}

+ (DebugUIMessagesAction *)allFakeAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake"
                                                    subactions:[self allFakeActions:thread includeLabels:YES]];
}

+ (void)selectFakeAction:(TSThread *)thread
{
    OWSAssertIsOnMainThread();
    OWSAssert(thread);

    [self selectActionUI:[self allFakeActions:thread includeLabels:NO] label:@"Select Fake"];
}

+ (void)selectActionUI:(NSArray<DebugUIMessagesAction *> *)actions label:(NSString *)label
{
    OWSAssertIsOnMainThread();
    UIAlertController *alert =
        [UIAlertController alertControllerWithTitle:label message:nil preferredStyle:UIAlertControllerStyleActionSheet];
    for (DebugUIMessagesAction *action in actions) {
        [alert addAction:[UIAlertAction actionWithTitle:action.label
                                                  style:UIAlertActionStyleDefault
                                                handler:^(UIAlertAction *ignore) {
                                                    [self performActionNTimes:action];
                                                }]];
    }

    [alert addAction:[OWSAlerts cancelAction]];

    UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
    [fromViewController presentViewController:alert animated:YES completion:nil];
}

#pragma mark - Sequences

+ (NSArray<DebugUIMessagesAction *> *)allFakeSequenceActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Short Message Sequences ⚠️"]];
    }

    [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming"]];
    [actions
        addObject:[self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSent text:@"Outgoing"]];
    [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 1"]];
    [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 2"]];
    [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 3"]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateFailed
                                                      text:@"Outgoing Unsent 1"]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateFailed
                                                      text:@"Outgoing Unsent 2"]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSending
                                                      text:@"Outgoing Sending 1"]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSending
                                                      text:@"Outgoing Sending 2"]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSent
                                                      text:@"Outgoing Sent 1"]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSent
                                                      text:@"Outgoing Sent 2"]];
    [actions addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                           text:@"Outgoing Delivered 1"
                                                   messageState:TSOutgoingMessageStateSent
                                                    isDelivered:YES
                                                         isRead:NO]];
    [actions addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                           text:@"Outgoing Delivered 2"
                                                   messageState:TSOutgoingMessageStateSent
                                                    isDelivered:YES
                                                         isRead:NO]];
    [actions addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                           text:@"Outgoing Read 1"
                                                   messageState:TSOutgoingMessageStateSent
                                                    isDelivered:YES
                                                         isRead:YES]];
    [actions addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                           text:@"Outgoing Read 2"
                                                   messageState:TSOutgoingMessageStateSent
                                                    isDelivered:YES
                                                         isRead:YES]];
    [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming"]];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Long Message Sequences ⚠️"]];
    }

    NSString *longText = @"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla "
                         @"vitae pretium hendrerit, tellus turpis pharetra libero...";

    [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSent
                                                      text:[@"Outgoing" stringByAppendingString:longText]]];
    [actions
        addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 1" stringByAppendingString:longText]]];
    [actions
        addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 2" stringByAppendingString:longText]]];
    [actions
        addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 3" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateFailed
                                                      text:[@"Outgoing Unsent 1" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateFailed
                                                      text:[@"Outgoing Unsent 2" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSending
                                                      text:[@"Outgoing Sending 1" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSending
                                                      text:[@"Outgoing Sending 2" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSent
                                                      text:[@"Outgoing Sent 1" stringByAppendingString:longText]]];
    [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                              messageState:TSOutgoingMessageStateSent
                                                      text:[@"Outgoing Sent 2" stringByAppendingString:longText]]];
    [actions
        addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                      text:[@"Outgoing Delivered 1" stringByAppendingString:longText]
                                              messageState:TSOutgoingMessageStateSent
                                               isDelivered:YES
                                                    isRead:NO]];
    [actions
        addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                      text:[@"Outgoing Delivered 2" stringByAppendingString:longText]
                                              messageState:TSOutgoingMessageStateSent
                                               isDelivered:YES
                                                    isRead:NO]];
    [actions addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                           text:[@"Outgoing Read 1" stringByAppendingString:longText]
                                                   messageState:TSOutgoingMessageStateSent
                                                    isDelivered:YES
                                                         isRead:YES]];
    [actions addObject:[self fakeShortOutgoingTextMessageAction:thread
                                                           text:[@"Outgoing Read 2" stringByAppendingString:longText]
                                                   messageState:TSOutgoingMessageStateSent
                                                    isDelivered:YES
                                                         isRead:YES]];
    [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming" stringByAppendingString:longText]]];

    return actions;
}

+ (DebugUIMessagesAction *)allFakeSequencesAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Sequences"
                                                    subactions:[self allFakeSequenceActions:thread includeLabels:YES]];
}

#pragma mark - Back-dated

+ (DebugUIMessagesAction *)fakeBackDatedMessageAction:(TSThread *)thread
                                                label:(NSString *)label
                                           dateOffset:(int64_t)dateOffset
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction
               actionWithLabel:[NSString stringWithFormat:@"Fake Back-Date Message (%@)", label]
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            NSString *messageBody =
                [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:self.randomText];
            TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread
                                                             messageBody:messageBody
                                                         fakeAssetLoader:nil
                                                            messageState:TSOutgoingMessageStateSent
                                                             isDelivered:NO
                                                                  isRead:NO
                                                           quotedMessage:nil
                                                            contactShare:nil
                                                             transaction:transaction];
            [message setReceivedAtTimestamp:(uint64_t)((int64_t)[NSDate ows_millisecondTimeStamp] + dateOffset)];
            [message saveWithTransaction:transaction];
        }];
}

+ (NSArray<DebugUIMessagesAction *> *)allFakeBackDatedActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Back-Dated ⚠️"]];
    }

    [actions
        addObject:[self fakeBackDatedMessageAction:thread label:@"One Minute Ago" dateOffset:-(int64_t)kMinuteInMs]];
    [actions addObject:[self fakeBackDatedMessageAction:thread label:@"One Hour Ago" dateOffset:-(int64_t)kHourInMs]];
    [actions addObject:[self fakeBackDatedMessageAction:thread label:@"One Day Ago" dateOffset:-(int64_t)kDayInMs]];
    [actions
        addObject:[self fakeBackDatedMessageAction:thread label:@"Two Days Ago" dateOffset:-(int64_t)kDayInMs * 2]];
    [actions
        addObject:[self fakeBackDatedMessageAction:thread label:@"Ten Days Ago" dateOffset:-(int64_t)kDayInMs * 10]];
    [actions
        addObject:[self fakeBackDatedMessageAction:thread label:@"400 Days Ago" dateOffset:-(int64_t)kDayInMs * 400]];

    return actions;
}

+ (DebugUIMessagesAction *)allFakeBackDatedAction:(TSThread *)thread
{
    OWSAssert(thread);

    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Back-Dated"
                                                    subactions:[self allFakeBackDatedActions:thread includeLabels:YES]];
}

+ (void)selectBackDatedAction:(TSThread *)thread
{
    OWSAssertIsOnMainThread();
    OWSAssert(thread);

    [self selectActionUI:[self allFakeBackDatedActions:thread includeLabels:NO] label:@"Select Back-Dated"];
}

#pragma mark - Contact Shares

typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transaction);

+ (DebugUIMessagesAction *)fakeContactShareMessageAction:(TSThread *)thread
                                                   label:(NSString *)label
                                            contactBlock:(OWSContactBlock)contactBlock
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction
               actionWithLabel:[NSString stringWithFormat:@"Fake Contact Share (%@)", label]
        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
            OWSContact *contact = contactBlock(transaction);
            TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread
                                                             messageBody:nil
                                                         fakeAssetLoader:nil
                                                            messageState:TSOutgoingMessageStateSent
                                                             isDelivered:NO
                                                                  isRead:NO
                                                           quotedMessage:nil
                                                            contactShare:contact
                                                             transaction:transaction];
            [message saveWithTransaction:transaction];
        }];
}

+ (NSArray<DebugUIMessagesAction *> *)allFakeContactShareActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Share Contact ⚠️"]];
    }

    [actions addObject:[self fakeContactShareMessageAction:thread
                                                     label:@"Name & Number"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Alice";
                                                  OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
                                                  phoneNumber.phoneType = OWSContactPhoneType_Home;
                                                  phoneNumber.phoneNumber = @"+13213214321";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self fakeContactShareMessageAction:thread
                                                     label:@"Name & Email"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Bob";
                                                  OWSContactEmail *email = [OWSContactEmail new];
                                                  email.emailType = OWSContactEmailType_Home;
                                                  email.email = @"a@b.com";
                                                  contact.emails = @[
                                                      email,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self fakeContactShareMessageAction:thread
                                                     label:@"Complicated"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Alice";
                                                  name.familyName = @"Carol";
                                                  name.middleName = @"Bob";
                                                  name.namePrefix = @"Ms.";
                                                  name.nameSuffix = @"Esq.";
                                                  name.organizationName = @"Falafel Hut";

                                                  OWSContactPhoneNumber *phoneNumber1 = [OWSContactPhoneNumber new];
                                                  phoneNumber1.phoneType = OWSContactPhoneType_Home;
                                                  phoneNumber1.phoneNumber = @"+13213215555";
                                                  OWSContactPhoneNumber *phoneNumber2 = [OWSContactPhoneNumber new];
                                                  phoneNumber2.phoneType = OWSContactPhoneType_Custom;
                                                  phoneNumber2.label = @"Carphone";
                                                  phoneNumber2.phoneNumber = @"+13332226666";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber1,
                                                      phoneNumber2,
                                                  ];

                                                  NSMutableArray<OWSContactEmail *> *emails = [NSMutableArray new];
                                                  for (NSUInteger i = 0; i < 16; i++) {
                                                      OWSContactEmail *email = [OWSContactEmail new];
                                                      email.emailType = OWSContactEmailType_Home;
                                                      email.email = [NSString stringWithFormat:@"a%zd@b.com", i];
                                                      [emails addObject:email];
                                                  }
                                                  contact.emails = emails;

                                                  OWSContactAddress *address1 = [OWSContactAddress new];
                                                  address1.addressType = OWSContactAddressType_Home;
                                                  address1.street = @"123 home st.";
                                                  address1.neighborhood = @"round the bend.";
                                                  address1.city = @"homeville";
                                                  address1.region = @"HO";
                                                  address1.postcode = @"12345";
                                                  address1.country = @"USA";
                                                  OWSContactAddress *address2 = [OWSContactAddress new];
                                                  address2.addressType = OWSContactAddressType_Custom;
                                                  address2.label = @"Otra casa";
                                                  address2.pobox = @"caja 123";
                                                  address2.street = @"123 casa calle";
                                                  address2.city = @"barrio norte";
                                                  address2.region = @"AB";
                                                  address2.postcode = @"53421";
                                                  address2.country = @"MX";
                                                  contact.addresses = @[
                                                      address1,
                                                      address2,
                                                  ];

                                                  UIImage *avatarImage =
                                                      [OWSAvatarBuilder buildRandomAvatarWithDiameter:200];
                                                  [contact saveAvatarImage:avatarImage transaction:transaction];

                                                  return contact;
                                              }]];
    [actions addObject:[self fakeContactShareMessageAction:thread
                                                     label:@"Long values"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Bobasdjasdlkjasldkjas";
                                                  name.familyName = @"Bobasdjasdlkjasldkjas";
                                                  OWSContactEmail *email = [OWSContactEmail new];
                                                  email.emailType = OWSContactEmailType_Mobile;
                                                  email.email = @"asdlakjsaldkjasldkjasdlkjasdlkjasdlkajsa@b.com";
                                                  contact.emails = @[
                                                      email,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self fakeContactShareMessageAction:thread
                                                     label:@"System Contact w/o Signal"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Add Me To Your Contacts";
                                                  OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
                                                  phoneNumber.phoneType = OWSContactPhoneType_Work;
                                                  phoneNumber.phoneNumber = @"+324602053911";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self fakeContactShareMessageAction:thread
                                                     label:@"System Contact w. Signal"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Add Me To Your Contacts";
                                                  OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
                                                  phoneNumber.phoneType = OWSContactPhoneType_Work;
                                                  phoneNumber.phoneNumber = @"+32460205392";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber,
                                                  ];
                                                  return contact;
                                              }]];

    return actions;
}

+ (DebugUIMessagesAction *)fakeAllContactShareAction:(TSThread *)thread
{
    OWSAssert(thread);

    return
        [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Contact Shares"
                                                 subactions:[self allFakeContactShareActions:thread includeLabels:YES]];
}


+ (DebugUIMessagesAction *)sendContactShareMessageAction:(TSThread *)thread
                                                   label:(NSString *)label
                                            contactBlock:(OWSContactBlock)contactBlock
{
    OWSAssert(thread);

    return [DebugUIMessagesSingleAction
             actionWithLabel:[NSString stringWithFormat:@"Send Contact Share (%@)", label]
        staggeredActionBlock:^(NSUInteger index,
            YapDatabaseReadWriteTransaction *transaction,
            ActionSuccessBlock success,
            ActionFailureBlock failure) {
            OWSContact *contact = contactBlock(transaction);
            DDLogVerbose(@"%@ sending contact: %@", self.logTag, contact.debugDescription);
            OWSMessageSender *messageSender = [Environment current].messageSender;
            [ThreadUtil sendMessageWithContactShare:contact inThread:thread messageSender:messageSender completion:nil];

            success();
        }];
}

+ (NSArray<DebugUIMessagesAction *> *)allSendContactShareActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
{
    OWSAssert(thread);

    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];

    if (includeLabels) {
        [actions addObject:[self fakeOutgoingTextMessageAction:thread
                                                  messageState:TSOutgoingMessageStateSent
                                                          text:@"⚠️ Send Share Contact ⚠️"]];
    }

    [actions addObject:[self sendContactShareMessageAction:thread
                                                     label:@"Name & Number"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Alice";
                                                  OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
                                                  phoneNumber.phoneType = OWSContactPhoneType_Home;
                                                  phoneNumber.phoneNumber = @"+13213214321";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self sendContactShareMessageAction:thread
                                                     label:@"Name & Email"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Bob";
                                                  OWSContactEmail *email = [OWSContactEmail new];
                                                  email.emailType = OWSContactEmailType_Home;
                                                  email.email = @"a@b.com";
                                                  contact.emails = @[
                                                      email,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self sendContactShareMessageAction:thread
                                                     label:@"Complicated"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Alice";
                                                  name.familyName = @"Carol";
                                                  name.middleName = @"Bob";
                                                  name.namePrefix = @"Ms.";
                                                  name.nameSuffix = @"Esq.";
                                                  name.organizationName = @"Falafel Hut";

                                                  OWSContactPhoneNumber *phoneNumber1 = [OWSContactPhoneNumber new];
                                                  phoneNumber1.phoneType = OWSContactPhoneType_Home;
                                                  phoneNumber1.phoneNumber = @"+13213214321";
                                                  OWSContactPhoneNumber *phoneNumber2 = [OWSContactPhoneNumber new];
                                                  phoneNumber2.phoneType = OWSContactPhoneType_Custom;
                                                  phoneNumber2.label = @"Carphone";
                                                  phoneNumber2.phoneNumber = @"+13332221111";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber1,
                                                      phoneNumber2,
                                                  ];

                                                  NSMutableArray<OWSContactEmail *> *emails = [NSMutableArray new];
                                                  for (NSUInteger i = 0; i < 16; i++) {
                                                      OWSContactEmail *email = [OWSContactEmail new];
                                                      email.emailType = OWSContactEmailType_Home;
                                                      email.email = [NSString stringWithFormat:@"a%zd@b.com", i];
                                                      [emails addObject:email];
                                                  }
                                                  contact.emails = emails;

                                                  OWSContactAddress *address1 = [OWSContactAddress new];
                                                  address1.addressType = OWSContactAddressType_Home;
                                                  address1.street = @"123 home st.";
                                                  address1.neighborhood = @"round the bend.";
                                                  address1.city = @"homeville";
                                                  address1.region = @"HO";
                                                  address1.postcode = @"12345";
                                                  address1.country = @"USA";
                                                  OWSContactAddress *address2 = [OWSContactAddress new];
                                                  address2.addressType = OWSContactAddressType_Custom;
                                                  address2.label = @"Otra casa";
                                                  address2.pobox = @"caja 123";
                                                  address2.street = @"123 casa calle";
                                                  address2.city = @"barrio norte";
                                                  address2.region = @"AB";
                                                  address2.postcode = @"53421";
                                                  address2.country = @"MX";
                                                  contact.addresses = @[
                                                      address1,
                                                      address2,
                                                  ];

                                                  UIImage *avatarImage =
                                                      [OWSAvatarBuilder buildRandomAvatarWithDiameter:200];
                                                  [contact saveAvatarImage:avatarImage transaction:transaction];

                                                  return contact;
                                              }]];
    [actions addObject:[self sendContactShareMessageAction:thread
                                                     label:@"Long values"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Bobasdjasdlkjasldkjas";
                                                  name.familyName = @"Bobasdjasdlkjasldkjas";
                                                  OWSContactEmail *email = [OWSContactEmail new];
                                                  email.emailType = OWSContactEmailType_Mobile;
                                                  email.email = @"asdlakjsaldkjasldkjasdlkjasdlkjasdlkajsa@b.com";
                                                  contact.emails = @[
                                                      email,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self sendContactShareMessageAction:thread
                                                     label:@"System Contact w/o Signal"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Add Me To Your Contacts";
                                                  OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
                                                  phoneNumber.phoneType = OWSContactPhoneType_Work;
                                                  phoneNumber.phoneNumber = @"+324602053911";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber,
                                                  ];
                                                  return contact;
                                              }]];
    [actions addObject:[self sendContactShareMessageAction:thread
                                                     label:@"System Contact w. Signal"
                                              contactBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                                                  OWSContact *contact = [OWSContact new];
                                                  OWSContactName *name = [OWSContactName new];
                                                  contact.name = name;
                                                  name.givenName = @"Add Me To Your Contacts";
                                                  OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
                                                  phoneNumber.phoneType = OWSContactPhoneType_Work;
                                                  phoneNumber.phoneNumber = @"+32460205392";
                                                  contact.phoneNumbers = @[
                                                      phoneNumber,
                                                  ];
                                                  return contact;
                                              }]];

    return actions;
}

+ (void)sendAllContacts:(TSThread *)thread
{
    NSArray<DebugUIMessagesAction *> *subactions = [self allSendContactShareActions:thread includeLabels:NO];
    DebugUIMessagesAction *action =
        [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Send All Contact Shares" subactions:subactions];
    [action prepareAndPerformNTimes:subactions.count];
}

#pragma mark -

+ (NSString *)randomOversizeText
{
    NSMutableString *message = [NSMutableString new];
    while (message.length < kOversizeTextMessageSizeThreshold) {
        [message appendString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla "
                              @"vitae pretium hendrerit, tellus turpis pharetra libero, vitae sodales tortor ante vel "
                              @"sem. Fusce sed nisl a lorem gravida tincidunt. Suspendisse efficitur non quam ac "
                              @"sodales. Aenean ut velit maximus, posuere sem a, accumsan nunc. Donec ullamcorper "
                              @"turpis lorem. Quisque dignissim purus eu placerat ultricies. Proin at urna eget mi "
                              @"semper congue. Aenean non elementum ex. Praesent pharetra quam at sem vestibulum, "
                              @"vestibulum ornare dolor elementum. Vestibulum massa tortor, scelerisque sit amet "
                              @"pulvinar a, rhoncus vitae nisl. Sed mi nunc, tempus at varius in, malesuada vitae "
                              @"dui. Vivamus efficitur pulvinar erat vitae congue. Proin vehicula turpis non felis "
                              @"congue facilisis. Nullam aliquet dapibus ligula ac mollis. Etiam sit amet posuere "
                              @"lorem, in rhoncus nisi.\n\n"];
    }
    return message;
}

+ (void)sendOversizeTextMessage:(TSThread *)thread
{
    OWSMessageSender *messageSender = [Environment current].messageSender;
    NSString *message = [self randomOversizeText];
    DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:message];
    SignalAttachment *attachment =
        [SignalAttachment attachmentWithDataSource:dataSource dataUTI:kOversizeTextAttachmentUTI];
    [ThreadUtil sendMessageWithAttachment:attachment
                                 inThread:thread
                         quotedReplyModel:nil
                            messageSender:messageSender
                               completion:nil];
}

+ (NSData *)createRandomNSDataOfSize:(size_t)size
{
    OWSAssert(size % 4 == 0);

    return [Randomness generateRandomBytes:(int)size];
}

+ (void)sendRandomAttachment:(TSThread *)thread uti:(NSString *)uti
{
    [self sendRandomAttachment:thread uti:uti length:256];
}

+ (NSString *)randomCaptionText
{
    return [NSString stringWithFormat:@"%@ (caption)", [self randomText]];
}

+ (void)sendRandomAttachment:(TSThread *)thread uti:(NSString *)uti length:(NSUInteger)length
{
    OWSMessageSender *messageSender = [Environment current].messageSender;
    DataSource *_Nullable dataSource =
        [DataSourceValue dataSourceWithData:[self createRandomNSDataOfSize:length] utiType:uti];
    SignalAttachment *attachment =
        [SignalAttachment attachmentWithDataSource:dataSource dataUTI:uti imageQuality:TSImageQualityOriginal];

    if (arc4random_uniform(100) > 50) {
        // give 1/2 our attachments captions, and add a hint that it's a caption since we
        // style them indistinguishably from a separate text message.
        attachment.captionText = [self randomCaptionText];
    }
    [ThreadUtil sendMessageWithAttachment:attachment
                                 inThread:thread
                         quotedReplyModel:nil
                            messageSender:messageSender
                             ignoreErrors:YES
                               completion:nil];
}

+ (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread
{
    OWSAssert(thread);

    uint64_t timestamp = [NSDate ows_millisecondTimeStamp];
    NSString *source = ^{
        if ([thread isKindOfClass:[TSGroupThread class]]) {
            TSGroupThread *gThread = (TSGroupThread *)thread;
            return gThread.groupModel.groupMemberIds[0];
        } else if ([thread isKindOfClass:[TSContactThread class]]) {
            TSContactThread *contactThread = (TSContactThread *)thread;
            return contactThread.contactIdentifier;
        } else {
            OWSFail(@"%@ failure: unknown thread type", self.logTag);
            return @"unknown-source-id";
        }
    }();

    SSKProtoEnvelopeBuilder *envelopeBuilder =
        [[SSKProtoEnvelopeBuilder alloc] initWithType:SSKProtoEnvelopeTypeCiphertext
                                               source:source
                                         sourceDevice:1
                                            timestamp:timestamp];
    NSError *error;
    SSKProtoEnvelope *_Nullable envelope = [envelopeBuilder buildAndReturnError:&error];
    if (error || !envelope) {
        OWSFail(@"%@ Could not construct envelope: %@.", self.logTag, error);
        return nil;
    }
    return envelope;
}

+ (NSArray<TSInteraction *> *)unsavedSystemMessagesInThread:(TSThread *)thread
{
    OWSAssert(thread);

    NSMutableArray<TSInteraction *> *result = [NSMutableArray new];

    [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        if ([thread isKindOfClass:[TSContactThread class]]) {
            TSContactThread *contactThread = (TSContactThread *)thread;

            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeIncoming
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeOutgoing
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeIncomingMissed
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeOutgoingIncomplete
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeIncomingIncomplete
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeIncomingDeclined
                                                       inThread:contactThread]];
            [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                 withCallNumber:@"+19174054215"
                                                       callType:RPRecentCallTypeOutgoingMissed
                                                       inThread:contactThread]];
        }

        {
            NSNumber *durationSeconds = [OWSDisappearingMessagesConfiguration validDurationsSeconds][0];
            OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
                [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId
                                                                       enabled:YES
                                                               durationSeconds:(uint32_t)[durationSeconds intValue]];
            [result addObject:[[OWSDisappearingConfigurationUpdateInfoMessage alloc]
                                       initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                  thread:thread
                                           configuration:disappearingMessagesConfiguration
                                     createdByRemoteName:@"Alice"
                                  createdInExistingGroup:NO]];
        }

        {
            NSNumber *durationSeconds = [OWSDisappearingMessagesConfiguration validDurationsSeconds][0];
            OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
                [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId
                                                                       enabled:YES
                                                               durationSeconds:(uint32_t)[durationSeconds intValue]];
            [result addObject:[[OWSDisappearingConfigurationUpdateInfoMessage alloc]
                                       initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                  thread:thread
                                           configuration:disappearingMessagesConfiguration
                                     createdByRemoteName:nil
                                  createdInExistingGroup:YES]];
        }

        {
            NSNumber *durationSeconds = [[OWSDisappearingMessagesConfiguration validDurationsSeconds] lastObject];
            OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
                [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId
                                                                       enabled:YES
                                                               durationSeconds:(uint32_t)[durationSeconds intValue]];
            [result addObject:[[OWSDisappearingConfigurationUpdateInfoMessage alloc]
                                       initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                  thread:thread
                                           configuration:disappearingMessagesConfiguration
                                     createdByRemoteName:@"Alice"
                                  createdInExistingGroup:NO]];
        }
        {
            OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
                [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId
                                                                       enabled:NO
                                                               durationSeconds:0];
            [result addObject:[[OWSDisappearingConfigurationUpdateInfoMessage alloc]
                                       initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                  thread:thread
                                           configuration:disappearingMessagesConfiguration
                                     createdByRemoteName:@"Alice"
                                  createdInExistingGroup:NO]];
        }

        [result addObject:[TSInfoMessage userNotRegisteredMessageInThread:thread recipientId:@"+19174054215"]];

        [result addObject:[[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                          inThread:thread
                                                       messageType:TSInfoMessageTypeSessionDidEnd]];
        // TODO: customMessage?
        [result addObject:[[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                          inThread:thread
                                                       messageType:TSInfoMessageTypeGroupUpdate]];
        // TODO: customMessage?
        [result addObject:[[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                          inThread:thread
                                                       messageType:TSInfoMessageTypeGroupQuit]];

        [result addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                                thread:thread
                                                                           recipientId:@"+19174054215"
                                                                     verificationState:OWSVerificationStateDefault
                                                                         isLocalChange:YES]];
        [result addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                                thread:thread
                                                                           recipientId:@"+19174054215"
                                                                     verificationState:OWSVerificationStateVerified
                                                                         isLocalChange:YES]];
        [result
            addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                            thread:thread
                                                                       recipientId:@"+19174054215"
                                                                 verificationState:OWSVerificationStateNoLongerVerified
                                                                     isLocalChange:YES]];
        [result addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                                thread:thread
                                                                           recipientId:@"+19174054215"
                                                                     verificationState:OWSVerificationStateDefault
                                                                         isLocalChange:NO]];
        [result addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                                thread:thread
                                                                           recipientId:@"+19174054215"
                                                                     verificationState:OWSVerificationStateVerified
                                                                         isLocalChange:NO]];
        [result
            addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                            thread:thread
                                                                       recipientId:@"+19174054215"
                                                                 verificationState:OWSVerificationStateNoLongerVerified
                                                                     isLocalChange:NO]];

        [result addObject:[TSErrorMessage missingSessionWithEnvelope:[self createEnvelopeForThread:thread]
                                                     withTransaction:transaction]];
        [result addObject:[TSErrorMessage invalidKeyExceptionWithEnvelope:[self createEnvelopeForThread:thread]
                                                          withTransaction:transaction]];
        [result addObject:[TSErrorMessage invalidVersionWithEnvelope:[self createEnvelopeForThread:thread]
                                                     withTransaction:transaction]];
        [result addObject:[TSErrorMessage corruptedMessageWithEnvelope:[self createEnvelopeForThread:thread]
                                                       withTransaction:transaction]];
        
        
        TSInvalidIdentityKeyReceivingErrorMessage *_Nullable blockingSNChangeMessage =
            [TSInvalidIdentityKeyReceivingErrorMessage untrustedKeyWithEnvelope:[self createEnvelopeForThread:thread]
                                                                withTransaction:transaction];
        OWSAssert(blockingSNChangeMessage);
        [result addObject:blockingSNChangeMessage];
        [result addObject:[[TSErrorMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                           inThread:thread
                                                  failedMessageType:TSErrorMessageNonBlockingIdentityChange
                                                        recipientId:@"+19174054215"]];
    }];

    return result;
}

+ (void)createSystemMessagesInThread:(TSThread *)thread
{
    OWSAssert(thread);

    NSArray<TSInteraction *> *messages = [self unsavedSystemMessagesInThread:thread];
    [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        for (TSInteraction *message in messages) {
            [message saveWithTransaction:transaction];
        }
    }];
}

+ (void)createSystemMessageInThread:(TSThread *)thread
{
    OWSAssert(thread);

    NSArray<TSInteraction *> *messages = [self unsavedSystemMessagesInThread:thread];
    TSInteraction *message = messages[(NSUInteger)arc4random_uniform((uint32_t)messages.count)];
    [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        [message saveWithTransaction:transaction];
    }];
}

+ (void)sendTextAndSystemMessages:(NSUInteger)counter thread:(TSThread *)thread
{
    if (counter < 1) {
        return;
    }
    if (arc4random_uniform(2) == 0) {
        [self sendTextMessageInThread:thread counter:counter];
    } else {
        [self createSystemMessageInThread:thread];
    }
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self sendTextAndSystemMessages:counter - 1 thread:thread];
    });
}

+ (NSString *)randomText
{
    NSArray<NSString *> *randomTexts = @[
        @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. ",
        (@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
         @"Suspendisse rutrum, nulla vitae pretium hendrerit, tellus "
         @"turpis pharetra libero, vitae sodales tortor ante vel sem."),
        @"In a time of universal deceit - telling the truth is a revolutionary act.",
        @"If you want a vision of the future, imagine a boot stamping on a human face - forever.",
        @"Who controls the past controls the future. Who controls the present controls the past.",
        @"All animals are equal, but some animals are more equal than others.",
        @"War is peace. Freedom is slavery. Ignorance is strength.",
        (@"All the war-propaganda, all the screaming and lies and hatred, comes invariably from people who are not "
         @"fighting."),
        (@"Political language. . . is designed to make lies sound truthful and murder respectable, and to give an "
         @"appearance of solidity to pure wind."),
        (@"The nationalist not only does not disapprove of atrocities committed by his own side, but he has a "
         @"remarkable capacity for not even hearing about them."),
        (@"Every generation imagines itself to be more intelligent than the one that went before it, and wiser than "
         @"the "
         @"one that comes after it."),
        @"War against a foreign country only happens when the moneyed classes think they are going to profit from it.",
        @"People have only as much liberty as they have the intelligence to want and the courage to take.",
        (@"You cannot buy the revolution. You cannot make the revolution. You can only be the revolution. It is in your "
        @"spirit, or it is nowhere."),
        (@"That is what I have always understood to be the essence of anarchism: the conviction that the burden of "
        @"proof has to be placed on authority, and that it should be dismantled if that burden cannot be met."),
        (@"Ask for work. If they don't give you work, ask for bread. If they do not give you work or bread, then take "
        @"bread."),
        @"Every society has the criminals it deserves.",
        (@"Anarchism is founded on the observation that since few men are wise enough to rule themselves, even fewer "
        @"are wise enough to rule others."),
        @"If you would know who controls you see who you may not criticise.",
        @"At one time in the world there were woods that no one owned."
    ];
    NSString *randomText = randomTexts[(NSUInteger)arc4random_uniform((uint32_t)randomTexts.count)];
    return randomText;
}

+ (void)createFakeThreads:(NSUInteger)threadCount withFakeMessages:(NSUInteger)messageCount
{
    [DebugUIContacts
        createRandomContacts:threadCount
              contactHandler:^(CNContact *_Nonnull contact, NSUInteger idx, BOOL *_Nonnull stop) {
                  NSString *phoneNumberText = contact.phoneNumbers.firstObject.value.stringValue;
                  OWSAssert(phoneNumberText);
                  PhoneNumber *phoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:phoneNumberText];
                  OWSAssert(phoneNumber);
                  OWSAssert(phoneNumber.toE164);

                  TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:phoneNumber.toE164];
                  [self sendFakeMessages:messageCount thread:contactThread];
                  DDLogError(@"Create fake thread: %@, interactions: %lu",
                      phoneNumber.toE164,
                      (unsigned long)contactThread.numberOfInteractions);
              }];
}

+ (void)sendFakeMessages:(NSUInteger)counter thread:(TSThread *)thread
{
    [self sendFakeMessages:counter thread:thread isTextOnly:NO];
}

+ (void)sendFakeMessages:(NSUInteger)counter thread:(TSThread *)thread isTextOnly:(BOOL)isTextOnly
{
    const NSUInteger kMaxBatchSize = 2500;
    if (counter < kMaxBatchSize) {
        [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            [self sendFakeMessages:counter thread:thread isTextOnly:isTextOnly transaction:transaction];
        }];
    } else {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSUInteger remainder = counter;
            while (remainder > 0) {
                NSUInteger batchSize = MIN(kMaxBatchSize, remainder);
                [OWSPrimaryStorage.dbReadWriteConnection
                    readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                        [self sendFakeMessages:batchSize thread:thread isTextOnly:isTextOnly transaction:transaction];
                    }];
                remainder -= batchSize;
                DDLogInfo(@"%@ sendFakeMessages %lu / %lu",
                    self.logTag,
                    (unsigned long)(counter - remainder),
                    (unsigned long)counter);
            }
        });
    }
}

+ (void)thrashInsertAndDeleteForThread:(TSThread *)thread counter:(NSUInteger)counter
{
    if (counter == 0) {
        return;
    }
    uint32_t sendDelay = arc4random_uniform((uint32_t)(0.01 * NSEC_PER_SEC));
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, sendDelay), dispatch_get_main_queue(), ^{
        [self sendFakeMessages:1 thread:thread];
    });

    uint32_t deleteDelay = arc4random_uniform((uint32_t)(0.01 * NSEC_PER_SEC));
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, deleteDelay), dispatch_get_main_queue(), ^{
        [OWSPrimaryStorage.sharedManager.dbReadWriteConnection
            asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
                [self deleteRandomMessages:1 thread:thread transaction:transaction];
            }];
        [self thrashInsertAndDeleteForThread:thread counter:counter - 1];
    });
}

// TODO: Remove.
+ (void)sendFakeMessages:(NSUInteger)counter
                  thread:(TSThread *)thread
              isTextOnly:(BOOL)isTextOnly
             transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    DDLogInfo(@"%@ sendFakeMessages: %lu", self.logTag, (unsigned long)counter);

    for (NSUInteger i = 0; i < counter; i++) {
        NSString *randomText = [self randomText];
        switch (arc4random_uniform(isTextOnly ? 2 : 4)) {
            case 0: {
                TSIncomingMessage *message =
                    [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                       inThread:thread
                                                                       authorId:@"+19174054215"
                                                                 sourceDeviceId:0
                                                                    messageBody:randomText
                                                                  attachmentIds:@[]
                                                               expiresInSeconds:0
                                                                  quotedMessage:nil
                                                                   contactShare:nil];
                [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
                break;
            }
            case 1: {
                [self createFakeOutgoingMessage:thread
                                    messageBody:randomText
                                fakeAssetLoader:nil
                                   messageState:TSOutgoingMessageStateFailed
                                    isDelivered:NO
                                         isRead:NO
                                  quotedMessage:nil
                                   contactShare:nil
                                    transaction:transaction];
                break;
            }
            case 2: {
                UInt32 filesize = 64;
                TSAttachmentPointer *pointer =
                    [[TSAttachmentPointer alloc] initWithServerId:237391539706350548
                                                              key:[self createRandomNSDataOfSize:filesize]
                                                           digest:nil
                                                        byteCount:filesize
                                                      contentType:@"audio/mp3"
                                                   sourceFilename:@"test.mp3"
                                                   attachmentType:TSAttachmentTypeDefault];
                pointer.state = TSAttachmentPointerStateFailed;
                [pointer saveWithTransaction:transaction];
                TSIncomingMessage *message =
                    [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                                       inThread:thread
                                                                       authorId:@"+19174054215"
                                                                 sourceDeviceId:0
                                                                    messageBody:nil
                                                                  attachmentIds:@[
                                                                      pointer.uniqueId,
                                                                  ]
                                                               expiresInSeconds:0
                                                                  quotedMessage:nil
                                                                   contactShare:nil];
                [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
                break;
            }
            case 3: {
                NSString *filename = @"test.mp3";
                UInt32 filesize = 16;

                TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"audio/mp3"
                                                                                             byteCount:filesize
                                                                                        sourceFilename:filename];

                NSError *error;
                BOOL success = [attachmentStream writeData:[self createRandomNSDataOfSize:filesize] error:&error];
                OWSAssert(success && !error);
                [attachmentStream saveWithTransaction:transaction];

                [self createFakeOutgoingMessage:thread
                                    messageBody:nil
                                   attachmentId:attachmentStream.uniqueId
                                       filename:filename
                                   messageState:TSOutgoingMessageStateFailed
                                    isDelivered:NO
                                         isRead:NO
                                 isVoiceMessage:NO
                                  quotedMessage:nil
                                   contactShare:nil
                                    transaction:transaction];
                break;
            }
        }
    }
}

#pragma mark -

+ (void)createNewGroups:(NSUInteger)counter recipientId:(NSString *)recipientId
{
    if (counter < 1) {
        return;
    }

    NSString *groupName = [NSUUID UUID].UUIDString;
    NSMutableArray<NSString *> *recipientIds = [@[
        recipientId,
        [TSAccountManager localNumber],
    ] mutableCopy];
    NSData *groupId = [SecurityUtils generateRandomBytes:16];
    TSGroupModel *groupModel =
        [[TSGroupModel alloc] initWithTitle:groupName memberIds:recipientIds image:nil groupId:groupId];

    __block TSGroupThread *thread;
    [OWSPrimaryStorage.dbReadWriteConnection
        readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
            thread = [TSGroupThread getOrCreateThreadWithGroupModel:groupModel transaction:transaction];
        }];
    OWSAssert(thread);

    TSOutgoingMessage *message =
        [TSOutgoingMessage outgoingMessageInThread:thread groupMetaMessage:TSGroupMessageNew expiresInSeconds:0];
    [message updateWithCustomMessage:NSLocalizedString(@"GROUP_CREATED", nil)];

    OWSMessageSender *messageSender = [Environment current].messageSender;
    void (^completion)(void) = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            [ThreadUtil sendMessageWithText:[@(counter) description]
                                   inThread:thread
                           quotedReplyModel:nil
                              messageSender:messageSender];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
                [self createNewGroups:counter - 1 recipientId:recipientId];
            });
        });
    };
    [messageSender enqueueMessage:message
                          success:completion
                          failure:^(NSError *error) {
                              completion();
                          }];
}

+ (void)injectFakeIncomingMessages:(NSUInteger)counter thread:(TSThread *)thread
{
    // Wait 5 seconds so debug user has time to navigate to another
    // view before message processing occurs.
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.f * NSEC_PER_SEC)),
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
        ^{
            for (NSUInteger i = 0; i < counter; i++) {
                [self injectIncomingMessageInThread:thread counter:counter - i];
            }
        });
}

+ (void)injectIncomingMessageInThread:(TSThread *)thread counter:(NSUInteger)counter
{
    OWSAssert(thread);

    DDLogInfo(@"%@ injectIncomingMessageInThread: %lu", self.logTag, (unsigned long)counter);

    NSString *randomText = [self randomText];
    NSString *text = [[[@(counter) description] stringByAppendingString:@" "] stringByAppendingString:randomText];

    SSKProtoDataMessageBuilder *dataMessageBuilder = [SSKProtoDataMessageBuilder new];
    [dataMessageBuilder setBody:text];

    if ([thread isKindOfClass:[TSGroupThread class]]) {
        TSGroupThread *groupThread = (TSGroupThread *)thread;
        SSKProtoGroupContextBuilder *groupBuilder =
            [[SSKProtoGroupContextBuilder alloc] initWithId:groupThread.groupModel.groupId
                                                       type:SSKProtoGroupContextTypeDeliver];
        [dataMessageBuilder setGroup:groupBuilder.buildIgnoringErrors];
    }

    SSKProtoContentBuilder *payloadBuilder = [SSKProtoContentBuilder new];
    [payloadBuilder setDataMessage:dataMessageBuilder.buildIgnoringErrors];
    NSData *plaintextData = [payloadBuilder buildIgnoringErrors].serializedDataIgnoringErrors;

    // Try to use an arbitrary member of the current thread that isn't
    // ourselves as the sender.
    NSString *_Nullable recipientId = [[thread recipientIdentifiers] firstObject];
    // This might be an "empty" group with no other members.  If so, use a fake
    // sender id.
    if (!recipientId) {
        recipientId = @"+12345678901";
    }

    uint64_t timestamp = [NSDate ows_millisecondTimeStamp];
    NSString *source = recipientId;
    uint32_t sourceDevice = 1;
    SSKProtoEnvelopeType envelopeType = SSKProtoEnvelopeTypeCiphertext;
    NSData *content = plaintextData;

    SSKProtoEnvelopeBuilder *envelopeBuilder = [[SSKProtoEnvelopeBuilder alloc] initWithType:envelopeType
                                                                                      source:source
                                                                                sourceDevice:sourceDevice
                                                                                   timestamp:timestamp];
    envelopeBuilder.content = content;
    NSError *error;
    NSData *_Nullable envelopeData = [envelopeBuilder buildSerializedDataAndReturnError:&error];
    if (error || !envelopeData) {
        OWSFail(@"%@ Could not serialize envelope: %@.", self.logTag, error);
        return;
    }

    [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        [[OWSBatchMessageProcessor sharedInstance] enqueueEnvelopeData:envelopeData
                                                         plaintextData:plaintextData
                                                           transaction:transaction];
    }];
}

+ (void)performRandomActions:(NSUInteger)counter thread:(TSThread *)thread
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)),
                   dispatch_get_main_queue(),
                   ^{
                       [self performRandomActionInThread:thread counter:counter];
                       if (counter > 0) {
                           [self performRandomActions:counter - 1 thread:thread];
                       }
                   });
}

+ (void)performRandomActionInThread:(TSThread *)thread counter:(NSUInteger)counter
{
    typedef void (^TransactionBlock)(YapDatabaseReadWriteTransaction *transaction);
    NSArray<TransactionBlock> *actionBlocks = @[
        ^(YapDatabaseReadWriteTransaction *transaction) {
            // injectIncomingMessageInThread doesn't take a transaction.
            dispatch_async(dispatch_get_main_queue(), ^{
                [self injectIncomingMessageInThread:thread counter:counter];
            });
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            // sendTextMessageInThread doesn't take a transaction.
            dispatch_async(dispatch_get_main_queue(), ^{
                [self sendTextMessageInThread:thread counter:counter];
            });
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self sendFakeMessages:messageCount thread:thread isTextOnly:NO transaction:transaction];
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self deleteRandomMessages:messageCount thread:thread transaction:transaction];
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self deleteLastMessages:messageCount thread:thread transaction:transaction];
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self deleteRandomRecentMessages:messageCount thread:thread transaction:transaction];
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self insertAndDeleteNewOutgoingMessages:messageCount thread:thread transaction:transaction];
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self resurrectNewOutgoingMessages1:messageCount thread:thread transaction:transaction];
        },
        ^(YapDatabaseReadWriteTransaction *transaction) {
            NSUInteger messageCount = (NSUInteger)(1 + arc4random_uniform(4));
            [self resurrectNewOutgoingMessages2:messageCount thread:thread transaction:transaction];
        },
    ];
    [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        NSUInteger actionCount = 1 + (NSUInteger)arc4random_uniform(3);
        for (NSUInteger actionIdx = 0; actionIdx < actionCount; actionIdx++) {
            TransactionBlock actionBlock = actionBlocks[(NSUInteger)arc4random_uniform((uint32_t)actionBlocks.count)];
            actionBlock(transaction);
        }
    }];
}

+ (void)deleteRandomMessages:(NSUInteger)count
                      thread:(TSThread *)thread
                 transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    DDLogInfo(@"%@ deleteRandomMessages: %zd", self.logTag, count);

    YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
    NSUInteger messageCount = [interactionsByThread numberOfItemsInGroup:thread.uniqueId];

    NSMutableArray<NSNumber *> *messageIndices = [NSMutableArray new];
    for (NSUInteger messageIdx = 0; messageIdx < messageCount; messageIdx++) {
        [messageIndices addObject:@(messageIdx)];
    }
    NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
    for (NSUInteger i = 0; i < count && messageIndices.count > 0; i++) {
        NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)messageIndices.count);
        NSNumber *messageIdx = messageIndices[idx];
        [messageIndices removeObjectAtIndex:idx];

        TSInteraction *_Nullable interaction =
            [interactionsByThread objectAtIndex:messageIdx.unsignedIntegerValue inGroup:thread.uniqueId];
        OWSAssert(interaction);
        [interactions addObject:interaction];
    }

    for (TSInteraction *interaction in interactions) {
        [interaction removeWithTransaction:transaction];
    }
}

+ (void)deleteLastMessages:(NSUInteger)count
                    thread:(TSThread *)thread
               transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    DDLogInfo(@"%@ deleteLastMessages", self.logTag);

    YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
    NSUInteger messageCount = (NSUInteger)[interactionsByThread numberOfItemsInGroup:thread.uniqueId];

    NSMutableArray<NSNumber *> *messageIndices = [NSMutableArray new];
    for (NSUInteger i = 0; i < count && i < messageCount; i++) {
        NSUInteger messageIdx = messageCount - (1 + i);
        [messageIndices addObject:@(messageIdx)];
    }
    NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
    for (NSNumber *messageIdx in messageIndices) {
        TSInteraction *_Nullable interaction =
            [interactionsByThread objectAtIndex:messageIdx.unsignedIntegerValue inGroup:thread.uniqueId];
        OWSAssert(interaction);
        [interactions addObject:interaction];
    }
    for (TSInteraction *interaction in interactions) {
        [interaction removeWithTransaction:transaction];
    }
}

+ (void)deleteRandomRecentMessages:(NSUInteger)count
                            thread:(TSThread *)thread
                       transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    DDLogInfo(@"%@ deleteRandomRecentMessages: %zd", self.logTag, count);

    YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName];
    NSInteger messageCount = (NSInteger)[interactionsByThread numberOfItemsInGroup:thread.uniqueId];

    NSMutableArray<NSNumber *> *messageIndices = [NSMutableArray new];
    const NSInteger kRecentMessageCount = 10;
    for (NSInteger i = 0; i < kRecentMessageCount; i++) {
        NSInteger messageIdx = messageCount - (1 + i);
        if (messageIdx >= 0) {
            [messageIndices addObject:@(messageIdx)];
        }
    }
    NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
    for (NSUInteger i = 0; i < count && messageIndices.count > 0; i++) {
        NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)messageIndices.count);
        NSNumber *messageIdx = messageIndices[idx];
        [messageIndices removeObjectAtIndex:idx];

        TSInteraction *_Nullable interaction =
            [interactionsByThread objectAtIndex:messageIdx.unsignedIntegerValue inGroup:thread.uniqueId];
        OWSAssert(interaction);
        [interactions addObject:interaction];
    }
    for (TSInteraction *interaction in interactions) {
        [interaction removeWithTransaction:transaction];
    }
}

+ (void)insertAndDeleteNewOutgoingMessages:(NSUInteger)count
                                    thread:(TSThread *)thread
                               transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    DDLogInfo(@"%@ insertAndDeleteNewOutgoingMessages: %zd", self.logTag, count);

    NSMutableArray<TSOutgoingMessage *> *messages = [NSMutableArray new];
    for (NSUInteger i =0; i < count; i++) {
        NSString *text = [self randomText];
        OWSDisappearingMessagesConfiguration *configuration =
            [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId transaction:transaction];

        uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0);
        TSOutgoingMessage *message = [TSOutgoingMessage outgoingMessageInThread:thread
                                                                    messageBody:text
                                                                   attachmentId:nil
                                                               expiresInSeconds:expiresInSeconds];
        DDLogError(@"%@ insertAndDeleteNewOutgoingMessages timestamp: %llu.", self.logTag, message.timestamp);
        [messages addObject:message];
    }

    for (TSOutgoingMessage *message in messages) {
        [message saveWithTransaction:transaction];
    }
    for (TSOutgoingMessage *message in messages) {
        [message removeWithTransaction:transaction];
    }
}

+ (void)resurrectNewOutgoingMessages1:(NSUInteger)count
                               thread:(TSThread *)thread
                          transaction:(YapDatabaseReadWriteTransaction *)initialTransaction
{
    DDLogInfo(@"%@ resurrectNewOutgoingMessages1.1: %zd", self.logTag, count);

    NSMutableArray<TSOutgoingMessage *> *messages = [NSMutableArray new];
    for (NSUInteger i =0; i < count; i++) {
        NSString *text = [self randomText];
        OWSDisappearingMessagesConfiguration *configuration =
            [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId
                                                              transaction:initialTransaction];

        uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0);
        TSOutgoingMessage *message = [TSOutgoingMessage outgoingMessageInThread:thread
                                                                    messageBody:text
                                                                   attachmentId:nil
                                                               expiresInSeconds:expiresInSeconds];
        DDLogError(@"%@ resurrectNewOutgoingMessages1 timestamp: %llu.", self.logTag, message.timestamp);
        [messages addObject:message];
    }

    for (TSOutgoingMessage *message in messages) {
        [message saveWithTransaction:initialTransaction];
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        DDLogInfo(@"%@ resurrectNewOutgoingMessages1.2: %zd", self.logTag, count);
        [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            for (TSOutgoingMessage *message in messages) {
                [message removeWithTransaction:transaction];
            }
            for (TSOutgoingMessage *message in messages) {
                [message saveWithTransaction:transaction];
            }
        }];
    });
}

+ (void)resurrectNewOutgoingMessages2:(NSUInteger)count
                               thread:(TSThread *)thread
                          transaction:(YapDatabaseReadWriteTransaction *)initialTransaction
{
    DDLogInfo(@"%@ resurrectNewOutgoingMessages2.1: %zd", self.logTag, count);

    NSMutableArray<TSOutgoingMessage *> *messages = [NSMutableArray new];
    for (NSUInteger i =0; i < count; i++) {
        NSString *text = [self randomText];
        OWSDisappearingMessagesConfiguration *configuration =
            [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId
                                                              transaction:initialTransaction];
        TSOutgoingMessage *message = [[TSOutgoingMessage alloc]
            initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                    inThread:thread
                                 messageBody:text
                               attachmentIds:[NSMutableArray new]
                            expiresInSeconds:(configuration.isEnabled ? configuration.durationSeconds
                                                                      : 0)expireStartedAt:0
                              isVoiceMessage:NO
                            groupMetaMessage:TSGroupMessageUnspecified
                               quotedMessage:nil
                                contactShare:nil];
        DDLogError(@"%@ resurrectNewOutgoingMessages2 timestamp: %llu.", self.logTag, message.timestamp);
        [messages addObject:message];
    }

    for (TSOutgoingMessage *message in messages) {
        [message updateWithFakeMessageState:TSOutgoingMessageStateSending transaction:initialTransaction];
        [message saveWithTransaction:initialTransaction];
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        DDLogInfo(@"%@ resurrectNewOutgoingMessages2.2: %zd", self.logTag, count);
        [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            for (TSOutgoingMessage *message in messages) {
                [message removeWithTransaction:transaction];
            }
        }];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            DDLogInfo(@"%@ resurrectNewOutgoingMessages2.3: %zd", self.logTag, count);
            [OWSPrimaryStorage.dbReadWriteConnection
                readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
                    for (TSOutgoingMessage *message in messages) {
                        [message saveWithTransaction:transaction];
                    }
                }];
        });
    });
}

+ (void)createTimestampMessagesInThread:(TSThread *)thread
{
    OWSAssert(thread);

    long long now = (long long)[NSDate ows_millisecondTimeStamp];
    NSArray<NSNumber *> *timestamps = @[
        @(now + 1 * (long long)kHourInMs),
        @(now),
        @(now - 1 * (long long)kHourInMs),
        @(now - 12 * (long long)kHourInMs),
        @(now - 1 * (long long)kDayInMs),
        @(now - 2 * (long long)kDayInMs),
        @(now - 3 * (long long)kDayInMs),
        @(now - 6 * (long long)kDayInMs),
        @(now - 7 * (long long)kDayInMs),
        @(now - 8 * (long long)kDayInMs),
        @(now - 2 * (long long)kWeekInMs),
        @(now - 1 * (long long)kMonthInMs),
        @(now - 2 * (long long)kMonthInMs),
    ];
    NSMutableArray<NSString *> *recipientIds = [thread.recipientIdentifiers mutableCopy];
    [recipientIds removeObject:[TSAccountManager localNumber]];
    NSString *recipientId = (recipientIds.count > 0 ? recipientIds.firstObject : @"+19174054215");

    [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
        for (NSNumber *timestamp in timestamps) {
            NSString *randomText = [self randomText];
            {
                TSIncomingMessage *message =
                    [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp.unsignedLongLongValue
                                                                       inThread:thread
                                                                       authorId:recipientId
                                                                 sourceDeviceId:0
                                                                    messageBody:randomText
                                                                  attachmentIds:[NSMutableArray new]
                                                               expiresInSeconds:0
                                                                  quotedMessage:nil
                                                                   contactShare:nil];
                [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
            }
            {
                TSOutgoingMessage *message =
                    [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:timestamp.unsignedLongLongValue
                                                                       inThread:thread
                                                                    messageBody:randomText
                                                                  attachmentIds:[NSMutableArray new]
                                                               expiresInSeconds:0
                                                                expireStartedAt:0
                                                                 isVoiceMessage:NO
                                                               groupMetaMessage:TSGroupMessageUnspecified
                                                                  quotedMessage:nil
                                                                   contactShare:nil];
                [message saveWithTransaction:transaction];
                [message updateWithFakeMessageState:TSOutgoingMessageStateSent transaction:transaction];
                [message updateWithSentRecipient:recipientId transaction:transaction];
                [message updateWithDeliveredRecipient:recipientId deliveryTimestamp:timestamp transaction:transaction];
                [message updateWithReadRecipientId:recipientId
                                     readTimestamp:timestamp.unsignedLongLongValue
                                       transaction:transaction];
            }
        }
    }];
}

+ (void)createDisappearingMessagesWhichFailedToStartInThread:(TSThread *)thread
{
    uint64_t now = [NSDate ows_millisecondTimeStamp];
    TSIncomingMessage *message = [[TSIncomingMessage alloc]
        initIncomingMessageWithTimestamp:now
                                inThread:thread
                                authorId:thread.recipientIdentifiers.firstObject
                          sourceDeviceId:0
                             messageBody:[NSString
                                             stringWithFormat:@"Should disappear 60s after %lu", (unsigned long)now]
                           attachmentIds:[NSMutableArray new]
                        expiresInSeconds:60
                           quotedMessage:nil
                            contactShare:nil];
    // private setter to avoid starting expire machinery.
    message.read = YES;
    [message save];
}

+ (void)testIndicScriptsInThread:(TSThread *)thread
{
    NSArray<NSString *> *strings = @[
        @"\u0C1C\u0C4D\u0C1E\u200C\u0C3E",
        @"\u09B8\u09CD\u09B0\u200C\u09C1",
        @"non-crashing string",
    ];

    [OWSPrimaryStorage.sharedManager.dbReadWriteConnection
        readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            for (NSString *string in strings) {
                // DO NOT log these strings with the debugger attached.
                //        DDLogInfo(@"%@ %@", self.logTag, string);

                {
                    [self createFakeIncomingMessage:thread
                                        messageBody:string
                                    fakeAssetLoader:nil
                             isAttachmentDownloaded:NO
                                      quotedMessage:nil
                                        transaction:transaction];
                }
                {
                    NSString *recipientId = @"+19174054215";
                    NSString *groupName = string;
                    NSMutableArray<NSString *> *recipientIds = [@[
                        recipientId,
                        [TSAccountManager localNumber],
                    ] mutableCopy];
                    NSData *groupId = [SecurityUtils generateRandomBytes:16];
                    TSGroupModel *groupModel =
                        [[TSGroupModel alloc] initWithTitle:groupName memberIds:recipientIds image:nil groupId:groupId];

                    TSGroupThread *groupThread =
                        [TSGroupThread getOrCreateThreadWithGroupModel:groupModel transaction:transaction];
                    OWSAssert(groupThread);
                }
            }
        }];
}

+ (void)testZalgoTextInThread:(TSThread *)thread
{
    NSArray<NSString *> *strings = @[
        @"Ṱ̴̤̺̣͚͚̭̰̤̮̑̓̀͂͘͡h̵̢̤͔̼̗̦̖̬͌̀͒̀͘i̴̮̤͎͎̝̖̻͓̅̆͆̓̎͘͡ͅŝ̡̡̳͔̓͗̾̀̇͒͘͢͢͡͡ ỉ̛̲̩̫̝͉̀̒͐͋̾͘͢͡͞s̶̨̫̞̜̹͛́̇͑̅̒̊̈ s̵͍̲̗̠̗͈̦̬̉̿͂̏̐͆̾͐͊̾ǫ̶͍̼̝̉͊̉͢͜͞͝ͅͅṁ̵̡̨̬̤̝͔̣̄̍̋͊̿̄͋̈ͅe̪̪̻̱͖͚͈̲̍̃͘͠͝ z̷̢̢̛̩̦̱̺̼͑́̉̾ą͕͎̠̮̹̱̓̔̓̈̈́̅̐͢l̵̨͚̜͉̟̜͉͎̃͆͆͒͑̍̈̚͜͞ğ͔̖̫̞͎͍̒̂́̒̿̽̆͟o̶̢̬͚̘̤̪͇̻̒̋̇̊̏͢͡͡͠ͅ t̡̛̥̦̪̮̅̓̑̈́̉̓̽͛͢͡ȩ̡̩͓͈̩͎͗̔͑̌̓͊͆͝x̫̦͓̤͓̘̝̪͊̆͌͊̽̃̏͒͘͘͢ẗ̶̢̨̛̰̯͕͔́̐͗͌͟͠.̷̩̼̼̩̞̘̪́͗̅͊̎̾̅̏̀̕͟ͅ",
        @"This is some normal text",
    ];

    [OWSPrimaryStorage.sharedManager.dbReadWriteConnection
        readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            for (NSString *string in strings) {
                DDLogInfo(@"%@ sending zalgo", self.logTag);

                {
                    [self createFakeIncomingMessage:thread
                                        messageBody:string
                                    fakeAssetLoader:nil
                             isAttachmentDownloaded:NO
                                      quotedMessage:nil
                                        transaction:transaction];
                }
                {
                    NSString *recipientId = @"+19174054215";
                    NSString *groupName = string;
                    NSMutableArray<NSString *> *recipientIds = [@[
                        recipientId,
                        [TSAccountManager localNumber],
                    ] mutableCopy];
                    NSData *groupId = [SecurityUtils generateRandomBytes:16];
                    TSGroupModel *groupModel =
                        [[TSGroupModel alloc] initWithTitle:groupName memberIds:recipientIds image:nil groupId:groupId];

                    TSGroupThread *groupThread =
                        [TSGroupThread getOrCreateThreadWithGroupModel:groupModel transaction:transaction];
                    OWSAssert(groupThread);
                }
            }
        }];
}

+ (void)testDirectionalFilenamesInThread:(TSThread *)thread
{
    NSMutableArray<NSString *> *filenames = [@[
        @"a_test\u202Dabc.exe",
        @"b_test\u202Eabc.exe",
        @"c_testabc.exe",
    ] mutableCopy];
    __block void (^sendUnsafeFile)(void);
    sendUnsafeFile = ^{
        if (filenames.count < 1) {
            return;
        }
        NSString *filename = filenames.lastObject;
        [filenames removeLastObject];
        OWSMessageSender *messageSender = [Environment current].messageSender;
        NSString *utiType = (NSString *)kUTTypeData;
        const NSUInteger kDataLength = 32;
        DataSource *_Nullable dataSource =
            [DataSourceValue dataSourceWithData:[self createRandomNSDataOfSize:kDataLength] utiType:utiType];
        [dataSource setSourceFilename:filename];
        SignalAttachment *attachment =
            [SignalAttachment attachmentWithDataSource:dataSource dataUTI:utiType imageQuality:TSImageQualityOriginal];

        OWSAssert(attachment);
        if ([attachment hasError]) {
            DDLogError(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]);
            [DDLog flushLog];
        }
        OWSAssert(![attachment hasError]);
        [ThreadUtil sendMessageWithAttachment:attachment
                                     inThread:thread
                             quotedReplyModel:nil
                                messageSender:messageSender
                                   completion:nil];

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            sendUnsafeFile();
            sendUnsafeFile = nil;
        });
    };
}

+ (void)deleteAllMessagesInThread:(TSThread *)thread
{
    [OWSPrimaryStorage.sharedManager.newDatabaseConnection
        readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            [thread removeAllThreadInteractionsWithTransaction:transaction];
        }];
}

#pragma mark - Utility

+ (NSString *)actionLabelForHasCaption:(BOOL)hasCaption
                  outgoingMessageState:(TSOutgoingMessageState)outgoingMessageState
                           isDelivered:(BOOL)isDelivered
                                isRead:(BOOL)isRead
{
    NSMutableString *label = [NSMutableString new];
    if (hasCaption) {
        [label appendString:@" 🔤"];
    }
    if (outgoingMessageState == TSOutgoingMessageStateFailed) {
        [label appendString:@" (Unsent)"];
    } else if (outgoingMessageState == TSOutgoingMessageStateSending) {
        [label appendString:@" (Sending)"];
    } else if (outgoingMessageState == TSOutgoingMessageStateSent) {
        if (isRead) {
            [label appendString:@" (Read)"];
        } else if (isDelivered) {
            [label appendString:@" (Delivered)"];
        } else {
            [label appendString:@" (Sent)"];
        }
    } else {
        OWSFail(@"%@ unknown message state.", self.logTag);
    }
    return label;
}

+ (TSOutgoingMessage *)createFakeOutgoingMessage:(TSThread *)thread
                                     messageBody:(nullable NSString *)messageBody
                                 fakeAssetLoader:(nullable DebugUIMessagesAssetLoader *)fakeAssetLoader
                                    messageState:(TSOutgoingMessageState)messageState
                                     isDelivered:(BOOL)isDelivered
                                          isRead:(BOOL)isRead
                                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
                                    contactShare:(nullable OWSContact *)contactShare
                                     transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(thread);

    // Seamlessly convert oversize text messages to oversize text attachments.
    if ([messageBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
        OWSAssert(!fakeAssetLoader);
        fakeAssetLoader = [DebugUIMessagesAssetLoader oversizeTextInstanceWithText:messageBody];
        messageBody = nil;
    }

    TSAttachment *_Nullable attachment = nil;
    if (fakeAssetLoader) {
        attachment = [self createFakeAttachment:fakeAssetLoader isAttachmentDownloaded:YES transaction:transaction];
    }

    return [self createFakeOutgoingMessage:thread
                               messageBody:messageBody
                              attachmentId:attachment.uniqueId
                                  filename:fakeAssetLoader.filename
                              messageState:messageState
                               isDelivered:isDelivered
                                    isRead:isRead
                            isVoiceMessage:attachment.isVoiceMessage
                             quotedMessage:quotedMessage
                              contactShare:contactShare
                               transaction:transaction];
}

+ (TSOutgoingMessage *)createFakeOutgoingMessage:(TSThread *)thread
                                     messageBody:(nullable NSString *)messageBody
                                    attachmentId:(nullable NSString *)attachmentId
                                        filename:(nullable NSString *)filename
                                    messageState:(TSOutgoingMessageState)messageState
                                     isDelivered:(BOOL)isDelivered
                                          isRead:(BOOL)isRead
                                  isVoiceMessage:(BOOL)isVoiceMessage
                                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
                                    contactShare:(nullable OWSContact *)contactShare
                                     transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(thread);
    OWSAssert(transaction);
    OWSAssert(messageBody.length > 0 || attachmentId.length > 0 || contactShare);

    NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
    if (attachmentId) {
        [attachmentIds addObject:attachmentId];
    }

    TSOutgoingMessage *message =
        [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                           inThread:thread
                                                        messageBody:messageBody
                                                      attachmentIds:attachmentIds
                                                   expiresInSeconds:0
                                                    expireStartedAt:0
                                                     isVoiceMessage:isVoiceMessage
                                                   groupMetaMessage:TSGroupMessageUnspecified
                                                      quotedMessage:quotedMessage
                                                       contactShare:contactShare];

    if (attachmentId.length > 0 && filename.length > 0) {
        message.attachmentFilenameMap[attachmentId] = filename;
    }

    [message saveWithTransaction:transaction];
    [message updateWithFakeMessageState:messageState transaction:transaction];
    if (isDelivered) {
        NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject;
        OWSAssert(recipientId.length > 0);
        [message updateWithDeliveredRecipient:recipientId
                            deliveryTimestamp:@([NSDate ows_millisecondTimeStamp])
                                  transaction:transaction];
    }
    if (isRead) {
        NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject;
        OWSAssert(recipientId.length > 0);
        [message updateWithReadRecipientId:recipientId
                             readTimestamp:[NSDate ows_millisecondTimeStamp]
                               transaction:transaction];
    }
    return message;
}

+ (TSIncomingMessage *)createFakeIncomingMessage:(TSThread *)thread
                                     messageBody:(nullable NSString *)messageBody
                                 fakeAssetLoader:(nullable DebugUIMessagesAssetLoader *)fakeAssetLoader
                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
                                     transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(thread);

    // Seamlessly convert oversize text messages to oversize text attachments.
    if ([messageBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
        OWSAssert(!fakeAssetLoader);
        fakeAssetLoader = [DebugUIMessagesAssetLoader oversizeTextInstanceWithText:messageBody];
        messageBody = nil;
    }

    TSAttachment *_Nullable attachment = nil;
    if (fakeAssetLoader) {
        attachment = [self createFakeAttachment:fakeAssetLoader
                         isAttachmentDownloaded:isAttachmentDownloaded
                                    transaction:transaction];
    }

    return [self createFakeIncomingMessage:thread
                               messageBody:messageBody
                              attachmentId:attachment.uniqueId
                                  filename:fakeAssetLoader.filename
                    isAttachmentDownloaded:isAttachmentDownloaded
                             quotedMessage:quotedMessage
                               transaction:transaction];
}

+ (TSIncomingMessage *)createFakeIncomingMessage:(TSThread *)thread
                                     messageBody:(nullable NSString *)messageBody
                                    attachmentId:(nullable NSString *)attachmentId
                                        filename:(nullable NSString *)filename
                          isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
                                     transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(thread);
    OWSAssert(transaction);
    OWSAssert(messageBody.length > 0 || attachmentId.length > 0);

    NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
    if (attachmentId) {
        [attachmentIds addObject:attachmentId];
    }

    //    // Random time within last n years. Helpful for filling out a media gallery over time.
    //    double yearsMillis = 4.0 * kYearsInMs;
    //    uint64_t millisAgo = (uint64_t)(((double)arc4random() / ((double)0xffffffff)) * yearsMillis);
    //    uint64_t timestamp = [NSDate ows_millisecondTimeStamp] - millisAgo;

    TSIncomingMessage *message =
        [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp]
                                                           inThread:thread
                                                           authorId:@"+19174054215"
                                                     sourceDeviceId:0
                                                        messageBody:messageBody
                                                      attachmentIds:attachmentIds
                                                   expiresInSeconds:0
                                                      quotedMessage:quotedMessage
                                                       contactShare:nil];
    [message markAsReadNowWithSendReadReceipt:NO transaction:transaction];
    return message;
}

+ (TSAttachment *)createFakeAttachment:(DebugUIMessagesAssetLoader *)fakeAssetLoader
                isAttachmentDownloaded:(BOOL)isAttachmentDownloaded
                           transaction:(YapDatabaseReadWriteTransaction *)transaction
{
    OWSAssert(fakeAssetLoader);
    OWSAssert(fakeAssetLoader.filePath);
    OWSAssert(transaction);

    if (isAttachmentDownloaded) {
        DataSource *dataSource =
            [DataSourcePath dataSourceWithFilePath:fakeAssetLoader.filePath shouldDeleteOnDeallocation:NO];
        NSString *filename = dataSource.sourceFilename;
        // To support "fake missing" attachments, we sometimes lie about the
        // length of the data.
        UInt32 nominalDataLength = (UInt32)MAX((NSUInteger)1, dataSource.dataLength);
        TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:fakeAssetLoader.mimeType
                                                                                     byteCount:nominalDataLength
                                                                                sourceFilename:filename];
        NSError *error;
        BOOL success = [attachmentStream writeData:dataSource.data error:&error];
        OWSAssert(success && !error);
        [attachmentStream saveWithTransaction:transaction];
        return attachmentStream;
    } else {
        UInt32 filesize = 64;
        TSAttachmentPointer *attachmentPointer =
            [[TSAttachmentPointer alloc] initWithServerId:237391539706350548
                                                      key:[self createRandomNSDataOfSize:filesize]
                                                   digest:nil
                                                byteCount:filesize
                                              contentType:fakeAssetLoader.mimeType
                                           sourceFilename:fakeAssetLoader.filename
                                           attachmentType:TSAttachmentTypeDefault];
        attachmentPointer.state = TSAttachmentPointerStateFailed;
        [attachmentPointer saveWithTransaction:transaction];
        return attachmentPointer;
    }
}

@end

NS_ASSUME_NONNULL_END
