// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/test/earl_grey/chrome_egtest_bundle_main.h"

#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <grpc/grpc.h>
#import <grpcpp/grpcpp.h>
#import <objc/runtime.h>

#import <memory>

#import "base/apple/bundle_locations.h"
#import "base/at_exit.h"
#import "base/check.h"
#import "base/command_line.h"
#import "base/debug/stack_trace.h"
#import "base/i18n/icu_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/types/fixed_array.h"
#import "ios/chrome/test/earl_grey/chrome_egtest_plugin_client.h"
#import "ios/testing/test_expectations.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/resource/resource_bundle.h"

using chrome_egtest_plugin::TestPluginClient;
using grpc::Channel;

namespace {

const grpc::string gRPCHost = "localhost:32279";

// Contains startup code for a Chrome EG2 test module. Performs startup tasks
// when constructed and shutdown tasks when destroyed. Callers should create an
// instance of this object before running any tests and destroy the instance
// after tests have completed.
class TestMain {
 public:
  TestMain() {
    NSArray* arguments = NSProcessInfo.processInfo.arguments;

    // Convert NSArray to the required input type of `base::CommandLine::Init`.
    int argc = arguments.count;
    base::FixedArray<const char*> argv(argc);
    std::vector<std::string> argv_store;
    // Avoid using std::vector::push_back (or any other method that could cause
    // the vector to grow) as this will cause the std::string to be copied or
    // moved (depends on the C++ implementation) which may invalidates the
    // pointer returned by std::string::c_str(). Even if the strings are moved,
    // this may cause garbage if std::string uses optimisation for small strings
    // (by returning pointer to the object internals in that case).
    argv_store.resize(argc);
    for (int i = 0; i < argc; i++) {
      argv_store[i] = base::SysNSStringToUTF8(arguments[i]);
      argv[i] = argv_store[i].c_str();
    }

    // Initialize the CommandLine with arguments. ResourceBundle requires
    // CommandLine to exist.
    base::CommandLine::Init(argc, argv.data());

    // Configures the default framework bundle to point to the test module
    // bundle instead of the test runner app.
    base::apple::SetOverrideFrameworkBundle(
        [NSBundle bundleForClass:[ChromeEGTestBundleMain class]]);

    base::i18n::InitializeICU();

    // Load pak files into the ResourceBundle.
    l10n_util::OverrideLocaleWithCocoaLocale();
    const std::string loaded_locale =
        ui::ResourceBundle::InitSharedInstanceWithLocale(
            /*pref_locale=*/std::string(), /*delegate=*/nullptr,
            ui::ResourceBundle::LOAD_COMMON_RESOURCES);
    CHECK(!loaded_locale.empty());
  }

  TestMain(const TestMain&) = delete;
  TestMain& operator=(const TestMain&) = delete;

  ~TestMain() {}

 private:
  base::AtExitManager exit_manager_;
};
}  // namespace

@class XCTSourceCodeSymbolInfo;
@protocol XCTSymbolInfoProviding <NSObject>
- (XCTSourceCodeSymbolInfo*)symbolInfoForAddressInCurrentProcess:(pid_t)pid
                                                           error:
                                                               (NSError**)error;
@end

@interface XCTSymbolicationService
+ (void)setSharedService:(id<XCTSymbolInfoProviding>)arg1;
@end

@interface ChromeEGTestBundleMain () <XCTestObservation> {
  std::unique_ptr<TestMain> _testMain;
  std::unique_ptr<TestPluginClient> _testPluginClient;
}

@property BOOL pluginsEnabled;

@end

@implementation ChromeEGTestBundleMain

- (instancetype)init {
  if ((self = [super init])) {
    [[XCTestObservationCenter sharedTestObservationCenter]
        addTestObserver:self];
    self.pluginsEnabled = NO;
  }

  // initializing test plugin client iff test plugin server is running on the
  // host and at least one plugin is enabled for this test run
  _testPluginClient = std::make_unique<TestPluginClient>(
      grpc::CreateChannel(gRPCHost, grpc::InsecureChannelCredentials()));
  NSLog(@"Checking whether any test plugins are enabled...");
  std::vector<std::string> enabledPlugins =
      _testPluginClient->ListEnabledPlugins();

  // we will not use the test plugin feature if test runner server side is not
  // running (e.g. tests are run locally), or no plugins are enabled for this
  // test run.
  if (enabledPlugins.size() == 0) {
    NSLog(@"iOS test runner is not running, or no test plugins are enabled. "
          @"Test plugins feature will not be used.");
    _testPluginClient.reset();

  } else {
    self.pluginsEnabled = YES;
    NSLog(@"At least one test plugin is enabled. Test plugins features will be "
          @"used throughout tests executions");
  }
  return self;
}

// -waitForQuiescenceIncludingAnimationsIdle:isPreEvent: tends to introduce an
// unnecessary delay, as EarlGrey already checks for animations to complete.
// Swizzling and skipping the following call speeds up test runs.
- (void)disableWaitForIdle {
  SEL originalSelector = NSSelectorFromString(
      @"waitForQuiescenceIncludingAnimationsIdle:isPreEvent:");
  SEL swizzledSelector = @selector(skipQuiescenceDelay);
  Method originalMethod = class_getInstanceMethod(
      objc_getClass("XCUIApplicationProcess"), originalSelector);
  if (originalMethod == nil) {
    NSLog(@"Could not swizzle waitForQuiescenceIncludingAnimationsIdle.");
    return;
  }
  Method swizzledMethod =
      class_getInstanceMethod([self class], swizzledSelector);
  method_exchangeImplementations(originalMethod, swizzledMethod);
}

// Empty swizzled method to be invoked by XCTest at the start of each test case.
// Since earl grey synchronizes automatically, do nothing here.
- (void)skipQuiescenceDelay {
}

#pragma mark - XCTestObservation

- (void)testBundleWillStart:(NSBundle*)testBundle {
  DCHECK(!_testMain);
  _testMain = std::make_unique<TestMain>();

  // Ensure that //ios/web and the bulk of //ios/chrome/browser are not compiled
  // into the test module. This is hard to assert at compile time, due to
  // transitive dependencies, but at runtime it's easy to check if certain key
  // classes are present or absent.
  CHECK(NSClassFromString(@"CRWWebController") == nil);
  CHECK(NSClassFromString(@"MainController") == nil);
  CHECK(NSClassFromString(@"BrowserViewController") == nil);

  // Disable aggressive symbolication and disable symbolication service to work
  // around slow XCTest assertion failures. These failures are spending a very
  // long time attempting to symbolicate.
  Class symbolicationService = NSClassFromString(@"XCTSymbolicationService");
  if (symbolicationService != nil) {
    [symbolicationService setSharedService:nil];
  }
  [[NSUserDefaults standardUserDefaults]
      setBool:YES
       forKey:@"XCTDisableAggressiveSymbolication"];

  // Disable long wait for idle messages.
  [self disableWaitForIdle];
}

- (void)testBundleDidFinish:(NSBundle*)testBundle {
  if (self.pluginsEnabled) {
    NSLog(@"calling testBundleWillFinish to test plugin server");
    std::string deviceName =
        base::SysNSStringToUTF8(UIDevice.currentDevice.name);
    _testPluginClient->TestBundleWillFinish(deviceName);
  }

  [[XCTestObservationCenter sharedTestObservationCenter]
      removeTestObserver:self];

  _testMain.reset();
}

- (void)testCaseWillStart:(XCTestCase*)testCase {
  if (self.pluginsEnabled) {
    NSLog(@"calling testCaseWillStart to test plugin server");
    std::string testName = base::SysNSStringToUTF8(testCase.name);
    std::string deviceName =
        base::SysNSStringToUTF8(UIDevice.currentDevice.name);
    _testPluginClient->TestCaseWillStart(testName, deviceName);
  }
}

// this is called when test case failed unexpectedly
- (void)testCase:(XCTestCase*)testCase didRecordIssue:(XCTIssue*)issue {
  if (self.pluginsEnabled) {
    NSLog(@"calling testCaseDidFail to test plugin server");
    std::string testName = base::SysNSStringToUTF8(testCase.name);
    std::string deviceName =
        base::SysNSStringToUTF8(UIDevice.currentDevice.name);
    _testPluginClient->TestCaseDidFail(testName, deviceName);
  }
  NSString* current_stack =
      base::SysUTF8ToNSString(base::debug::StackTrace().ToString());
  NSLog(@"%@", current_stack);
}

- (void)testCaseDidFinish:(XCTestCase*)testCase {
  if (self.pluginsEnabled) {
    NSLog(@"calling testCaseDidFinish to test plugin server");
    std::string testName = base::SysNSStringToUTF8(testCase.name);
    std::string deviceName =
        base::SysNSStringToUTF8(UIDevice.currentDevice.name);
    _testPluginClient->TestCaseDidFinish(testName, deviceName);
  }
}

@end
