/*
 * 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 "FBXCTestProcess.h"

#import <sys/wait.h>

#import <FBControlCore/FBControlCore.h>

#import "FBXCTestConstants.h"
#import "XCTestBootstrapError.h"

static NSTimeInterval const CrashLogStartDateFuzz = -20;
static NSTimeInterval const CrashLogWaitTime = 180; // In case resources are pegged, just wait
static NSTimeInterval const SampleDuration = 1;
static NSTimeInterval const KillBackoffTimeout = 1;

@implementation FBXCTestProcess

#pragma mark Public

+ (FBFuture<NSNumber *> *)ensureProcess:(FBProcess *)process completesWithin:(NSTimeInterval)timeout crashLogCommands:(id<FBCrashLogCommands>)crashLogCommands queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger
{
  // The start date of the process appear slightly older than we might think, so avoid missing it by a few seconds.
  NSDate *startDate = [NSDate.date dateByAddingTimeInterval:CrashLogStartDateFuzz];

  [logger logFormat:@"Waiting for %d to exit within %f seconds", process.processIdentifier, timeout];
  return [[[process
    statLoc]
    onQueue:queue timeout:timeout handler:^{
      return [FBXCTestProcess performSampleStackshotOnProcess:process forTimeout:timeout queue:queue logger:logger];;
    }]
    onQueue:queue fmap:^(id _) {
      // This will not be reached if the sample error ran.
      return [[process
        exitCode] // Re-map to the exit code as the first part of the chain will fire on *any* exit (including crashes).
        onQueue:queue chain:^ FBFuture<NSNumber *> * (FBFuture<NSNumber *> *exitCodeFuture) {
          // If there's an exit code, there wasn't a crash. Exit code handling is done in the caller.
          if (exitCodeFuture.state == FBFutureStateDone) {
            return exitCodeFuture;
          }
          // Here we know a signalled exit has occurred. This return happens if no crash log detection is present.
          if (!crashLogCommands) {
            return exitCodeFuture;
          }
          // Here we know we want to find the crash log, so attempt to get it.
          return [FBXCTestProcess performCrashLogQueryForProcess:process startDate:startDate crashLogCommands:crashLogCommands crashLogWaitTime:CrashLogWaitTime queue:queue logger:logger];
        }];
    }];
}

+ (nullable NSString *)describeFailingExitCode:(int)exitCode
{
  switch (exitCode) {
    case 0:
      return nil;
    case 1:
      return nil;
    case TestShimExitCodeDLOpenError:
      return @"DLOpen Error";
    case TestShimExitCodeBundleOpenError:
      return @"Error opening test bundle";
    case TestShimExitCodeMissingExecutable:
      return @"Missing executable";
    case TestShimExitCodeXCTestFailedLoading:
      return @"XCTest Framework failed loading";
    default:
      return [NSString stringWithFormat:@"Unknown xctest exit code %d", exitCode];
  }
}

+ (FBFuture<id> *)performSampleStackshotOnProcessIdentifier:(pid_t)processIdentifier forTimeout:(NSTimeInterval)timeout queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger
{
  [logger logFormat:@"Performing stackshot on process %d as it has not exited after %f seconds", processIdentifier, timeout];
  return [[[[FBProcessBuilder
    withLaunchPath:@"/usr/bin/sample" arguments:@[@(processIdentifier).stringValue, @(SampleDuration).stringValue]]
    runUntilCompletionWithAcceptableExitCodes:nil]
    onQueue:queue handleError:^(NSError *error) {
      return [[[FBXCTestError
        describeFormat:@"Failed to obtain a stack sample of stalled xctest process %d", processIdentifier]
        causedBy:error]
        failFuture];
    }]
    onQueue:queue fmap:^(FBProcess<NSNull *, NSData *, NSData *> *task) {
      [logger logFormat:@"Stackshot completed of process %d", processIdentifier];
      return [[FBXCTestError
        describeFormat:@"Waited %f seconds for process %d to terminate, but the xctest process stalled: %@", timeout, processIdentifier, task.stdOut]
        failFuture];
    }];
}

#pragma mark Private

+ (FBFuture<id> *)performSampleStackshotOnProcess:(FBProcess *)process forTimeout:(NSTimeInterval)timeout queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger
{
  return [[self
    performSampleStackshotOnProcessIdentifier:process.processIdentifier forTimeout:timeout queue:queue logger:logger]
    onQueue:queue notifyOfCompletion:^(FBFuture *_) {
      [logger logFormat:@"Terminating stalled xctest process %@", process];
      [[process
        sendSignal:SIGTERM backingOffToKillWithTimeout:KillBackoffTimeout logger:logger]
        onQueue:queue notifyOfCompletion:^(FBFuture *__) {
          [logger logFormat:@"Stalled xctest process %@ has been terminated", process];
        }];
    }];
}

+ (FBFuture<NSNumber *> *)performCrashLogQueryForProcess:(FBProcess *)process startDate:(NSDate *)startDate crashLogCommands:(id<FBCrashLogCommands>)crashLogCommands crashLogWaitTime:(NSTimeInterval)crashLogWaitTime queue:(dispatch_queue_t)queue logger:(id<FBControlCoreLogger>)logger
{
  [logger logFormat:@"xctest process (%d) died prematurely, checking for crash log for %f seconds", process.processIdentifier, crashLogWaitTime];
  return [[[FBXCTestProcess
    crashLogsForTerminationOfProcess:process since:startDate crashLogCommands:crashLogCommands crashLogWaitTime:crashLogWaitTime queue:queue]
    rephraseFailure:@"xctest process (%d) exited abnormally with no crash log, to check for yourself look in ~/Library/Logs/DiagnosticReports", process.processIdentifier]
    onQueue:queue fmap:^(FBCrashLogInfo *crashInfo) {
      NSString *crashString = [NSString stringWithContentsOfFile:crashInfo.crashPath encoding:NSUTF8StringEncoding error:nil];
      return [[FBXCTestError
        describeFormat:@"xctest process crashed\n %@", crashString]
        failFuture];
    }];
}

+ (FBFuture<FBCrashLogInfo *> *)crashLogsForTerminationOfProcess:(FBProcess *)process since:(NSDate *)sinceDate crashLogCommands:(id<FBCrashLogCommands>)crashLogCommands crashLogWaitTime:(NSTimeInterval)crashLogWaitTime queue:(dispatch_queue_t)queue
{
  NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[
    [FBCrashLogInfo predicateForCrashLogsWithProcessID:process.processIdentifier],
    [FBCrashLogInfo predicateNewerThanDate:sinceDate],
  ]];

  return [[crashLogCommands
    notifyOfCrash:predicate]
    timeout:crashLogWaitTime waitingFor:@"Crash logs for terminated process %d to appear", process.processIdentifier];
}

@end
