#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "Knotapi.h"
#import "KnotAPI/KnotAPI-Swift.h"

/**
 * KnotapiEventTests - Comprehensive tests for event handling logic
 * 
 * This test suite focuses specifically on testing the event handling mechanisms
 * in the Knotapi bridge, including safety checks, data validation, and
 * proper event transmission to React Native.
 */
@interface KnotapiEventTests : XCTestCase
@property (nonatomic, strong) Knotapi *knotapi;
@property (nonatomic, strong) id mockKnot;
@end

@implementation KnotapiEventTests

- (void)setUp {
    [super setUp];
    self.knotapi = [[Knotapi alloc] init];
    self.mockKnot = OCMClassMock([Knot class]);
}

- (void)tearDown {
    self.knotapi = nil;
    self.mockKnot = nil;
    [super tearDown];
}

#pragma mark - Event Safety Tests

/**
 * Test that events are ignored when bridge is inactive
 */
- (void)testEventsIgnoredWhenBridgeInactive {
    // Given: Bridge is inactive
    self.knotapi.isBridgeActive = NO;
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Various events are received
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    [mockKnotapi onEventWithEvent:mockEvent];
    
    [mockKnotapi onSuccessWithMerchant:@"test_merchant"];
    [mockKnotapi onExit];
    
    // Then: No events should be sent
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test that events are ignored when bridge is closing
 */
- (void)testEventsIgnoredWhenBridgeClosing {
    // Given: Bridge is closing
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = YES;
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Various events are received
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    [mockKnotapi onEventWithEvent:mockEvent];
    
    [mockKnotapi onSuccessWithMerchant:@"test_merchant"];
    [mockKnotapi onExit];
    
    // Then: No events should be sent
    OCMVerifyAll(mockKnotapi);
}

#pragma mark - Error Event Tests

/**
 * Test error event handling with valid data
 */
- (void)testErrorEventHandling {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"errorCode"] isEqualToString:@"Invalid_Session"] && 
               [body[@"errorDescription"] isEqualToString:@"The session is invalid."];
    }]]);
    
    // When: Error event is received
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    // Then: Error event should be sent with correct data
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test error event handling with product name
 */
- (void)testErrorEventWithProductName {
    // Given: Bridge is active and product name is set
    XCTAssertTrue(self.knotapi.isBridgeActive);
    self.knotapi.productName = @"card_switcher";
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"errorCode"] isEqualToString:@"Invalid_Session"] && 
               [body[@"errorDescription"] isEqualToString:@"The session is invalid."] &&
               [body[@"product"] isEqualToString:@"card_switcher"];
    }]]);
    
    // When: Error event is received
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    // Then: Error event should include product name
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test all error types are handled correctly
 */
- (void)testAllErrorTypes {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Test data for each error type
    NSArray *testCases = @[
        @[@(KnotErrorInvalidSession), @"Invalid_Session", @"The session is invalid."],
        @[@(KnotErrorInvalidCardName), @"INVALID_CARD_NAME", @"The card name is invalid."],
        @[@(KnotErrorInvalidCustomerName), @"INVALID_CUSTOMER_NAME", @"The customer name is invalid."],
        @[@(KnotErrorInvalidLogoId), @"INVALID_LOGO_ID", @"The logo ID is invalid."],
        @[@(KnotErrorExpiredSession), @"Expired_Session", @"The session has expired."],
        @[@(KnotErrorInvalidClientId), @"Invalid_Client_Id", @"The client ID is invalid."],
        @[@(KnotErrorInternalError), @"Internal_Error", @"An internal error occurred."],
        @[@(KnotErrorMerchantIdNotFound), @"Merchant_Id_Not_Found", @"The merchant ID is required."]
    ];
    
    for (NSArray *testCase in testCases) {
        KnotError error = [testCase[0] intValue];
        NSString *expectedCode = testCase[1];
        NSString *expectedDescription = testCase[2];
        
        // Create fresh mock for each test
        Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
        OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
            return [body[@"errorCode"] isEqualToString:expectedCode] && 
                   [body[@"errorDescription"] isEqualToString:expectedDescription];
        }]]);
        
        // When: Error event is received
        [mockKnotapi onErrorWithError:error];
        
        // Then: Error event should be sent with correct data
        OCMVerifyAll(mockKnotapi);
    }
}

#pragma mark - General Event Tests

/**
 * Test general event handling with valid data
 */
- (void)testGeneralEventHandling {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with valid data
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentSandbox));
    OCMStub([mockEvent event]).andReturn(@"test_event");
    OCMStub([mockEvent merchant]).andReturn(@"test_merchant");
    OCMStub([mockEvent merchantId]).andReturn(@"123");
    OCMStub([mockEvent product]).andReturnValue(@(0));
    OCMStub([mockEvent taskId]).andReturn(@"task_123");
    OCMStub([mockEvent metaData]).andReturn(@{@"key": @"value"});
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"event"] isEqualToString:@"test_event"] &&
               [body[@"environment"] isEqualToString:@"sandbox"] &&
               [body[@"merchantName"] isEqualToString:@"test_merchant"] &&
               [body[@"merchantId"] isEqualToString:@"123"] &&
               [body[@"product"] isEqualToString:@"card_switcher"] &&
               [body[@"taskId"] isEqualToString:@"task_123"] &&
               [body[@"metaData"] isKindOfClass:[NSDictionary class]];
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent with correct data
    OCMVerifyAll(mockKnotapi);
    XCTAssertEqualObjects(self.knotapi.productName, @"card_switcher", @"Product name should be set");
}

/**
 * Test general event handling with nil data
 */
- (void)testGeneralEventHandlingWithNilData {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with nil data
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentProduction));
    OCMStub([mockEvent event]).andReturn(nil);
    OCMStub([mockEvent merchant]).andReturn(nil);
    OCMStub([mockEvent merchantId]).andReturn(nil);
    OCMStub([mockEvent product]).andReturnValue(@(1));
    OCMStub([mockEvent taskId]).andReturn(nil);
    OCMStub([mockEvent metaData]).andReturn(nil);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not crash and should create a body without nil values
        return body != nil && 
               body[@"event"] == nil && 
               body[@"merchantName"] == nil && 
               body[@"merchantId"] == nil &&
               [body[@"environment"] isEqualToString:@"production"] &&
               [body[@"product"] isEqualToString:@"transaction_link"];
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent safely without crashing
    OCMVerifyAll(mockKnotapi);
    XCTAssertEqualObjects(self.knotapi.productName, @"transaction_link", @"Product name should be set");
}

/**
 * Test general event handling with NSNull data
 */
- (void)testGeneralEventHandlingWithNSNullData {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with NSNull data
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentDevelopment));
    OCMStub([mockEvent event]).andReturn([NSNull null]);
    OCMStub([mockEvent merchant]).andReturn([NSNull null]);
    OCMStub([mockEvent merchantId]).andReturn([NSNull null]);
    OCMStub([mockEvent product]).andReturnValue(@(2));
    OCMStub([mockEvent taskId]).andReturn([NSNull null]);
    OCMStub([mockEvent metaData]).andReturn([NSNull null]);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not crash and should create a body without NSNull values
        return body != nil && 
               body[@"event"] == nil && 
               body[@"merchantName"] == nil && 
               body[@"merchantId"] == nil &&
               [body[@"environment"] isEqualToString:@"development"] &&
               [body[@"product"] isEqualToString:@"link"];
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent safely without crashing
    OCMVerifyAll(mockKnotapi);
    XCTAssertEqualObjects(self.knotapi.productName, @"link", @"Product name should be set");
}

#pragma mark - Success Event Tests

/**
 * Test success event handling
 */
- (void)testSuccessEventHandling {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onSuccess" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"merchant"] isEqualToString:@"test_merchant"];
    }]]);
    
    // When: Success event is received
    [mockKnotapi onSuccessWithMerchant:@"test_merchant"];
    
    // Then: Success event should be sent
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test success event handling with product name
 */
- (void)testSuccessEventWithProductName {
    // Given: Bridge is active and product name is set
    XCTAssertTrue(self.knotapi.isBridgeActive);
    self.knotapi.productName = @"card_switcher";
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onSuccess" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"merchant"] isEqualToString:@"test_merchant"] &&
               [body[@"product"] isEqualToString:@"card_switcher"];
    }]]);
    
    // When: Success event is received
    [mockKnotapi onSuccessWithMerchant:@"test_merchant"];
    
    // Then: Success event should include product name
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test success event handling with nil merchant
 */
- (void)testSuccessEventWithNilMerchant {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onSuccess" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not include merchant key when merchant is nil
        return body != nil && body[@"merchant"] == nil;
    }]]);
    
    // When: Success event is received with nil merchant
    [mockKnotapi onSuccessWithMerchant:nil];
    
    // Then: Success event should be sent without merchant
    OCMVerifyAll(mockKnotapi);
}

#pragma mark - Exit Event Tests

/**
 * Test exit event handling
 */
- (void)testExitEventHandling {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onExit" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Exit event should have empty body or only product if set
        return body != nil;
    }]]);
    
    // When: Exit event is received
    [mockKnotapi onExit];
    
    // Then: Exit event should be sent
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test exit event handling with product name
 */
- (void)testExitEventWithProductName {
    // Given: Bridge is active and product name is set
    XCTAssertTrue(self.knotapi.isBridgeActive);
    self.knotapi.productName = @"card_switcher";
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onExit" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"product"] isEqualToString:@"card_switcher"];
    }]]);
    
    // When: Exit event is received
    [mockKnotapi onExit];
    
    // Then: Exit event should include product name
    OCMVerifyAll(mockKnotapi);
}

#pragma mark - Helper Method Tests

/**
 * Test isBridgeReadyForEvents helper method
 */
- (void)testIsBridgeReadyForEvents {
    // Test when bridge is active and not closing
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = NO;
    XCTAssertTrue([self.knotapi isBridgeReadyForEvents], @"Bridge should be ready when active and not closing");
    
    // Test when bridge is inactive
    self.knotapi.isBridgeActive = NO;
    self.knotapi.isClosing = NO;
    XCTAssertFalse([self.knotapi isBridgeReadyForEvents], @"Bridge should not be ready when inactive");
    
    // Test when bridge is closing
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = YES;
    XCTAssertFalse([self.knotapi isBridgeReadyForEvents], @"Bridge should not be ready when closing");
    
    // Test when bridge is both inactive and closing
    self.knotapi.isBridgeActive = NO;
    self.knotapi.isClosing = YES;
    XCTAssertFalse([self.knotapi isBridgeReadyForEvents], @"Bridge should not be ready when inactive and closing");
}

/**
 * Test createSafeDictionaryFromDictionary helper method
 */
- (void)testCreateSafeDictionaryFromDictionary {
    // Test with valid dictionary
    NSDictionary *validDict = @{@"key1": @"value1", @"key2": @"value2"};
    NSDictionary *result = [self.knotapi createSafeDictionaryFromDictionary:validDict];
    XCTAssertEqualObjects(result, validDict, @"Valid dictionary should be returned unchanged");
    
    // Test with dictionary containing nil values
    NSMutableDictionary *dictWithNil = [NSMutableDictionary dictionary];
    dictWithNil[@"key1"] = @"value1";
    dictWithNil[@"key2"] = [NSNull null];
    dictWithNil[@"key3"] = @"value3";
    
    NSDictionary *expectedResult = @{@"key1": @"value1", @"key3": @"value3"};
    NSDictionary *safeResult = [self.knotapi createSafeDictionaryFromDictionary:dictWithNil];
    XCTAssertEqualObjects(safeResult, expectedResult, @"NSNull values should be filtered out");
    
    // Test with nil input
    NSDictionary *nilResult = [self.knotapi createSafeDictionaryFromDictionary:nil];
    XCTAssertEqualObjects(nilResult, @{}, @"Nil input should return empty dictionary");
    
    // Test with non-dictionary input
    NSDictionary *nonDictResult = [self.knotapi createSafeDictionaryFromDictionary:@"not a dictionary"];
    XCTAssertEqualObjects(nonDictResult, @{}, @"Non-dictionary input should return empty dictionary");
}

/**
 * Test addSafeStringValue helper method
 */
- (void)testAddSafeStringValue {
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    
    // Test with valid string
    [self.knotapi addSafeStringValue:@"test" forKey:@"key" toDictionary:dict];
    XCTAssertEqualObjects(dict[@"key"], @"test", @"Valid string should be added");
    
    // Test with nil value
    [self.knotapi addSafeStringValue:nil forKey:@"key2" toDictionary:dict];
    XCTAssertNil(dict[@"key2"], @"Nil value should not be added");
    
    // Test with NSNull value
    [self.knotapi addSafeStringValue:(NSString *)[NSNull null] forKey:@"key3" toDictionary:dict];
    XCTAssertNil(dict[@"key3"], @"NSNull value should not be added");
    
    // Test with non-string value
    [self.knotapi addSafeStringValue:(NSString *)@123 forKey:@"key4" toDictionary:dict];
    XCTAssertNil(dict[@"key4"], @"Non-string value should not be added");
    
    // Test with nil key
    [self.knotapi addSafeStringValue:@"test" forKey:nil toDictionary:dict];
    XCTAssertEqual(dict.count, 1, @"Nil key should not be added");
}

/**
 * Test addSafeMetaData helper method
 */
- (void)testAddSafeMetaData {
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    
    // Test with valid metadata
    NSDictionary *validMetaData = @{@"key1": @"value1", @"key2": @"value2"};
    [self.knotapi addSafeMetaData:validMetaData forKey:@"metaData" toDictionary:dict];
    XCTAssertEqualObjects(dict[@"metaData"], validMetaData, @"Valid metadata should be added");
    
    // Test with metadata containing NSNull
    NSMutableDictionary *metaDataWithNull = [NSMutableDictionary dictionary];
    metaDataWithNull[@"key1"] = @"value1";
    metaDataWithNull[@"key2"] = [NSNull null];
    metaDataWithNull[@"key3"] = @"value3";
    
    [self.knotapi addSafeMetaData:metaDataWithNull forKey:@"metaData2" toDictionary:dict];
    NSDictionary *expectedMetaData = @{@"key1": @"value1", @"key3": @"value3"};
    XCTAssertEqualObjects(dict[@"metaData2"], expectedMetaData, @"NSNull values should be filtered from metadata");
    
    // Test with nil metadata
    [self.knotapi addSafeMetaData:nil forKey:@"metaData3" toDictionary:dict];
    XCTAssertNil(dict[@"metaData3"], @"Nil metadata should not be added");
    
    // Test with NSNull metadata
    [self.knotapi addSafeMetaData:(NSDictionary *)[NSNull null] forKey:@"metaData4" toDictionary:dict];
    XCTAssertNil(dict[@"metaData4"], @"NSNull metadata should not be added");
    
    // Test with empty metadata
    [self.knotapi addSafeMetaData:@{} forKey:@"metaData5" toDictionary:dict];
    XCTAssertNil(dict[@"metaData5"], @"Empty metadata should not be added");
}

#pragma mark - Edge Case Tests

/**
 * Test event handling with completely nil event object
 */
- (void)testEventHandlingWithNilEvent {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Nil event is received
    [mockKnotapi onEventWithEvent:nil];
    
    // Then: No event should be sent
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test that product name is correctly set for different product types
 */
- (void)testProductNameSetting {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Test product type 0 (card_switcher)
    KnotEvent *mockEvent1 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent1 product]).andReturnValue(@(0));
    OCMStub([mockEvent1 environment]).andReturnValue(@(EnvironmentProduction));
    
    [self.knotapi onEventWithEvent:mockEvent1];
    XCTAssertEqualObjects(self.knotapi.productName, @"card_switcher", @"Product type 0 should set card_switcher");
    
    // Test product type 1 (transaction_link)
    KnotEvent *mockEvent2 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent2 product]).andReturnValue(@(1));
    OCMStub([mockEvent2 environment]).andReturnValue(@(EnvironmentProduction));
    
    [self.knotapi onEventWithEvent:mockEvent2];
    XCTAssertEqualObjects(self.knotapi.productName, @"transaction_link", @"Product type 1 should set transaction_link");
    
    // Test product type 2 (link)
    KnotEvent *mockEvent3 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent3 product]).andReturnValue(@(2));
    OCMStub([mockEvent3 environment]).andReturnValue(@(EnvironmentProduction));
    
    [self.knotapi onEventWithEvent:mockEvent3];
    XCTAssertEqualObjects(self.knotapi.productName, @"link", @"Product type 2 should set link");
    
    // Test unknown product type (should default to card_switcher)
    KnotEvent *mockEvent4 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent4 product]).andReturnValue(@(999));
    OCMStub([mockEvent4 environment]).andReturnValue(@(EnvironmentProduction));
    
    [self.knotapi onEventWithEvent:mockEvent4];
    XCTAssertEqualObjects(self.knotapi.productName, @"card_switcher", @"Unknown product type should default to card_switcher");
}

/**
 * Test event handling with nil metadata
 */
- (void)testEventHandlingWithNilMetadata {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with nil metadata
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentProduction));
    OCMStub([mockEvent event]).andReturn(@"test_event");
    OCMStub([mockEvent merchant]).andReturn(@"test_merchant");
    OCMStub([mockEvent merchantId]).andReturn(@"123");
    OCMStub([mockEvent product]).andReturnValue(@(0));
    OCMStub([mockEvent taskId]).andReturn(@"task_123");
    OCMStub([mockEvent metaData]).andReturn(nil);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not crash and should not include metadata key
        return body != nil && body[@"metaData"] == nil;
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent safely without metadata
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test event handling with nested nil values in metadata
 */
- (void)testEventHandlingWithNestedNilMetadata {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with nested nil metadata
    NSMutableDictionary *nestedMeta = [NSMutableDictionary dictionary];
    nestedMeta[@"validKey"] = @"validValue";
    nestedMeta[@"nilValue"] = nil;
    nestedMeta[@"nullValue"] = [NSNull null];
    nestedMeta[@"nestedDict"] = @{
        @"innerValid": @"innerValue",
        @"innerNil": nil,
        @"innerNull": [NSNull null]
    };
    
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentProduction));
    OCMStub([mockEvent event]).andReturn(@"test_event");
    OCMStub([mockEvent merchant]).andReturn(@"test_merchant");
    OCMStub([mockEvent merchantId]).andReturn(@"123");
    OCMStub([mockEvent product]).andReturnValue(@(0));
    OCMStub([mockEvent taskId]).andReturn(@"task_123");
    OCMStub([mockEvent metaData]).andReturn(nestedMeta);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not crash and should have cleaned metadata
        NSDictionary *metaData = body[@"metaData"];
        return body != nil && 
               metaData != nil && 
               metaData.count == 2 && // Only validKey and nestedDict should remain
               [metaData[@"validKey"] isEqualToString:@"validValue"] &&
               metaData[@"nestedDict"] != nil;
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent with cleaned metadata
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test event handling with deeply nested nil values
 */
- (void)testEventHandlingWithDeeplyNestedNilMetadata {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with deeply nested nil metadata
    NSMutableDictionary *deepNestedMeta = [NSMutableDictionary dictionary];
    deepNestedMeta[@"level1"] = @{
        @"level2": @{
            @"level3": @{
                @"valid": @"value",
                @"nil": nil,
                @"null": [NSNull null]
            },
            @"level2nil": nil,
            @"level2null": [NSNull null]
        },
        @"level1nil": nil,
        @"level1null": [NSNull null]
    };
    
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentProduction));
    OCMStub([mockEvent event]).andReturn(@"test_event");
    OCMStub([mockEvent merchant]).andReturn(@"test_merchant");
    OCMStub([mockEvent merchantId]).andReturn(@"123");
    OCMStub([mockEvent product]).andReturnValue(@(0));
    OCMStub([mockEvent taskId]).andReturn(@"task_123");
    OCMStub([mockEvent metaData]).andReturn(deepNestedMeta);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not crash and should have cleaned metadata
        NSDictionary *metaData = body[@"metaData"];
        return body != nil && 
               metaData != nil && 
               metaData.count == 1 && // Only level1 should remain
               metaData[@"level1"] != nil;
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent with cleaned metadata
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test event handling with metadata containing only nil/NSNull values
 */
- (void)testEventHandlingWithOnlyNilMetadata {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create mock event with metadata containing only nil/NSNull values
    NSMutableDictionary *allNilMeta = [NSMutableDictionary dictionary];
    allNilMeta[@"nil1"] = nil;
    allNilMeta[@"null1"] = [NSNull null];
    allNilMeta[@"nil2"] = nil;
    allNilMeta[@"null2"] = [NSNull null];
    
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentProduction));
    OCMStub([mockEvent event]).andReturn(@"test_event");
    OCMStub([mockEvent merchant]).andReturn(@"test_merchant");
    OCMStub([mockEvent merchantId]).andReturn(@"123");
    OCMStub([mockEvent product]).andReturnValue(@(0));
    OCMStub([mockEvent taskId]).andReturn(@"task_123");
    OCMStub([mockEvent metaData]).andReturn(allNilMeta);
    
    // Create partial mock to spy on sendEventWithName calls
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not crash and should not include metadata key
        return body != nil && body[@"metaData"] == nil;
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent without metadata
    OCMVerifyAll(mockKnotapi);
}

/**
 * Test environment string conversion
 */
- (void)testEnvironmentStringConversion {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Test sandbox environment
    KnotEvent *mockEvent1 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent1 environment]).andReturnValue(@(EnvironmentSandbox));
    OCMStub([mockEvent1 product]).andReturnValue(@(0));
    
    Knotapi *mockKnotapi1 = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi1 sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"environment"] isEqualToString:@"sandbox"];
    }]]);
    
    [mockKnotapi1 onEventWithEvent:mockEvent1];
    OCMVerifyAll(mockKnotapi1);
    
    // Test development environment
    KnotEvent *mockEvent2 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent2 environment]).andReturnValue(@(EnvironmentDevelopment));
    OCMStub([mockEvent2 product]).andReturnValue(@(0));
    
    Knotapi *mockKnotapi2 = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi2 sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"environment"] isEqualToString:@"development"];
    }]]);
    
    [mockKnotapi2 onEventWithEvent:mockEvent2];
    OCMVerifyAll(mockKnotapi2);
    
    // Test production environment
    KnotEvent *mockEvent3 = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent3 environment]).andReturnValue(@(EnvironmentProduction));
    OCMStub([mockEvent3 product]).andReturnValue(@(0));
    
    Knotapi *mockKnotapi3 = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi3 sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"environment"] isEqualToString:@"production"];
    }]]);
    
    [mockKnotapi3 onEventWithEvent:mockEvent3];
    OCMVerifyAll(mockKnotapi3);
}

@end
