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

#import <Foundation/Foundation.h>

#import <dlfcn.h>
#import <objc/message.h>
#import <objc/runtime.h>

#import "FBXCTestConstants.h"
#import "XCTestPrivate.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"

static NSString *const XCTestFilterArg = @"XCTest";
static NSString *const XCTestFrameworkName = @"XCTest";
static NSString *const XCTestProbeClassName = @"XCTestProbe";
static NSString *const XCTestSuiteClassName = @"XCTestSuite";

static FILE *__stdout;
static FILE *__stderr;

static NSMutableArray<NSDictionary<NSString *, id> *> *__testExceptions = nil;
static int __testSuiteDepth = 0;

static void parseXCTestCase(XCTestCase *testCase, NSString **classNameOut, NSString **methodNameOut, NSString **testKeyOut)
{
  NSString *className = NSStringFromClass(testCase.class);
  NSString *methodName;
  if ([testCase respondsToSelector:@selector(languageAgnosticTestMethodName)]) {
    methodName = [testCase languageAgnosticTestMethodName];
  } else {
    methodName = NSStringFromSelector([testCase.invocation selector]);
  }
  NSString *testKey = [NSString stringWithFormat:@"-[%@ %@]", className, methodName];
  if (classNameOut) {
    *classNameOut = className;
  }
  if (methodNameOut) {
    *methodNameOut = methodName;
  }
  if (testKeyOut) {
    *testKeyOut = testKey;
  }
}

static NSString *parseXCTestSuiteKey(XCTestSuite *suite)
{
  NSString *testKey = nil;
  for (id test in suite.tests) {
    if (![test isKindOfClass:NSClassFromString(@"XCTestCase")]) {
      return [suite name];
    }
    XCTestCase *testCase = test;
    NSString *innerTestKey = nil;
    parseXCTestCase(testCase, &innerTestKey, nil, nil);
    if (!testKey) {
      testKey = innerTestKey;
      continue;
    }
    if (![innerTestKey isEqualToString:testKey]) {
      return [suite name];
    }
  }
  return testKey ?: [suite name];
}

NSDictionary<NSString *, id> *EventDictionaryWithNameAndContent(NSString *name, NSDictionary *content)
{
  NSMutableDictionary<NSString *, id> *eventJSON = [NSMutableDictionary dictionaryWithDictionary:@{
    kReporter_Event_Key: name,
    kReporter_TimestampKey: @([[NSDate date] timeIntervalSince1970])
  }];
  [eventJSON addEntriesFromDictionary:content];
  return eventJSON;
}

void XTSwizzleClassSelectorForFunction(Class cls, SEL sel, IMP newImp) __attribute__((no_sanitize("nullability-arg")))
{
  Class clscls = object_getClass((id)cls);
  Method originalMethod = class_getClassMethod(cls, sel);

  NSString *selectorName = [[NSString alloc] initWithFormat:
                            @"__%s_%s",
                            class_getName(cls),
                            sel_getName(sel)];
  SEL newSelector = sel_registerName([selectorName UTF8String]);

  class_addMethod(clscls, newSelector, newImp, method_getTypeEncoding(originalMethod));
  Method replacedMethod = class_getClassMethod(cls, newSelector);
  method_exchangeImplementations(originalMethod, replacedMethod);
}

void XTSwizzleSelectorForFunction(Class cls, SEL sel, IMP newImp)
{
  Method originalMethod = class_getInstanceMethod(cls, sel);
  const char *typeEncoding = method_getTypeEncoding(originalMethod);

  NSString *selectorName = [[NSString alloc] initWithFormat:
                            @"__%s_%s",
                            class_getName(cls),
                            sel_getName(sel)];
  SEL newSelector = sel_registerName([selectorName UTF8String]);

  class_addMethod(cls, newSelector, newImp, typeEncoding);

  Method newMethod = class_getInstanceMethod(cls, newSelector);
  // @lint-ignore FBOBJCDISCOURAGEDFUNCTION
  if (class_addMethod(cls, sel, newImp, typeEncoding)) {
    class_replaceMethod(cls, newSelector, method_getImplementation(originalMethod), typeEncoding);
  } else {
    method_exchangeImplementations(originalMethod, newMethod);
  }
}

NSArray<XCTestCase *> *TestsFromSuite(id testSuite)
{
  NSMutableArray<XCTestCase *> *tests = [NSMutableArray array];
  NSMutableArray<id> *queue = [NSMutableArray array];
  [queue addObject:testSuite];

  while ([queue count] > 0) {
    id test = [queue objectAtIndex:0];
    [queue removeObjectAtIndex:0];

    if ([test isKindOfClass:[testSuite class]] ||
        [test respondsToSelector:@selector(tests)]) {
      // XCTestSuite keep a list of tests in an ivar called 'tests'.
      id testsInSuite = [test valueForKey:@"tests"];
      NSCAssert(testsInSuite != nil, @"Can't get tests for suite: %@", testSuite);
      [queue addObjectsFromArray:testsInSuite];
    } else {
      [tests addObject:test];
    }
  }
  return tests;
}

// Key used by objc_setAssociatedObject
static int TestDescriptionKey;

static NSString *TestCase_nameOrDescription(id self, SEL cmd)
{
  id description = objc_getAssociatedObject(self, &TestDescriptionKey);
  NSCAssert(description != nil, @"Value for `TestNameKey` wasn't set.");
  return description;
}

static char *const kEventQueueLabel = "xctool.events";

@interface XCToolAssertionHandler : NSAssertionHandler
@end

@implementation XCToolAssertionHandler

- (void)handleFailureInFunction:(NSString *)functionName
                           file:(NSString *)fileName
                     lineNumber:(NSInteger)line
                    description:(NSString *)format, ...
{
  // Format message
  va_list vl;
  va_start(vl, format);
  NSString *msg = [[NSString alloc] initWithFormat:format arguments:vl];
  va_end(vl);

  // Raise exception
  [NSException raise:NSInternalInconsistencyException format:@"*** Assertion failure in %@, %@:%lld: %@", functionName, fileName, (long long)line, msg];
}

@end

static dispatch_queue_t EventQueue()
{
  static dispatch_queue_t eventQueue = {0};
  static dispatch_once_t onceToken;

  dispatch_once(&onceToken, ^{
    // We'll serialize all events through this queue.
    eventQueue = dispatch_queue_create(kEventQueueLabel, DISPATCH_QUEUE_SERIAL);
  });

  return eventQueue;
}

static void PrintJSON(id JSONObject)
{
  NSError *error = nil;
  NSData *data = [NSJSONSerialization dataWithJSONObject:JSONObject options:0 error:&error];

  if (error) {
    fprintf(__stderr,
            "ERROR: Error generating JSON for object: %s: %s\n",
            [[JSONObject description] UTF8String],
            [[error localizedFailureReason] UTF8String]);
    exit(1);
  }

  fwrite([data bytes], 1, [data length], __stdout);
  fputs("\n", __stdout);
  fflush(__stdout);
}

#pragma mark - testSuiteDidStart

static void XCToolLog_testSuiteDidStart(NSString *name)
{
  if (__testSuiteDepth > 0) {
    dispatch_sync(EventQueue(), ^{
      PrintJSON(EventDictionaryWithNameAndContent(
        kReporter_Events_BeginTestSuite,
        @{kReporter_BeginTestSuite_SuiteKey : name}
      ));
    });
  }
  __testSuiteDepth++;
}

static void XCTestLog_testSuiteDidStart(id self, SEL sel, XCTestSuiteRun *run)
{
  XCToolLog_testSuiteDidStart(kReporter_TestSuite_TopLevelSuiteName);
}

static void XCTestLog_testSuiteWillStart(id self, SEL sel, XCTestSuite *suite)
{
  XCToolLog_testSuiteDidStart(parseXCTestSuiteKey(suite));
}

#pragma mark - testSuiteDidStop

static void XCToolLog_testSuiteDidStop(NSString *testSuiteName, XCTestSuiteRun *run)
{
  __testSuiteDepth--;

  if (__testSuiteDepth > 0) {
    NSDictionary<NSString *, id> *content =
      @{
        kReporter_EndTestSuite_SuiteKey : testSuiteName,
        kReporter_EndTestSuite_TestCaseCountKey : @([run testCaseCount]),
        kReporter_EndTestSuite_TotalFailureCountKey : @([run totalFailureCount]),
        kReporter_EndTestSuite_UnexpectedExceptionCountKey : @([run unexpectedExceptionCount]),
        kReporter_EndTestSuite_TestDurationKey: @([run testDuration]),
        kReporter_EndTestSuite_TotalDurationKey : @([run totalDuration]),
      };
    NSDictionary<NSString *, id> *json = EventDictionaryWithNameAndContent(kReporter_Events_EndTestSuite, content);
    dispatch_sync(EventQueue(), ^{
      PrintJSON(json);
    });
  }
}

static void XCTestLog_testSuiteDidStop(id self, SEL sel, XCTestSuiteRun *run)
{
  XCToolLog_testSuiteDidStop(kReporter_TestSuite_TopLevelSuiteName, run);
}

static void XCTestLog_testSuiteDidFinish(id self, SEL sel, XCTestSuite *suite)
{
  XCToolLog_testSuiteDidStop(parseXCTestSuiteKey(suite), (id)suite.testRun);
}

#pragma mark - testCaseDidStart

static void XCToolLog_testCaseDidStart(XCTestCase *testCase)
{
  dispatch_sync(EventQueue(), ^{
    NSString *testKey;
    NSString *className;
    NSString *methodName;
    parseXCTestCase(testCase, &className, &methodName, &testKey);

    PrintJSON(EventDictionaryWithNameAndContent(
      kReporter_Events_BeginTest, @{
        kReporter_BeginTest_TestKey : testKey,
        kReporter_BeginTest_ClassNameKey : className,
        kReporter_BeginTest_MethodNameKey : methodName,
      }
    ));

    __testExceptions = [[NSMutableArray alloc] init];
  });
}

static void XCTestLog_testCaseDidStart(id self, SEL sel, XCTestCaseRun *run)
{
  XCToolLog_testCaseDidStart([run test]);
}

static void XCTestLog_testCaseWillStart(id self, SEL sel, XCTestCase *testCase)
{
  id (*msgsend)(id, SEL) = (void *) objc_msgSend;
  XCTestLog_testCaseDidStart(self, sel, msgsend(testCase, @selector(testRun)));
}

#pragma mark - testCaseDidStop

static void XCToolLog_testCaseDidStop(XCTestCase *testCase, NSNumber *unexpectedExceptionCount, NSNumber *failureCount, NSNumber *totalDuration)
{
  dispatch_sync(EventQueue(), ^{
    NSString *className = nil;
    NSString *methodName = nil;
    NSString *testKey = nil;
    parseXCTestCase(testCase, &className, &methodName, &testKey);

    BOOL errored = [unexpectedExceptionCount integerValue] > 0;
    BOOL failed = [failureCount integerValue] > 0;
    BOOL succeeded = NO;
    NSString *result;
    if (errored) {
      result = kReporter_EndTest_ResultValueError;
    } else if (failed) {
      result = kReporter_EndTest_ResultValueFailure;
    } else {
      result = kReporter_EndTest_ResultValueSuccess;
      succeeded = YES;
    }

    // report test results
    NSArray<NSDictionary<NSString *, id> *> *retExceptions = [__testExceptions copy];
    NSDictionary<NSString *, id> *json = EventDictionaryWithNameAndContent(
      kReporter_Events_EndTest, @{
        kReporter_EndTest_TestKey : testKey,
        kReporter_EndTest_ClassNameKey : className,
        kReporter_EndTest_MethodNameKey : methodName,
        kReporter_EndTest_SucceededKey: @(succeeded),
        kReporter_EndTest_ResultKey : result,
        kReporter_EndTest_TotalDurationKey : totalDuration,
        kReporter_EndTest_ExceptionsKey : retExceptions,
    });

    PrintJSON(json);
  });
}

static void XCTestLog_testCaseDidStop(id self, SEL sel, XCTestCaseRun *run)
{
  XCToolLog_testCaseDidStop([run test], @([run unexpectedExceptionCount]), @([run failureCount]), @([run totalDuration]));
}

static void XCTestLog_testCaseDidFinish(id self, SEL sel, XCTestCase *testCase)
{
  id (*msgsend)(id, SEL) = (void *) objc_msgSend;
  XCTestLog_testCaseDidStop(self, sel, msgsend(testCase, @selector(testRun)));
}

#pragma mark - testCaseDidFail

static void XCToolLog_testCaseDidFail(NSDictionary *exceptionInfo)
{
  dispatch_sync(EventQueue(), ^{
    [__testExceptions addObject:exceptionInfo];
  });
}

static void XCTestLog_testCaseDidFail(id self, SEL sel, XCTestCaseRun *run, NSString *description, NSString *file, NSUInteger line)
{
  XCToolLog_testCaseDidFail(@{
    kReporter_EndTest_Exception_FilePathInProjectKey : file ?: @"Unknown File",
    kReporter_EndTest_Exception_LineNumberKey : @(line),
    kReporter_EndTest_Exception_ReasonKey : description,
  });
}

static void XCTestLog_testCaseDidFailWithDescription(id self, SEL sel, XCTestCase *testCase, NSString *description, NSString *file, NSUInteger line)
{
  id (*msgsend)(id, SEL) = (void *) objc_msgSend;
  XCTestLog_testCaseDidFail(self, sel, msgsend(testCase, @selector(testRun)), description, file, line);
}

#pragma mark - performTest

static void XCPerformTestWithSuppressedExpectedAssertionFailures(id self, SEL origSel, id arg1)
{
  void (*msgsend)(id, SEL, id) = (void *) objc_msgSend;
  int timeout = [@(getenv("TEST_SHIM_TEST_TIMEOUT") ?: "0") intValue];

  NSAssertionHandler *handler = [[XCToolAssertionHandler alloc] init];
  NSThread *currentThread = [NSThread currentThread];
  NSMutableDictionary *currentThreadDict = [currentThread threadDictionary];
  [currentThreadDict setObject:handler forKey:NSAssertionHandlerKey];

  if (timeout > 0) {
    BOOL isSuite = [self isKindOfClass:objc_getClass("XCTestCaseSuite")];
    // If running in a suite, time out if we run longer than the combined timeouts of all tests + a fudge factor.
    int64_t testCount = isSuite ? [[self tests] count] : 1;
    // When in a suite, add a second per test to help account for the time required to switch tests in a suite.
    int64_t fudgeFactor = isSuite ? MAX(testCount, 1) : 0;
    int64_t interval = (timeout * testCount + fudgeFactor) * NSEC_PER_SEC ;
    NSString *queueName = [NSString stringWithFormat:@"test.timer.%p", self];
    dispatch_queue_t queue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
    dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, interval), 0, 0);
    dispatch_source_set_event_handler(source, ^{
        if (isSuite) {
            NSString *additionalInformation = @"";
            if ([self respondsToSelector:@selector(testRun)]) {
                XCTestRun *run = [self testRun];
                NSUInteger executedTests = [run executionCount];
                if (executedTests == 0) {
                    additionalInformation = [NSString stringWithFormat:@"(No tests ran, likely stalled in +[%@ setUp])", [self name]];
                } else if (executedTests == testCount) {
                    additionalInformation = [NSString stringWithFormat:@"(All tests ran, likely stalled in +[%@ tearDown])", [self name]];
                }
            }

            [NSException raise:NSInternalInconsistencyException
                        format:@"*** Suite %@ ran longer than combined test time limit: %lld second(s) %@", [self name], testCount * timeout, additionalInformation];

        } else {
            [NSException raise:NSInternalInconsistencyException
                        format:@"*** Test %@ ran longer than specified test time limit: %d second(s)", self, timeout];
        }
    });
    dispatch_resume(source);

    // Call through original implementation
    msgsend(self, origSel, arg1);

    dispatch_source_cancel(source);
  } else {
    // Call through original implementation
    msgsend(self, origSel, arg1);
  }

  // The assertion handler hasn't been touched for our test, so we can safely remove it.
  [currentThreadDict removeObjectForKey:NSAssertionHandlerKey];
}

static void XCWaitForDebuggerIfNeeded()
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    NSDictionary<NSString *, NSString *> *env = [[NSProcessInfo processInfo] environment];
    BOOL waitForDebugger = [env[kEnv_WaitForDebugger] isEqualToString:@"YES"];
    if (waitForDebugger) {
      int pid = [[NSProcessInfo processInfo] processIdentifier];
      NSString *beginMessage = [NSString stringWithFormat:@"Waiting for debugger to be attached to pid '%d' ...", pid];
      dispatch_sync(EventQueue(), ^{
        PrintJSON(EventDictionaryWithNameAndContent(
          kReporter_Events_BeginStatus,
          @{
            kReporter_BeginStatus_MessageKey : beginMessage,
            kReporter_BeginStatus_LevelKey : @"Info"
          }
        ));
      });

      // Halt process execution until a debugger is attached
      raise(SIGSTOP);

      NSString *endMessage = [NSString stringWithFormat:@"Debugger was successfully attached to pid '%d'.", pid];
      dispatch_sync(EventQueue(), ^{
        PrintJSON(EventDictionaryWithNameAndContent(
          kReporter_Events_EndStatus,
          @{
            kReporter_BeginStatus_MessageKey : endMessage,
            kReporter_BeginStatus_LevelKey : @"Info"
          }
        ));
      });
    }
  });
}

static void XCTestCase_performTest(id self, SEL sel, id arg1)
{
  SEL originalSelector = @selector(__XCTestCase_performTest:);
  XCWaitForDebuggerIfNeeded();
  XCPerformTestWithSuppressedExpectedAssertionFailures(self, originalSelector, arg1);
}

static void XCTestCaseSuite_performTest(id self, SEL sel, id arg1)
{
  SEL originalSelector = @selector(__XCTestCaseSuite_performTest:);
  XCWaitForDebuggerIfNeeded();
  XCPerformTestWithSuppressedExpectedAssertionFailures(self, originalSelector, arg1);
}

static id XCTRunnerDaemonSession_sharedSession(Class cls, SEL cmd)
{
  return nil;
}

#pragma mark - _enableSymbolication

static BOOL XCTestCase__enableSymbolication(id self, SEL sel)
{
  return NO;
}

#pragma mark - Interposes

/*
 *  We need to close opened fds so all pipe readers are notified and unblocked.
 *  The not obvious and weird part is that we need to print "\n" before closing.
 *  For some reason `select()`, `poll()` and `dispatch_io_read()` will be stuck
 *  if a test calls `exit()` or `abort()`. The found workaround was to print
 *  anithing to a pipe before closing it. Simply closing a pipe doesn't send EOF
 *  to the pipe reader. Printing "\n" should be safe because reader is skipping
 *  empty lines.
 */
static void PrintNewlineAndCloseFDs()
{
  if (__stdout == NULL) {
    return;
  }
  fprintf(__stdout, "\n");
  fclose(__stdout);
  __stdout = NULL;
}

#pragma mark - Entry

static void SwizzleXCTestMethodsIfAvailable()
{
  if ([[[NSBundle mainBundle] bundleIdentifier] hasPrefix:@"com.apple.dt.xctest"]) {
    // Start from Xcode 11.1, XCTest will try to connect to testmanagerd service
    // when reporting test failures (for capture screenshots automatically), and crash
    // if it cannot make a connection.
    // We don't really boot the simulator for running logic tests, so just force
    // it to return nil.
    static dispatch_once_t token;
    dispatch_once(&token, ^{
      XTSwizzleClassSelectorForFunction(
        NSClassFromString(@"XCTRunnerDaemonSession"),
        @selector(sharedSession),
        (IMP)XCTRunnerDaemonSession_sharedSession
      );
    });
  }

  Class testLogClass = objc_getClass("XCTestLog");

  if (testLogClass == nil) {
    // Looks like the XCTest framework has not been loaded yet.
    return;
  }

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    if ([testLogClass instancesRespondToSelector:@selector(testSuiteWillStart:)]) {
      // Swizzle methods for Xcode 8.
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testSuiteWillStart:),
        (IMP)XCTestLog_testSuiteWillStart
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testSuiteDidFinish:),
        (IMP)XCTestLog_testSuiteDidFinish
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testCaseWillStart:),
        (IMP)XCTestLog_testCaseWillStart
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testCaseDidFinish:),
        (IMP)XCTestLog_testCaseDidFinish
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testCase:didFailWithDescription:inFile:atLine:),
        (IMP)XCTestLog_testCaseDidFailWithDescription
      );
    } else {
      // Swizzle methods for Xcode 7 and earlier.
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testSuiteDidStart:),
        (IMP)XCTestLog_testSuiteDidStart
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testSuiteDidStop:),
        (IMP)XCTestLog_testSuiteDidStop
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testCaseDidStart:),
        (IMP)XCTestLog_testCaseDidStart
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testCaseDidStop:),
        (IMP)XCTestLog_testCaseDidStop
      );
      XTSwizzleSelectorForFunction(
        testLogClass,
        @selector(testCaseDidFail:withDescription:inFile:atLine:),
        (IMP)XCTestLog_testCaseDidFail
      );
      XTSwizzleSelectorForFunction(
        objc_getClass("XCTestCaseSuite"),
        @selector(performTest:),
        (IMP)XCTestCaseSuite_performTest
      );
    }
    XTSwizzleSelectorForFunction(
      objc_getClass("XCTestCase"),
      @selector(performTest:),
      (IMP)XCTestCase_performTest
    );
    if ([objc_getClass("XCTestCase") respondsToSelector:@selector(_enableSymbolication)]) {
      // Disable symbolication thing on xctest 7 because it sometimes takes forever.
      XTSwizzleClassSelectorForFunction(
        objc_getClass("XCTestCase"),
        @selector(_enableSymbolication),
        (IMP)XCTestCase__enableSymbolication
      );
    }
  });
}

static void listBundle(NSString *testBundlePath, NSString *outputFile)
{
  NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:outputFile];
  NSBundle *bundle = [NSBundle bundleWithPath:testBundlePath];
  if (!bundle) {
    fprintf(
      stderr,
      "Bundle '%s' does not identify an accessible bundle directory.\n",
      testBundlePath.UTF8String
    );
    exit(TestShimExitCodeBundleOpenError);
  }
  if (![bundle executablePath]) {
    fprintf(stderr, "The bundle at %s does not contain an executable.\n", [testBundlePath UTF8String]);
    exit(TestShimExitCodeMissingExecutable);
  }

  // Make sure the 'XCTest' preference is cleared before we load the
  // test bundle - otherwise we may accidentally start running tests.
  //
  // Instead of seeing the JSON list of test methods, you'll see output like ...
  //
  //   Test Suite 'All tests' started at 2013-11-07 23:47:46 +0000
  //   Test Suite 'All tests' finished at 2013-11-07 23:47:46 +0000.
  //   Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
  //
  // Here's what happens -- As soon as we dlopen() the test bundle, it will also
  // trigger the linker to load XCTest.framework since those are linked by the test bundle.
  // And, as soon as the testing framework loads, the class initializer
  // '+[XCTestSuite initialize]' is triggered.
  //
  // By clearing the preference, we can prevent tests from running.
  [NSUserDefaults.standardUserDefaults removeObjectForKey:XCTestFrameworkName];
  [NSUserDefaults.standardUserDefaults synchronize];

  // We use dlopen() instead of -[NSBundle loadAndReturnError] because, if
  // something goes wrong, dlerror() gives us a much more helpful error message.
  if (dlopen([[bundle executablePath] UTF8String], RTLD_LAZY) == NULL) {
    fprintf(stderr, "%s\n", dlerror());
    exit(TestShimExitCodeDLOpenError);
  }

  // Load the Test Bundle's 'Principal Class' and initialize it.
  // This is necessary for some Testing Frameworks that dynamically add XCTests
  // inside the basic `-init` method.
  Class principalClass = [bundle principalClass];
  if (principalClass && [principalClass instancesRespondToSelector:@selector(init)]) {
    NSLog(@"Calling Principal Class initializer -[%@ init]", NSStringFromClass(principalClass));
    id principalObject = [[principalClass alloc] init];
    NSLog(@"Principal Class %@ initialized", principalObject);
  }

  // Ensure that the principal class exists.
  Class testSuiteClass = NSClassFromString(XCTestSuiteClassName);
  NSCAssert(testSuiteClass, @"Should have %@ class", XCTestFrameworkName);

  // By setting `-XCTest None`, we'll make `-[XCTestSuite allTests]`
  // return all tests.
  [NSUserDefaults.standardUserDefaults setObject:@"None" forKey:XCTestFilterArg];
  XCTestSuite *allTestsSuite = [testSuiteClass performSelector:@selector(allTests)];
  NSCAssert(allTestsSuite, @"Should have gotten a test suite from allTests");

  // Enumerate the test cases, constructing the reported name for them.
  NSArray<XCTestCase *> *allTestCases = TestsFromSuite(allTestsSuite);
  NSMutableArray<NSDictionary<NSString *, NSString *> *> *testsToReport = NSMutableArray.array;
  for (XCTestCase *testCase in allTestCases) {
    NSString *className = nil;
    NSString *methodName = nil;
    NSString *testKey = nil;
    parseXCTestCase(testCase, &className, &methodName, &testKey);
    NSString *legacyTestName = [NSString stringWithFormat:@"%@/%@", className, methodName];
    [testsToReport addObject:@{
      kReporter_ListTest_LegacyTestNameKey: legacyTestName,
      kReporter_ListTest_ClassNameKey: className,
      kReporter_ListTest_MethodNameKey: methodName,
      kReporter_ListTest_TestKey: testKey,
    }];
  }

  // Now write them out after sorting
  [testsToReport sortUsingComparator:^ NSComparisonResult (NSDictionary<NSString *, NSString *> *left, NSDictionary<NSString *, NSString *> *right) {
    return [left[kReporter_ListTest_LegacyTestNameKey] compare:right[kReporter_ListTest_LegacyTestNameKey]];
  }];
  NSError *error = nil;
  NSData *output = [NSJSONSerialization dataWithJSONObject:testsToReport options:0 error:&error];
  NSCAssert(output, @"Failed to generate list test JSON", error);
  [fileHandle writeData:output];

  // Close the file so the other end knows this is the end of the input.
  [fileHandle closeFile];
  exit(TestShimExitCodeSuccess);
}

static BOOL NSBundle_loadAndReturnError(id self, SEL sel, NSError **error)
{
  BOOL (*msgsend)(id, SEL, NSError **) = (void *) objc_msgSend;
  SEL originalSelector = @selector(__NSBundle_loadAndReturnError:);
  BOOL result = msgsend(self, originalSelector, error);
  SwizzleXCTestMethodsIfAvailable();
  return result;
}

static void assignOutputFiles(void)
{
  static const char *stdoutFileKey = "TEST_SHIM_STDOUT_PATH";
  FILE *shimStdoutFile = fopen(getenv(stdoutFileKey), "w");
  if (shimStdoutFile) {
    __stdout = shimStdoutFile;
  } else {
    int stdoutHandle = dup(STDOUT_FILENO);
    __stdout = fdopen(stdoutHandle, "w");
  }
  setvbuf(__stdout, NULL, _IONBF, 0);

  static const char *stderrFileKey = "TEST_SHIM_STDERR_PATH";
  FILE *shimStderrFile = fopen(getenv(stderrFileKey), "w");
  if (shimStderrFile) {
    __stderr = shimStderrFile;
  } else {
    int stderrHandle = dup(STDERR_FILENO);
    __stderr = fdopen(stderrHandle, "w");
  }
}

void handle_signal(int signal)
{
  PrintNewlineAndCloseFDs();
}

static id SimServiceContext_deviceSetWithPath_error(id cls, SEL sel, NSString *path, NSError **error)
{
  id (*msgsend)(id, SEL, NSString *, NSError **) = (void *) objc_msgSend;
  SEL originalSelector = @selector(__SimServiceContext_deviceSetWithPath:error:);
  NSString *simDeviceSetPath = NSProcessInfo.processInfo.environment[@"SIM_DEVICE_SET_PATH"];
  NSLog(@"Calling original -[SimServiceContext deviceSetWithPath:error:] with a custom path: %@", simDeviceSetPath);
  return msgsend(cls, originalSelector, simDeviceSetPath, error);
}

static void SwizzleXcodebuildMethods()
{
  static dispatch_once_t token;
  dispatch_once(&token, ^{
    NSLog(@"Swizzling -[SimServiceContext deviceSetWithPath:error:]");
    NSBundle *bundle = [[NSBundle alloc] initWithPath:@"/Library/Developer/PrivateFrameworks/CoreSimulator.framework"];
    NSError *error = nil;
    [bundle loadAndReturnError:&error];
    if (error) {
      NSLog(@"ERROR: failed to load CoreSimulator.framework: %@", [error localizedFailureReason]);
      exit(1);
    }
    XTSwizzleSelectorForFunction(
      // @lint-ignore FBOBJCDISCOURAGEDFUNCTION
      objc_getClass("SimServiceContext"),
      @selector(deviceSetWithPath:error:),
      (IMP)SimServiceContext_deviceSetWithPath_error
    );
  });
}

__attribute__((constructor)) static void EntryPoint()
{
  // Unset so we don't cascade into any other process that might be spawned.
  unsetenv("DYLD_INSERT_LIBRARIES");

  NSString *bundlePath = NSProcessInfo.processInfo.environment[@"TEST_SHIM_BUNDLE_PATH"];
  if (bundlePath) {
    assignOutputFiles();

    // Listing takes a different path, if the 'TEST_SHIM_OUTPUT_PATH' is set.
    NSString *listPath = NSProcessInfo.processInfo.environment[@"TEST_SHIM_OUTPUT_PATH"];
    if (listPath) {
      NSLog(@"Querying Bundle %@ to Path %@", bundlePath, listPath);
      listBundle(bundlePath, listPath);
      return;
    }


  // Install a signal handler to deal with tests crashing.
    struct sigaction sa_abort;
    sa_abort.sa_handler = &handle_signal;
    sigaction(SIGABRT, &sa_abort, NULL);

    // Let's register to get notified when libraries are initialized
    XTSwizzleSelectorForFunction([NSBundle class], @selector(loadAndReturnError:), (IMP)NSBundle_loadAndReturnError);

    // Then Swizzle
    SwizzleXCTestMethodsIfAvailable();
    return;
  }

  NSString *simDeviceSetPath = NSProcessInfo.processInfo.environment[@"SIM_DEVICE_SET_PATH"];
  if (simDeviceSetPath) {
    BOOL isDir = NO;
    if (![[NSFileManager defaultManager] fileExistsAtPath:simDeviceSetPath isDirectory:&isDir]) {
      NSLog(@"ERROR: SIM_DEVICE_SET_PATH (%@) does not exist", simDeviceSetPath);
      exit(1);
    }
    if (!isDir) {
      NSLog(@"ERROR: SIM_DEVICE_SET_PATH (%@) is not a directory", simDeviceSetPath);
      exit(1);
    }
    SwizzleXcodebuildMethods();
    return;
  }
}

__attribute__((destructor)) static void ExitPoint()
{
  PrintNewlineAndCloseFDs();
}

#pragma clang diagnostic pop
