//
//  TLSLoggingTests.m
//  TLSLoggingTests
//
//  Created on 12/11/13.
//  Copyright (c) 2016 Twitter, Inc.
//

#include <pthread.h>

#import <TwitterLoggingService/TwitterLoggingService.h>
#import <XCTest/XCTest.h>

static void LogStream(id<TLSOutputStream> stream, TLSLogLevel level, NSString *channel, NSString *file, NSString *function, unsigned int line, NSString *format, ...);
static NSTimeInterval NSTimeIntervalFromTLSFileOutputStreamTimestamp(NSString *timestamp);
static id GenerateLoggingArgument(void);
static void TurnConsoleChannelOn(NSString *channel, BOOL on);
static BOOL IsConsoleChannelOn(NSString *channel);

@interface TestLogger : NSObject <TLSOutputStream>
@property (nonatomic) TLSLogLevelMask permittedLoggingLevels;
@property (nonatomic) BOOL shouldFilterChannelsThatAreOff;
@property (nonatomic, readonly) NSUInteger loggedMessages;
- (void)setChannel:(NSString *)channel on:(BOOL)on;
@end

@interface TestStdErrLogger : TLSStdErrOutputStream
@end

@interface TestNSLogLogger : TLSNSLogOutputStream
@end

@interface TestFileLogger : TLSFileOutputStream
@end

@interface TLSLoggingTests : XCTestCase
@end

@interface TestRollingLogger : NSObject <TLSOutputStream>
@property (nonatomic) TLSLogLevelMask permittedLoggingLevels;
@property (nonatomic) BOOL shouldFilterChannelsThatAreOff;
@property (nonatomic, readonly) NSUInteger loggedMessages;
- (void)setChannel:(NSString *)channel on:(BOOL)on;
@end

@interface TestRollingFileLogger : TLSRollingFileOutputStream
@end

@interface TLSRollingFileTests : XCTestCase
@end

typedef void(^TestLoggingBlock)(NSString *channel);
typedef void(^TestStreamBlock)(id<TLSOutputStream> stream, NSString *channel);

static TLSLoggingService *sLoggingService;
static TLSStdErrOutputStream *sStdErrOut;
static TLSNSLogOutputStream *sNSLogOut;
static TLSFileOutputStream *sFileOut;
static TLSRollingFileOutputStream *sRollingFileOut;
static TestLoggingBlock sTestLoggingBlock;
static TestStreamBlock sTestStreamBlock;

static NSMutableSet *sOnConsoleChannels;
static NSMutableDictionary *sRuntimes;

#define TEST_MX_COUNT (3 * TEST_COUNT)

#define TEST_COUNT (250)

#define TEST_START NSDate *__start__ = [NSDate date];

#define TEST_STOP sRuntimes[NSStringFromSelector(_cmd)] = @([[NSDate date] timeIntervalSinceDate:__start__]);

#define TEST_UPDATE_CHANNEL(channel, on) do { [sLoggingService dispatchAsynchronousTransaction:^{ TurnConsoleChannelOn(channel, on); }]; [sLoggingService updateOutputStream:sStdErrOut]; [sLoggingService updateOutputStream:sNSLogOut]; } while (0)

#define TEST_CHANNEL_ON(channel)    TEST_UPDATE_CHANNEL(channel, YES)

#define TEST_CHANNEL_OFF(channel)   TEST_UPDATE_CHANNEL(channel, NO)

#define TEST_FILE_NAME @"TLSFileOutputStreamTest.log"

#define TEST_FLUSH_TRANSACTIONS() [sLoggingService dispatchSynchronousTransaction:^{}]

@implementation TLSLoggingTests

+ (void)setUp
{
    [super setUp];
    sLoggingService = [TLSLoggingService sharedInstance];
    sStdErrOut = [[TestStdErrLogger alloc] init];
    sNSLogOut = [[TestNSLogLogger alloc] init];
    NSString *defaultLogFileDirectoryPath = [TLSFileOutputStream defaultLogFileDirectoryPath];
    [[NSFileManager defaultManager] removeItemAtPath:defaultLogFileDirectoryPath error:NULL];
    sFileOut = [[TestFileLogger alloc] initWithLogFileName:TEST_FILE_NAME error:NULL];
    sRollingFileOut = [[TestRollingFileLogger alloc] initWithLogFileDirectoryPath:defaultLogFileDirectoryPath logFilePrefix:TLSRollingFileOutputStreamDefaultLogFilePrefix maxLogFiles:5 maxBytesPerLogFile:(1024 * 64)];
    sTestLoggingBlock = ^(NSString *theChannel) {
        TLSLogDebug(theChannel, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
        TLSLogInformation(theChannel, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
        TLSLogWarning(theChannel, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
        TLSLogError(theChannel, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
    };
    sTestStreamBlock = ^(id<TLSOutputStream> stream, NSString *channel) {
        LogStream(stream, TLSLogLevelDebug, channel, @(__FILE__), @(__PRETTY_FUNCTION__), __LINE__, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
        LogStream(stream, TLSLogLevelInformation, channel, @(__FILE__), @(__PRETTY_FUNCTION__), __LINE__, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
        LogStream(stream, TLSLogLevelWarning, channel, @(__FILE__), @(__PRETTY_FUNCTION__), __LINE__, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
        LogStream(stream, TLSLogLevelError, channel, @(__FILE__), @(__PRETTY_FUNCTION__), __LINE__, @"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil);
    };
    sRuntimes = [[NSMutableDictionary alloc] init];
    sOnConsoleChannels = [[NSMutableSet alloc] init];
}

- (void)testLoggingStdErr1
{
    TEST_START
    [sLoggingService addOutputStream:sStdErrOut];

    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-StdErr"];

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_STOP
}

- (void)testLoggingStdErr2
{
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-StdErr"];
    [sLoggingService removeOutputStream:sStdErrOut];
    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    [sLoggingService flush];
}

- (void)testLoggingStdErr3
{
    TEST_START
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-StdErrMx"];
    TEST_CHANNEL_ON(channel);
    [sLoggingService addOutputStream:sStdErrOut];
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestLoggingBlock(channel);
        });
    }

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    TEST_CHANNEL_OFF(channel);
    TEST_STOP

    [sLoggingService flush];
    [sLoggingService removeOutputStream:sStdErrOut];
}

- (void)testLoggingNSLog1
{
    TEST_START
    [sLoggingService addOutputStream:sNSLogOut];

    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-NSLog"];

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_STOP
}

- (void)testLoggingNSLog2
{
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-NSLog"];
    [sLoggingService removeOutputStream:sNSLogOut];
    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    };

    TEST_CHANNEL_OFF(channel);

    [sLoggingService flush];
}

- (void)testLoggingNSLog3
{
    TEST_START
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-NSLogMx"];
    TEST_CHANNEL_ON(channel);
    [sLoggingService addOutputStream:sNSLogOut];
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestLoggingBlock(channel);
        });
    }

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    TEST_CHANNEL_OFF(channel);
    TEST_STOP

    [sLoggingService flush];
    [sLoggingService removeOutputStream:sNSLogOut];
}

- (void)testStdErr1
{
    TEST_START
    (void)TLSLogChannelDefault;
    NSString *channel = @"StdErr";

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestStreamBlock(sStdErrOut, channel);
    }

    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestStreamBlock(sStdErrOut, channel);
    }

    TEST_CHANNEL_OFF(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestStreamBlock(sStdErrOut, channel);
    }

    TEST_STOP

    [sStdErrOut tls_flush];
}

- (void)testStdErr2
{
    TEST_START
    NSString *channel = @"StdErrMx";

    TEST_CHANNEL_ON(channel);

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestStreamBlock(sStdErrOut, channel);
        });
    }

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    TEST_CHANNEL_OFF(channel);
    TEST_STOP
}

- (void)testNSLog1
{
    TEST_START
    (void)TLSLogChannelDefault;
    NSString *channel = @"NSLog";

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestStreamBlock(sNSLogOut, channel);
    }

    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestStreamBlock(sNSLogOut, channel);
    }

    TEST_CHANNEL_OFF(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestStreamBlock(sNSLogOut, channel);
    }

    TEST_STOP
}

- (void)testNSLog2
{
    TEST_START
    NSString *channel = @"NSLogMx";

    TEST_CHANNEL_ON(channel);

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestStreamBlock(sNSLogOut, channel);
        });
    }

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    TEST_CHANNEL_OFF(channel);
    TEST_STOP
}

- (void)testLoggingFile1
{
    TEST_START
    [sLoggingService addOutputStream:sFileOut];

    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-File"];

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_STOP
}

- (void)testLoggingFile2
{
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-File"];
    [sLoggingService removeOutputStream:sFileOut];
    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    [sLoggingService flush];
}

- (void)testLoggingFile3
{
    TEST_START
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-FileMx"];
    TEST_CHANNEL_ON(channel);
    [sLoggingService addOutputStream:sFileOut];
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestLoggingBlock(channel);
        });
    }

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    TEST_CHANNEL_OFF(channel);
    TEST_STOP

    [sLoggingService flush];
    [sLoggingService removeOutputStream:sFileOut];
}

- (void)testLoggingFile4
{
    // Test Logging File Creation
#if TARGET_OS_SIMULATOR
    NSString *restrictedDir = @"/bin";
#else
    NSString *restrictedDir = @"/";
#endif

    NSError *error = nil;
    TLSFileOutputStream *stream = [[TLSFileOutputStream alloc] initWithLogFileDirectoryPath:restrictedDir logFileName:@"restricted.log" error:&error];
    XCTAssertNil(stream, @"'%@' directory should always result in an empty stream representing the failure to create a %@ stream! Your machine might have write permissions on '%@', which it shouldn't.", restrictedDir, NSStringFromClass([stream class]), restrictedDir);
    XCTAssertNotNil(error, @"'%@' directory should always result in an error describing the failure to create a %@ stream! Your machine might have write permissions on '%@', which it shouldn't.", restrictedDir, NSStringFromClass([stream class]), restrictedDir);
    error = nil;

    stream = [[TLSFileOutputStream alloc] initWithLogFileName:TEST_FILE_NAME error:&error];
    XCTAssertNil(error, @"TLSFileOutputStream should have succeeded");
    XCTAssertNotEqual(NULL, stream.logFile, @"TLSLoggingFileOuptutStream.logFile should not be NULL");
    XCTAssert([[[TLSFileOutputStream defaultLogFileDirectoryPath] stringByAppendingPathComponent:TEST_FILE_NAME] isEqualToString:stream.logFilePath], @"TLSFileOutputStream.tls_loggedDataEncoding should have been NSUTF8StringEncoding");
    XCTAssertEqual(NSUTF8StringEncoding, stream.tls_loggedDataEncoding, @"TLSFileOutputStream.tls_loggedDataEncoding should have been NSUTF8StringEncoding");

    NSString *data = @"TLSFileOutputStream data";
    [stream outputLogData:[data dataUsingEncoding:stream.tls_loggedDataEncoding]];
    XCTAssertEqual(data.length+1, stream.bytesWritten, @"TLSFileOutputStream bytes written should equal %tu", data.length);
}

- (void)testLoggingFile5
{
    NSArray *levels = @[ TLSLogLevelToString(TLSLogLevelError), TLSLogLevelToString(TLSLogLevelWarning), TLSLogLevelToString(TLSLogLevelInformation), TLSLogLevelToString(TLSLogLevelDebug) ];

    XCTAssertFalse([sFileOut conformsToProtocol:@protocol(TLSDataRetrieval)], @"baseline FileOutputStream should not conform to TLSDataRetrieval");

    // Validate on disk logs
    @autoreleasepool {
        NSString *string = [[NSString alloc] initWithContentsOfFile:sFileOut.logFilePath encoding:sFileOut.tls_loggedDataEncoding error:NULL];
        XCTAssertNotNil(string, @"reading log file must result in non-nil string");
        NSArray *lines = [string componentsSeparatedByString:@"\n"];
        XCTAssertTrue(lines.count > 0, @"each log file must have at least 1 line");
        for (NSString *line in lines) {
            NSMutableArray *tokens = [[line componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"[]"]] mutableCopy];
            [tokens removeObjectsAtIndexes:[tokens indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
                return [(NSString *)obj length] == 0;
            }]];
            if (tokens.count >= 5) {
                NSTimeInterval ti = NSTimeIntervalFromTLSFileOutputStreamTimestamp(tokens[0]);
                if (ti > 0.0f) {
                    XCTAssertTrue([levels containsObject:tokens[3]], @"Log level must be Error, Warning, Information or Debug");
                    XCTAssertTrue([tokens[2] hasPrefix:TLSLogChannelDefault], @"Logging channel is wrong");
                }
            }
        }
    }
}

- (void)testLoggingFile6
{
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString *logFileDirectoryPath = [TLSFileOutputStream defaultLogFileDirectoryPath];
    NSArray *logFiles = [fm contentsOfDirectoryAtPath:logFileDirectoryPath error:NULL];
    NSString *logFileOfInterest = logFiles[[logFiles indexOfObject:TEST_FILE_NAME]];

    unsigned long long bytes = [fm attributesOfItemAtPath:[logFileDirectoryPath stringByAppendingPathComponent:logFileOfInterest] error:NULL].fileSize;
    unsigned long long expectedBytes = [NSString stringWithFormat:@"%@ data\n", NSStringFromClass([TLSFileOutputStream class])].length;
    XCTAssertEqual(bytes, expectedBytes, @"file size doesn't match expectations");
}

- (void)testLoggingFileNSLogCombo
{
    TEST_START
    [sLoggingService addOutputStream:sNSLogOut];
    [sLoggingService addOutputStream:sFileOut];

    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-ComboMx"];
    TEST_CHANNEL_ON(channel);

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestLoggingBlock(channel);
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    TEST_CHANNEL_OFF(channel);
    TEST_STOP

    [sLoggingService flush];
    for (id<TLSOutputStream> stream in sLoggingService.outputStreams) {
        [sLoggingService removeOutputStream:stream];
    }
}

- (void)testLoggingRollingFile1
{
    TEST_START
    [sLoggingService addOutputStream:sRollingFileOut];

    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-Roll"];

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_STOP
}

- (void)testLoggingRollingFile2
{
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-Roll"];
    [sLoggingService removeOutputStream:sRollingFileOut];
    TEST_CHANNEL_ON(channel);

    for (int i = 0; i < TEST_COUNT; i++) {
        sTestLoggingBlock(channel);
    }

    TEST_CHANNEL_OFF(channel);

    [sLoggingService flush];
}

- (void)testLoggingRollingFile3
{
    TEST_START
    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-RollMx"];
    TEST_CHANNEL_ON(channel);
    [sLoggingService addOutputStream:sRollingFileOut];
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestLoggingBlock(channel);
        });
    }

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    TEST_CHANNEL_OFF(channel);
    TEST_STOP

    [sLoggingService flush];
}

- (void)testLoggingRollingFile4
{
    // Test Logging File Creation
#if TARGET_OS_SIMULATOR
    NSString *restrictedDir = @"/bin";
#else
    NSString *restrictedDir = @"/";
#endif

    NSError *error = nil;
    TLSRollingFileOutputStream *stream = [[TLSRollingFileOutputStream alloc] initWithLogFileDirectoryPath:restrictedDir logFilePrefix:@"log." maxLogFiles:5 maxBytesPerLogFile:1024 error:&error];
    XCTAssertNil(stream, @"'%@' directory should always result in a failure to create a %@ stream! Your machine might have write permissions on '%@', which it shouldn't.", restrictedDir, NSStringFromClass([stream class]), restrictedDir);
    XCTAssertNotNil(error, @"'%@' directory should always result in a failure to create a %@ stream! Your machine might have write permissions on '%@', which it shouldn't.", restrictedDir, NSStringFromClass([stream class]), restrictedDir);
    error = nil;

    NSString *path = [[TLSRollingFileOutputStream defaultLogFileDirectoryPath] stringByAppendingPathComponent:@"TLSLogging"];
    NSString *prefix = @"twp.";
    NSUInteger maxBytesPerLogFile = 256 * 1024;
    NSUInteger maxLogFiles = 10;

    stream = [[TLSRollingFileOutputStream alloc] initWithLogFileDirectoryPath:path logFilePrefix:prefix maxLogFiles:maxLogFiles maxBytesPerLogFile:maxBytesPerLogFile error:&error];
    XCTAssertNil(error, @"TLSRollingFileOutputStream should have succeeded");
    XCTAssertEqual(maxBytesPerLogFile, stream.maxBytesPerLogFile, @"maxBytesPerLogFile differs!");
    XCTAssertEqual(maxLogFiles, stream.maxLogFiles, @"maxLogFiles differs!");
    XCTAssertEqualObjects(path, stream.logFileDirectoryPath, @"logFileDirectoryPath differs!");
    XCTAssertEqualObjects(prefix, stream.logFilePrefix, @"logFilePrefix differs!");

    NSUInteger maxBytesPerLogFileModified = (NSUInteger)(1024ULL * 1024ULL * 1024ULL); // capped value (1GB)
    maxBytesPerLogFile = (NSUInteger)((unsigned long long)maxBytesPerLogFileModified * 2ULL);
    maxLogFiles = 100;
    NSUInteger maxLogFilesModified = (NSUInteger)((4ULL * 1024ULL * 1024ULL * 1024ULL) / (unsigned long long)maxBytesPerLogFileModified); // capped value
    stream = [[TLSRollingFileOutputStream alloc] initWithLogFileDirectoryPath:path logFilePrefix:prefix maxLogFiles:maxLogFiles maxBytesPerLogFile:maxBytesPerLogFile error:&error];
    XCTAssertNil(error, @"TLSRollingFileOutputStream should have succeeded");
    XCTAssertNotEqual(maxBytesPerLogFile, stream.maxBytesPerLogFile, @"maxBytesPerLogFile should have been capped!");
    XCTAssertEqual(maxBytesPerLogFileModified, stream.maxBytesPerLogFile, @"maxBytesPerLogFile differs!");
    XCTAssertNotEqual(maxLogFiles, stream.maxLogFiles, @"maxLogFiles should have been capped!");
    XCTAssertEqual(maxLogFilesModified, stream.maxLogFiles, @"maxLogFiles differ!");

    maxBytesPerLogFile = 0; // 256MB
    maxBytesPerLogFileModified = 1024; // min value
    maxLogFiles = 0;
    maxLogFilesModified = 1; // min value
    stream = [[TLSRollingFileOutputStream alloc] initWithLogFileDirectoryPath:path logFilePrefix:prefix maxLogFiles:maxLogFiles maxBytesPerLogFile:maxBytesPerLogFile error:&error];
    XCTAssertNil(error, @"TLSRollingFileOutputStream should have succeeded");
    XCTAssertNotEqual(maxBytesPerLogFile, stream.maxBytesPerLogFile, @"maxBytesPerLogFile should have been capped!");
    XCTAssertEqual(maxBytesPerLogFileModified, stream.maxBytesPerLogFile, @"maxBytesPerLogFile differs!");
    XCTAssertNotEqual(maxLogFiles, stream.maxLogFiles, @"maxLogFiles should have been capped!");
    XCTAssertEqual(maxLogFilesModified, stream.maxLogFiles, @"maxLogFiles differ!");
}

- (void)testLoggingRollingFile5
{
    NSArray *levels = @[ TLSLogLevelToString(TLSLogLevelError), TLSLogLevelToString(TLSLogLevelWarning), TLSLogLevelToString(TLSLogLevelInformation), TLSLogLevelToString(TLSLogLevelDebug) ];

    @autoreleasepool {
        // Validate received data
        NSData *newlineData = [NSData dataWithBytes:"\n" length:1];
        NSData *data = [sLoggingService retrieveLoggedDataFromOutputStream:sRollingFileOut maxBytes:0];
        NSUInteger dataLength1 = data.length;
        XCTAssertLessThanOrEqual(data.length, (sRollingFileOut.maxBytesPerLogFile * 2), @"Logs were not rolled over at the right time!");
        NSRange r;
        r.length = [data rangeOfData:newlineData options:NSDataSearchBackwards range:NSMakeRange(0, dataLength1 / 2)].location;
        r.location = [data rangeOfData:newlineData options:NSDataSearchBackwards range:NSMakeRange(0, r.length - 2)].location + 1;
        r.length -= r.location;
        NSString *exampleNewer = [[NSString alloc] initWithData:[data subdataWithRange:r] encoding:sRollingFileOut.tls_loggedDataEncoding];
        data = [sLoggingService retrieveLoggedDataFromOutputStream:sRollingFileOut maxBytes:sRollingFileOut.maxBytesPerLogFile + (2 * dataLength1)];
        NSUInteger dataLength2 = data.length;
        XCTAssertLessThanOrEqual(data.length, (sRollingFileOut.maxBytesPerLogFile * 3), @"Logs were not rolled over at the right time!");
        r.location = 0;
        r.length = dataLength2 - dataLength1;
        data = [data subdataWithRange:r];
        XCTAssertEqual(data.length, dataLength2 - dataLength1, @"Didn't create subset of data correctly");
        XCTAssertLessThanOrEqual(data.length, (sRollingFileOut.maxBytesPerLogFile * 2), @"Logs were not rolled over at the right time!");
        r.length = [data rangeOfData:newlineData options:NSDataSearchBackwards range:NSMakeRange(0, dataLength1 / 2)].location;
        r.location = [data rangeOfData:newlineData options:NSDataSearchBackwards range:NSMakeRange(0, r.length - 2)].location + 1;
        r.length -= r.location;
        NSString *exampleOlder = [[NSString alloc] initWithData:[data subdataWithRange:r] encoding:sRollingFileOut.tls_loggedDataEncoding];
        XCTAssertNotEqualObjects(exampleNewer, exampleOlder, @"Log lines should differ between files");
        NSMutableArray *tokensOlder = [[exampleOlder componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"[]"]] mutableCopy];
        NSMutableArray *tokensNewer = [[exampleNewer componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"[]"]] mutableCopy];
        [tokensOlder removeObjectsAtIndexes:[tokensOlder indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
            return [(NSString *)obj length] == 0;
        }]];
        [tokensNewer removeObjectsAtIndexes:[tokensNewer indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
            return [(NSString *)obj length] == 0;
        }]];
        XCTAssertNotEqualObjects(tokensOlder, tokensNewer, @"Log lines should differ between files");
        XCTAssertGreaterThanOrEqual(tokensOlder.count, 5, @"Must have at minimum: timestamp, threadId, channel, level and message in log entries");
        XCTAssertGreaterThanOrEqual(tokensNewer.count, 5, @"Must have at minimum: timestamp, threadId, channel, level and message in log entries");
        XCTAssertTrue([levels containsObject:tokensNewer[3]], @"Log level must be Error, Warning, Information or Debug");
        XCTAssertTrue([levels containsObject:tokensOlder[3]], @"Log level must be Error, Warning, Information or Debug");
        XCTAssertTrue([tokensNewer[2] hasPrefix:TLSLogChannelDefault], @"Logging channel is wrong");
        XCTAssertTrue([tokensOlder[2] hasPrefix:TLSLogChannelDefault], @"Logging channel is wrong");
        NSTimeInterval tiOlder = NSTimeIntervalFromTLSFileOutputStreamTimestamp(tokensOlder[0]);
        NSTimeInterval tiNewer = NSTimeIntervalFromTLSFileOutputStreamTimestamp(tokensNewer[0]);
        XCTAssertGreaterThan(tiOlder, 0.0, @"Invalid timestamp");
        XCTAssertGreaterThan(tiNewer, 0.0, @"Invalid timestamp");
        if ([tokensNewer[1] isEqualToString:tokensOlder[1]]) {
            XCTAssertGreaterThanOrEqual(tiNewer, tiOlder, @"Newer timestamp must be greater than older timestamp");
        } else if (tiOlder > tiNewer) {
            XCTAssertGreaterThanOrEqual(tiNewer, tiOlder + 0.1, @"Newer timestamp must be greater than older timestamp: %@ then %@", tokensOlder[0], tokensNewer[0]);
        }
    }

    // Validate on disk logs
    NSMutableArray *logs = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:sRollingFileOut.logFileDirectoryPath error:NULL] mutableCopy];
    [logs removeObjectsAtIndexes:[logs indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
        return ![[(NSString *)obj lastPathComponent] hasPrefix:sRollingFileOut.logFilePrefix];
    }]];
    XCTAssertEqual(logs.count, sRollingFileOut.maxLogFiles, @"Logs must have rolled over and been pruned!");
    [logs sortWithOptions:NSSortStable usingComparator:^NSComparisonResult(id obj1, id obj2) {
        NSString *path1 = obj1;
        NSString *path2 = obj2;
        return [path1 compare:path2];
    }];


    unsigned long inversionCount = 0;
    NSTimeInterval lastTimestamp = 0;
    __strong NSString *lastLog = nil;
    NSMutableDictionary<NSString *, NSMutableArray<NSNumber *> *> *perThreadTimestamps = [[NSMutableDictionary alloc] init];
    for (__strong NSString *path in logs) {
        @autoreleasepool {
            path = [sRollingFileOut.logFileDirectoryPath stringByAppendingPathComponent:path];
            NSString *string = [[NSString alloc] initWithContentsOfFile:path encoding:sRollingFileOut.tls_loggedDataEncoding error:NULL];
            XCTAssertNotNil(string, @"reading log file must result in nil string");
            NSArray *lines = [string componentsSeparatedByString:@"\n"];
            string = nil;
            XCTAssertGreaterThan(lines.count, 0, @"each log file must have at least 1 line");
            for (NSString *line in lines) {
                NSMutableArray *tokens = [[line componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"[]"]] mutableCopy];
                [tokens removeObjectsAtIndexes:[tokens indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
                    return [(NSString *)obj length] == 0;
                }]];
                if (tokens.count >= 5) {
                    NSTimeInterval ti = NSTimeIntervalFromTLSFileOutputStreamTimestamp(tokens[0]);
                    NSString *thread = tokens[1];
                    if (ti > 0.0) {
                        XCTAssertTrue([levels containsObject:tokens[3]], @"Log level must be Error, Warning, Information or Debug");
                        XCTAssertTrue([tokens[2] hasPrefix:TLSLogChannelDefault], @"Logging channel is wrong");
                        if (ti < lastTimestamp) {
                            inversionCount++;
                            // NSLog(@"Timestamps out of order %lu times", inversionCount);
                        }
                        lastTimestamp = ti;
                        lastLog = line;
                    }

                    NSMutableArray<NSNumber *> *timestamps = perThreadTimestamps[thread];
                    if (!timestamps) {
                        timestamps = [[NSMutableArray alloc] init];
                        perThreadTimestamps[thread] = timestamps;
                    }
                    [timestamps addObject:@(ti)];
                }
            }
        }
    }

    NSLog(@"Timestamps out of order %lu times", inversionCount);

    // Even though timestamps could be out of order in the log, that could be due to multi-threading.
    // Test to ensure that, per thread, the timestamps _ARE_ in order.
    [perThreadTimestamps enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull thread, NSMutableArray<NSNumber *> * _Nonnull timestamps, BOOL * _Nonnull stop) {
        NSTimeInterval previousTimestamp = 0;
        for (NSNumber *timestampNum in timestamps) {
            const NSTimeInterval ti = [timestampNum doubleValue];
            XCTAssertGreaterThanOrEqual(ti, previousTimestamp, @"Incorrect timestamp ordering for thread `%@`!", thread);
            previousTimestamp = ti;
        }
    }];
}

- (void)testLoggingRollingFile6
{
    unsigned long long bytes = 0;
    NSFileManager *fm = [NSFileManager defaultManager];
    NSArray *logFiles = [fm contentsOfDirectoryAtPath:sRollingFileOut.logFileDirectoryPath error:NULL];
    XCTAssert(logFiles, @"directory containing log files should exist");
    for (NSString *item in logFiles) {
        if ([item hasPrefix:sRollingFileOut.logFilePrefix]) {
            bytes += [fm attributesOfItemAtPath:[sRollingFileOut.logFileDirectoryPath stringByAppendingPathComponent:item] error:NULL].fileSize;
        }
    }
    NSData *logs = [sLoggingService retrieveLoggedDataFromOutputStream:sRollingFileOut maxBytes:NSUIntegerMax];
    XCTAssertTrue(bytes <= logs.length + 128 && bytes >= logs.length - 128, @"retrieved logged data doesn't match expectations");
    logs = nil;
    logs = [sLoggingService retrieveLoggedDataFromOutputStream:sRollingFileOut maxBytes:0];
    XCTAssertTrue(logs.length > 0, @"the minimum bytes you can cap is the maximum bytes per files, not 0");
}

- (void)testLoggingRollingNSLogCombo
{
    TEST_START
    [sLoggingService addOutputStream:sNSLogOut];
    [sLoggingService addOutputStream:sRollingFileOut];

    NSString *channel = [TLSLogChannelDefault stringByAppendingString:@"-ComboMx"];
    TEST_CHANNEL_ON(channel);

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t q = dispatch_queue_create("mxQ", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < TEST_MX_COUNT; i++) {
        dispatch_group_async(group, q, ^() {
            sTestLoggingBlock(channel);
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    TEST_CHANNEL_OFF(channel);
    TEST_STOP

    [sLoggingService flush];
    for (id<TLSOutputStream> stream in sLoggingService.outputStreams) {
        [sLoggingService removeOutputStream:stream];
    }
}

- (void)testZZLoggingSpeedVerification
{
    XCTAssertTrue(sFileOut != nil, @"TLSFileOutputStream must have successfully been created!");
    XCTAssertTrue(sRollingFileOut != nil, @"TLSRollingFileOutputStream must have successfully been created!");
    NSString *loggingPrefix = @"testLogging";
    NSString *nonLoggingPrefix = @"test";
    for (NSString *key in sRuntimes.allKeys) {
        if ([key hasPrefix:loggingPrefix]) {
            NSString *otherKey = [key stringByReplacingOccurrencesOfString:loggingPrefix withString:nonLoggingPrefix];
            if (sRuntimes[otherKey]) {
                double loggingTime = [sRuntimes[key] doubleValue];
                double nonLoggingTime = [sRuntimes[otherKey] doubleValue];

                double improvement = ((loggingTime < nonLoggingTime) ? ((nonLoggingTime - loggingTime) / loggingTime) : (((loggingTime - nonLoggingTime) / nonLoggingTime) * -1.0f)) * 100;
                NSLog(@"%f%% speed boost going from %@ (%f s) to %@ (%f s)", improvement, otherKey, nonLoggingTime, key, loggingTime);

// TODO: re-enable the following line when a the infrastructure has been added to disable occasionally flaky tests in CI
//                XCTAssertTrue(loggingTime < (nonLoggingTime * 2.0), @"%@ must perform as well as %@", key, otherKey);
            }
        }
    }
    XCTAssertTrue([[NSFileManager defaultManager] contentsOfDirectoryAtPath:[TLSFileOutputStream defaultLogFileDirectoryPath] error:NULL].count > 0, @"Must have logged something to log file directory");
    NSLog(@"Logs files: %@", [TLSFileOutputStream defaultLogFileDirectoryPath]);

    // Cleanup
    [[NSFileManager defaultManager] removeItemAtPath:[TLSFileOutputStream defaultLogFileDirectoryPath] error:NULL];
}

- (void)testZZZCustomLogger
{
#if DEBUG
    const NSUInteger kNumberOfLevelsSupported = 4;
#else
    const NSUInteger kNumberOfLevelsSupported = 3;
#endif

#define TEST_CUSTOM \
    do { \
        XCTAssertTrue(testLogger.loggedMessages == expectedLoggedMessages, @"Should have logged %tu messages not %tu", expectedLoggedMessages, testLogger.loggedMessages); \
        XCTAssertTrue(dispatchedLogMessages == expectedDispatchedMessages, @"Should have dispatched %tu messages not %tu", expectedDispatchedMessages, dispatchedLogMessages); \
        expectedLoggedMessages = testLogger.loggedMessages; \
        expectedDispatchedMessages = dispatchedLogMessages; \
    } while (0)

    __block NSUInteger dispatchedLogMessages = 0;
    TestLoggingBlock block = ^(NSString *theChannel) {
        if (TLSCanLog(nil, TLSLogLevelDebug, theChannel, NULL)) {
            dispatchedLogMessages++;
            [sLoggingService logWithLevel:TLSLogLevelDebug channel:theChannel file:@(__FILE__) function:@(__PRETTY_FUNCTION__) line:__LINE__ contextObject:NULL options:0 message:@"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil];
            [sLoggingService flush];
        }
        if (TLSCanLog(nil, TLSLogLevelInformation, theChannel, NULL)) {
            dispatchedLogMessages++;
            [sLoggingService logWithLevel:TLSLogLevelInformation channel:theChannel file:@(__FILE__) function:@(__PRETTY_FUNCTION__) line:__LINE__ contextObject:NULL options:0 message:@"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil];
            [sLoggingService flush];
        }
        if (TLSCanLog(nil, TLSLogLevelWarning, theChannel, NULL)) {
            dispatchedLogMessages++;
            [sLoggingService logWithLevel:TLSLogLevelWarning channel:theChannel file:@(__FILE__) function:@(__PRETTY_FUNCTION__) line:__LINE__ contextObject:NULL options:0 message:@"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil];
            [sLoggingService flush];
        }
        if (TLSCanLog(nil, TLSLogLevelError, theChannel, NULL)) {
            dispatchedLogMessages++;
            [sLoggingService logWithLevel:TLSLogLevelError channel:theChannel file:@(__FILE__) function:@(__PRETTY_FUNCTION__) line:__LINE__ contextObject:NULL options:0 message:@"%d %@ %f %@ %@", 1, @2, 3.0f, GenerateLoggingArgument(), nil];
            [sLoggingService flush];
        }
    };
#define TEST_FILTER(channel) block(channel)

    NSUInteger expectedLoggedMessages = 0;
    NSUInteger expectedDispatchedMessages = 0;
    NSString *defaultChannel = [TLSLogChannelDefault stringByAppendingString:@"-Custom"];
    TestLogger *testLogger = [[TestLogger alloc] init];

    [sLoggingService addOutputStream:testLogger];
    TEST_FLUSH_TRANSACTIONS();
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 1; // first log message would have been dispatched to cache the channel as off
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0; // channel is cached as off
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    TEST_CHANNEL_ON(defaultChannel);
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [sLoggingService updateOutputStream:testLogger];
    TEST_FLUSH_TRANSACTIONS();
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += kNumberOfLevelsSupported;
    TEST_CUSTOM;
    [sLoggingService removeOutputStream:testLogger];
    TEST_FLUSH_TRANSACTIONS();
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [sLoggingService addOutputStream:[[TLSNSLogOutputStream alloc] init]];
    TEST_FLUSH_TRANSACTIONS();
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [sLoggingService removeOutputStream:sLoggingService.outputStreams.anyObject];
    TEST_FLUSH_TRANSACTIONS();
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [sLoggingService addOutputStream:testLogger];
    TEST_FLUSH_TRANSACTIONS();
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += kNumberOfLevelsSupported;
    TEST_CUSTOM;

    // updated log level changes
    testLogger.permittedLoggingLevels = TLSLogLevelMaskInformation;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += 1;
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 1;
    expectedLoggedMessages += 1;
    TEST_CUSTOM;
    [sLoggingService updateOutputStream:testLogger];
    TEST_FLUSH_TRANSACTIONS();
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += 1;
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 1;
    expectedLoggedMessages += 1;
    TEST_CUSTOM;
    testLogger.permittedLoggingLevels = TLSLogLevelMaskAll;
    [sLoggingService updateOutputStream:testLogger];
    TEST_FLUSH_TRANSACTIONS();
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += kNumberOfLevelsSupported;
    TEST_CUSTOM;

    // update channel filtering
    TEST_CHANNEL_OFF(defaultChannel); // resets cache
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 1; // first log message would have been dispatched to cache the channel as off
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0; // cached as off
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    testLogger.shouldFilterChannelsThatAreOff = NO; // cache not updated
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [sLoggingService updateOutputStream:testLogger]; // cache reset
    TEST_FLUSH_TRANSACTIONS();
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += kNumberOfLevelsSupported;
    TEST_CUSTOM;
    TEST_CHANNEL_ON(defaultChannel);
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += kNumberOfLevelsSupported;
    TEST_CUSTOM;

    // update stream specific channel filter
    [testLogger setChannel:defaultChannel on:NO]; // doesn't update cache
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 1; // first log message will cache
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [testLogger setChannel:defaultChannel on:YES]; // doesn't update cache
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += 0;
    expectedLoggedMessages += 0;
    TEST_CUSTOM;
    [sLoggingService updateOutputStream:testLogger]; // resets cache
    TEST_FLUSH_TRANSACTIONS();
    TEST_FILTER(defaultChannel);
    expectedDispatchedMessages += kNumberOfLevelsSupported;
    expectedLoggedMessages += kNumberOfLevelsSupported;
    TEST_CUSTOM;

    [sLoggingService removeOutputStream:testLogger];
}

@end

@implementation TestLogger
{
    NSMutableSet *_channelsToFilter;
}

- (void)setPermittedLoggingLevels:(TLSLogLevelMask)permittedLoggingLevels
{
    if (_permittedLoggingLevels != permittedLoggingLevels) {
        _permittedLoggingLevels = permittedLoggingLevels;
    }
}

- (void)setShouldFilterChannelsThatAreOff:(BOOL)filterChannels
{
    if (_shouldFilterChannelsThatAreOff != filterChannels) {
        _shouldFilterChannelsThatAreOff = filterChannels;
    }
}

- (TLSFilterStatus)tls_shouldFilterLevel:(TLSLogLevel)level channel:(NSString *)channel contextObject:(id)contextObject
{
    if (0 == (_permittedLoggingLevels & (1 << level))) {
        return TLSFilterStatusCannotLogLevel;
    }

    if (_shouldFilterChannelsThatAreOff && !IsConsoleChannelOn(channel)) {
        return TLSFilterStatusCannotLogChannel;
    }

    if ([_channelsToFilter containsObject:channel]) {
        return TLSFilterStatusCannotLogChannel;
    }

    return TLSFilterStatusOK;
}

- (void)tls_outputLogInfo:(TLSLogMessageInfo *)logInfo
{
    _loggedMessages++;
}

- (void)setChannel:(NSString *)channel on:(BOOL)on
{
    if (on) {
        [_channelsToFilter removeObject:channel];
    } else {
        [_channelsToFilter addObject:channel];
    }
}

- (instancetype)init
{
    if (self = [super init]) {
        _shouldFilterChannelsThatAreOff = YES;
        _permittedLoggingLevels = TLSLogLevelMaskAll;
        _channelsToFilter = [NSMutableSet set];
    }
    return self;
}

@end

@implementation TestStdErrLogger

- (TLSFilterStatus)tls_shouldFilterLevel:(TLSLogLevel)level channel:(NSString *)channel contextObject:(id)contextObject
{
    if (!IsConsoleChannelOn(channel)) {
        return TLSFilterStatusCannotLogChannel;
    }
    return TLSFilterStatusOK;
}

@end

@implementation TestNSLogLogger

- (TLSFilterStatus)tls_shouldFilterLevel:(TLSLogLevel)level channel:(NSString *)channel contextObject:(id)contextObject
{
    if (!IsConsoleChannelOn(channel)) {
        return TLSFilterStatusCannotLogChannel;
    }
    return TLSFilterStatusOK;
}

@end

@implementation TestFileLogger
@end

@implementation TestRollingFileLogger
@end

static void LogStream(id<TLSOutputStream> stream, TLSLogLevel level, NSString *channel, NSString *file, NSString *function, unsigned int line, NSString *format, ...)
{
    NSDate *timestamp = [NSDate date];
    va_list list;
    va_start(list, format);
    TLSLogMessageInfo *info = [[TLSLogMessageInfo alloc] initWithLevel:level
                                                                  file:file
                                                              function:function
                                                                  line:line
                                                               channel:channel
                                                             timestamp:timestamp
                                                           logLifespan:[timestamp timeIntervalSinceDate:[[TLSLoggingService sharedInstance] startupTimestamp]]
                                                              threadId:pthread_mach_thread_np(pthread_self())
                                                            threadName:TLSCurrentThreadName()
                                                         contextObject:nil
                                                               message:[[NSString alloc] initWithFormat:format arguments:list]];
    va_end(list);
    __block BOOL isChannelOn = NO;
    [[TLSLoggingService sharedInstance] dispatchSynchronousTransaction:^{
        isChannelOn = IsConsoleChannelOn(channel);
    }];
    if (isChannelOn) {
        [stream tls_outputLogInfo:info];
    }
}

static NSTimeInterval NSTimeIntervalFromTLSFileOutputStreamTimestamp(NSString *timestamp)
{
    NSMutableArray<NSString *> *timestampElements = [[timestamp componentsSeparatedByString:@":"] mutableCopy];
    if (!timestampElements) {
        timestampElements = [NSMutableArray array];
    }
    if ([timestampElements.lastObject rangeOfString:@"."].location != NSNotFound) {
        NSArray<NSString *> *dotSeperatedEnd = [timestampElements.lastObject componentsSeparatedByString:@"."];
        [timestampElements removeLastObject];
        [timestampElements addObjectsFromArray:dotSeperatedEnd];
    }
    while (timestampElements.count < 4) {
        [timestampElements insertObject:@"0" atIndex:0];
    }
    NSTimeInterval ti = 0;
    ti += [timestampElements[0] integerValue] * 60 * 60;
    ti += [timestampElements[1] integerValue] * 60;
    ti += [timestampElements[2] integerValue];
    ti += ((double)[timestampElements[3] integerValue]) / 1000.0f;
    return ti;
}

static id GenerateLoggingArgument()
{
    // [NSThread sleepForTimeInterval:0.004]; // <- uncomment this to enable slow arguments in the test
    return [NSDate date];
}

static void TurnConsoleChannelOn(NSString *channel, BOOL on)
{
    if (on) {
        [sOnConsoleChannels addObject:channel];
    } else {
        [sOnConsoleChannels removeObject:channel];
    }
}

static BOOL IsConsoleChannelOn(NSString *channel)
{
    return [sOnConsoleChannels containsObject:channel];
}
