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

#import <RCTTest/RCTTestRunner.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTModuleMethod.h>
#import <React/RCTRootView.h>

@interface AllocationTestModule : NSObject <RCTBridgeModule, RCTInvalidating>

@property (atomic, assign, getter=isValid) BOOL valid;

@end

@implementation AllocationTestModule

RCT_EXPORT_MODULE();

- (instancetype)init
{
  if ((self = [super init])) {
    self.valid = YES;
  }
  return self;
}

- (void)invalidate
{
  self.valid = NO;
}

RCT_EXPORT_METHOD(test
                  : (__unused NSString *)a
                  : (__unused NSNumber *)b
                  : (__unused RCTResponseSenderBlock)c
                  : (__unused RCTResponseErrorBlock)d)
{
}

@end

@interface RCTAllocationTests : XCTestCase
@end

@implementation RCTAllocationTests {
  NSURL *_bundleURL;
}

- (void)setUp
{
  [super setUp];

  NSString *bundleContents =
      @"var __fbBatchedBridge = {"
       "  callFunctionReturnFlushedQueue: function() { return null; },"
       "  invokeCallbackAndReturnFlushedQueue: function() { return null; },"
       "  flushedQueue: function() { return null; },"
       "};";

  NSURL *tempDir = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
  [[NSFileManager defaultManager] createDirectoryAtURL:tempDir
                           withIntermediateDirectories:YES
                                            attributes:nil
                                                 error:NULL];
  NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString];
  NSString *fileName = [NSString stringWithFormat:@"rctallocationtests-bundle-%@.js", guid];

  _bundleURL = [tempDir URLByAppendingPathComponent:fileName];
  NSError *saveError;
  if (![bundleContents writeToURL:_bundleURL atomically:YES encoding:NSUTF8StringEncoding error:&saveError]) {
    XCTFail(@"Failed to save test bundle to %@, error: %@", _bundleURL, saveError);
  };
}

- (void)tearDown
{
  [super tearDown];

  [[NSFileManager defaultManager] removeItemAtURL:_bundleURL error:NULL];
}

- (void)testBridgeIsDeallocated
{
  __weak RCTBridge *weakBridge;
  @autoreleasepool {
    RCTRootView *view = [[RCTRootView alloc] initWithBundleURL:_bundleURL
                                                    moduleName:@""
                                             initialProperties:nil
                                                 launchOptions:nil];
    weakBridge = view.bridge;
    XCTAssertNotNil(weakBridge, @"RCTBridge should have been created");
    (void)view;
  }

  XCTAssertNil(weakBridge, @"RCTBridge should have been deallocated");
}

- (void)testModulesAreInvalidated
{
  AllocationTestModule *module = [AllocationTestModule new];
  @autoreleasepool {
    RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL
                                              moduleProvider:^{
                                                return @[ module ];
                                              }
                                               launchOptions:nil];
    XCTAssertTrue(module.isValid, @"AllocationTestModule should be valid");
    (void)bridge;
  }

  RCT_RUN_RUNLOOP_WHILE(module.isValid)
  XCTAssertFalse(module.isValid, @"AllocationTestModule should have been invalidated by the bridge");
}

- (void)testModulesAreDeallocated
{
  __weak AllocationTestModule *weakModule;
  @autoreleasepool {
    AllocationTestModule *module = [AllocationTestModule new];
    RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL
                                              moduleProvider:^{
                                                return @[ module ];
                                              }
                                               launchOptions:nil];
    XCTAssertNotNil(module, @"AllocationTestModule should have been created");
    weakModule = module;
    (void)bridge;
  }

  RCT_RUN_RUNLOOP_WHILE(weakModule)
  XCTAssertNil(weakModule, @"AllocationTestModule should have been deallocated");
}

- (void)testModuleMethodsAreDeallocated
{
  static RCTMethodInfo methodInfo = {
      .objcName = "test:(NSString *)a :(nonnull NSNumber *)b :(RCTResponseSenderBlock)c :(RCTResponseErrorBlock)d",
      .jsName = "",
      .isSync = false};

  __weak RCTModuleMethod *weakMethod;
  @autoreleasepool {
    __autoreleasing RCTModuleMethod *method =
        [[RCTModuleMethod alloc] initWithExportedMethod:&methodInfo moduleClass:[AllocationTestModule class]];
    XCTAssertNotNil(method, @"RCTModuleMethod should have been created");
    weakMethod = method;
  }

  RCT_RUN_RUNLOOP_WHILE(weakMethod)
  XCTAssertNil(weakMethod, @"RCTModuleMethod should have been deallocated");
}

- (void)testContentViewIsInvalidated
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL moduleProvider:nil launchOptions:nil];
  __weak UIView *rootContentView;
  @autoreleasepool {
    RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"" initialProperties:nil];
    RCT_RUN_RUNLOOP_WHILE(!(rootContentView = [rootView valueForKey:@"contentView"]))
    XCTAssertTrue(rootContentView.userInteractionEnabled, @"RCTContentView should be valid");
    (void)rootView;
  }

#if !TARGET_OS_TV // userInteractionEnabled is true for Apple TV views
  XCTAssertFalse(rootContentView.userInteractionEnabled, @"RCTContentView should have been invalidated");
#endif
}

/**
 * T42930872:
 *
 * Both bridge invalidation and bridge setUp occur execute concurrently.
 * Therefore, it's not safe for us to create a bridge, and immediately reload on
 * it. It's also unsafe to just reload the bridge, because that calls invalidate
 * and then setUp. Because of these race conditions, this test may randomly
 * crash. Hence, we should disable this test until we either fix the bridge
 * or delete it.
 */
- (void)disabled_testUnderlyingBridgeIsDeallocated
{
  RCTBridge *bridge;
  __weak id batchedBridge;
  @autoreleasepool {
    bridge = [[RCTBridge alloc] initWithBundleURL:_bundleURL moduleProvider:nil launchOptions:nil];
    batchedBridge = bridge.batchedBridge;
    XCTAssertTrue([batchedBridge isValid], @"RCTBridge impl should be valid");
    [bridge reload];
  }

  RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil)

  XCTAssertNotNil(bridge, @"RCTBridge should not have been deallocated");
  XCTAssertNil(batchedBridge, @"RCTBridge impl should have been deallocated");

  // Wait to complete the test until the new bridge impl is also deallocated
  @autoreleasepool {
    batchedBridge = bridge.batchedBridge;
    [bridge invalidate];
    bridge = nil;
  }

  RCT_RUN_RUNLOOP_WHILE(batchedBridge != nil);
  XCTAssertNil(batchedBridge);
}

@end
