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

@interface KnotapiTests : XCTestCase
@property (nonatomic, strong) Knotapi *knotapi;
@property (nonatomic, strong) id mockKnot;
@end

@implementation KnotapiTests

- (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 - Bridge State Management Tests

- (void)testBridgeInitializesAsActive {
    // Given: A new Knotapi instance
    // When: Initialized
    // Then: Bridge should be active
    XCTAssertTrue(self.knotapi.isBridgeActive, @"Bridge should be active on initialization");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing on initialization");
}

- (void)testOpenResetsBridgeState {
    // Given: Bridge is inactive and closing
    self.knotapi.isBridgeActive = NO;
    self.knotapi.isClosing = YES;
    
    // When: Opening with valid parameters
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox"
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    [self.knotapi open:params];
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Then: Bridge state should be reset
    XCTAssertTrue(self.knotapi.isBridgeActive, @"Bridge should be active after open");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after open");
}

#pragma mark - Close Method Tests

- (void)testCloseSetsClosingFlag {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    XCTAssertFalse(self.knotapi.isClosing);
    
    // When: Closing
    [self.knotapi close];
    
    // Then: Closing flag should be set immediately
    XCTAssertTrue(self.knotapi.isClosing, @"Closing flag should be set immediately");
}

- (void)testClosePreventsMultipleCalls {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // When: Calling close multiple times
    [self.knotapi close];
    [self.knotapi close];
    [self.knotapi close];
    
    // Then: Only first call should be processed
    XCTAssertTrue(self.knotapi.isClosing, @"Closing flag should be set");
    
    // Verify Knot.close is only called once
    OCMVerify([self.mockKnot close]);
}

- (void)testCloseSetsBridgeInactiveAfterCompletion {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // When: Closing
    [self.knotapi close];
    
    // Wait for async completion
    XCTestExpectation *expectation = [self expectationWithDescription:@"Close completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Then: Bridge should be inactive and not closing
    XCTAssertFalse(self.knotapi.isBridgeActive, @"Bridge should be inactive after close");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after close completes");
}

- (void)testCloseResetsIsClosingFlagAfterCompletion {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    XCTAssertFalse(self.knotapi.isClosing);
    
    // When: Closing
    [self.knotapi close];
    
    // Then: Should be marked as closing immediately
    XCTAssertTrue(self.knotapi.isClosing, @"Bridge should be marked as closing immediately");
    
    // Wait for async completion
    XCTestExpectation *expectation = [self expectationWithDescription:@"Close completes and isClosing is reset"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Then: isClosing should be reset to NO after completion
    XCTAssertFalse(self.knotapi.isClosing, @"isClosing should be reset to NO after close completes");
    XCTAssertFalse(self.knotapi.isBridgeActive, @"Bridge should be inactive after close");
}

#pragma mark - Delegate Method Safety Tests

- (void)testOnErrorIgnoresEventsWhenBridgeInactive {
    // Given: Bridge is inactive
    self.knotapi.isBridgeActive = NO;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Error event is received
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnErrorIgnoresEventsWhenBridgeClosing {
    // Given: Bridge is closing (but still active)
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = YES;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Error event is received
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnEventIgnoresEventsWhenBridgeInactive {
    // Given: Bridge is inactive
    self.knotapi.isBridgeActive = NO;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Event is received
    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(@{});
    
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: No event should be sent and productName should not be set
    OCMVerifyAll(mockKnotapi);
    XCTAssertNil(self.knotapi.productName, @"Product name should not be set when bridge is inactive");
}

- (void)testOnEventIgnoresEventsWhenBridgeClosing {
    // Given: Bridge is closing (but still active)
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = YES;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Event is received
    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(@{});
    
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnSuccessIgnoresEventsWhenBridgeInactive {
    // Given: Bridge is inactive
    self.knotapi.isBridgeActive = NO;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Success event is received
    [mockKnotapi onSuccessWithMerchant:@"test_merchant"];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnSuccessIgnoresEventsWhenBridgeClosing {
    // Given: Bridge is closing (but still active)
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = YES;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Success event is received
    [mockKnotapi onSuccessWithMerchant:@"test_merchant"];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnSuccessHandlesNilMerchant {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onSuccess" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Verify the body doesn't contain a merchant key when merchant is nil
        return body != nil && body[@"merchant"] == nil;
    }]]);
    
    // When: Success event is received with nil merchant
    [mockKnotapi onSuccessWithMerchant:nil];
    
    // Then: Should not crash and should send event without merchant key
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnExitIgnoresEventsWhenBridgeInactive {
    // Given: Bridge is inactive
    self.knotapi.isBridgeActive = NO;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Exit event is received
    [mockKnotapi onExit];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnExitIgnoresEventsWhenBridgeClosing {
    // Given: Bridge is closing (but still active)
    self.knotapi.isBridgeActive = YES;
    self.knotapi.isClosing = YES;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMReject([mockKnotapi sendEventWithName:[OCMArg any] body:[OCMArg any]]);
    
    // When: Exit event is received
    [mockKnotapi onExit];
    
    // Then: No event should be sent (sendEventWithName should not be called)
    OCMVerifyAll(mockKnotapi);
}

#pragma mark - Delegate Method Active Bridge Tests

- (void)testOnErrorSendsEventWhenBridgeActive {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create a partial mock to spy on sendEventWithName:body:
    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: Event should be sent with correct error details
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnEventSendsEventWhenBridgeActive {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create a partial mock to spy on sendEventWithName:body:
    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"];
    }]]);
    
    // When: Event is received
    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(@{});
    
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Event should be sent with correct details and productName should be set
    OCMVerifyAll(mockKnotapi);
    XCTAssertEqualObjects(self.knotapi.productName, @"card_switcher", @"Product name should be set");
}

- (void)testOnSuccessSendsEventWhenBridgeActive {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create a partial mock to spy on sendEventWithName:body:
    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: Event should be sent with correct merchant
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnExitSendsEventWhenBridgeActive {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create a partial mock to spy on sendEventWithName:body:
    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: Event should be sent
    OCMVerifyAll(mockKnotapi);
}

#pragma mark - Exception Handling Tests

- (void)testCloseHandlesExceptions {
    // Given: Knot.close throws an exception
    OCMStub([self.mockKnot close]).andThrow([NSException exceptionWithName:@"TestException" reason:@"Test" userInfo:nil]);
    
    // When: Closing
    // Then: Should not crash
    XCTAssertNoThrow([self.knotapi close], @"Close should handle exceptions gracefully");
    
    // Wait for async completion
    XCTestExpectation *expectation = [self expectationWithDescription:@"Close with exception completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Bridge should still be marked as inactive
    XCTAssertFalse(self.knotapi.isBridgeActive, @"Bridge should be inactive even after exception");
}

#pragma mark - Dealloc Tests

- (void)testDeallocSetsBridgeInactive {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    XCTAssertFalse(self.knotapi.isClosing);
    
    // When: Bridge is deallocated
    self.knotapi = nil;
    
    // Then: No crash should occur
    // (We can't easily test the dealloc method directly, but we can verify it doesn't crash)
}

#pragma mark - Nil Handling Tests

- (void)testOpenHandlesNilParams {
    // Given: nil params dictionary
    // When: Opening with nil params
    // Then: Should not crash and should log error
    XCTAssertNoThrow([self.knotapi open:nil], @"Open should handle nil params gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with nil params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesInvalidParamsType {
    // Given: Invalid params (not a dictionary)
    NSArray *invalidParams = @[@"not", @"a", @"dictionary"];
    
    // When: Opening with invalid params
    // Then: Should not crash and should log error
    XCTAssertNoThrow([self.knotapi open:(NSDictionary *)invalidParams], @"Open should handle invalid params gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with invalid params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesMissingRequiredParameters {
    // Given: Params missing required sessionId and clientId
    NSDictionary *incompleteParams = @{
        @"environment": @"sandbox"
    };
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"errorCode"] isEqualToString:@"INVALID_PARAMS"] &&
               [body[@"errorDescription"] isEqualToString:@"Missing required parameters (sessionId or clientId)"];
    }]]);
    
    // When: Opening with incomplete params
    // Then: Should not crash and should send error event
    XCTAssertNoThrow([mockKnotapi open:incompleteParams], @"Open should handle missing required params gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with missing params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Verify error event was sent
    OCMVerifyAll(mockKnotapi);
}

- (void)testOpenHandlesNilCustomerConfig {
    // Given: Params with nil customerConfig
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox",
        @"customerConfig": [NSNull null]
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with nil customerConfig
    // Then: Should not crash
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle nil customerConfig gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with nil customerConfig completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesInvalidCustomerConfigType {
    // Given: Params with invalid customerConfig type
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox",
        @"customerConfig": @"not_a_dictionary"
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with invalid customerConfig
    // Then: Should not crash
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle invalid customerConfig gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with invalid customerConfig completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesNilEnvironmentString {
    // Given: Params with nil environment string
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": [NSNull null]
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with nil environment
    // Then: Should default to production environment and not crash
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle nil environment gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with nil environment completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOnEventHandlesNilEventProperties {
    // Given: Bridge is active and event with nil properties
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    KnotEvent *mockEvent = OCMClassMock([KnotEvent class]);
    OCMStub([mockEvent environment]).andReturnValue(@(EnvironmentSandbox));
    OCMStub([mockEvent event]).andReturn(nil); // nil event
    OCMStub([mockEvent merchant]).andReturn(nil); // nil merchant
    OCMStub([mockEvent merchantId]).andReturn(nil); // nil merchantId
    OCMStub([mockEvent product]).andReturnValue(@(0));
    OCMStub([mockEvent taskId]).andReturn(nil); // nil taskId
    OCMStub([mockEvent metaData]).andReturn(nil); // nil metaData
    
    // Create a partial mock to spy on sendEventWithName:body:
    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[@"taskId"] == nil &&
               body[@"metaData"] == nil;
    }]]);
    
    // When: Event with nil properties is received
    // Then: Should not crash and should send event with safe body
    XCTAssertNoThrow([mockKnotapi onEventWithEvent:mockEvent], @"onEvent should handle nil properties gracefully");
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnErrorHandlesNilErrorCodeAndDescription {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should have valid error code and description for known error types
        return body != nil && 
               body[@"errorCode"] != nil && 
               body[@"errorDescription"] != nil;
    }]]);
    
    // When: Error event is received (this should always have valid error code/description)
    [mockKnotapi onErrorWithError:KnotErrorInvalidSession];
    
    // Then: Should send event with valid error details
    OCMVerifyAll(mockKnotapi);
}

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

- (void)testOnExitHandlesNilProductName {
    // Given: Bridge is active with nil productName
    XCTAssertTrue(self.knotapi.isBridgeActive);
    self.knotapi.productName = nil;
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onExit" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should not include product key when productName is nil
        return body != nil && body[@"product"] == nil;
    }]]);
    
    // When: Exit event is received
    [mockKnotapi onExit];
    
    // Then: Should send event without product key
    OCMVerifyAll(mockKnotapi);
}

- (void)testOnEventHandlesNilProductName {
    // Given: Bridge is active with nil productName
    XCTAssertTrue(self.knotapi.isBridgeActive);
    self.knotapi.productName = nil;
    
    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(@{});
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onEvent" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        // Should include product key after setting it from event
        return body != nil && [body[@"product"] isEqualToString:@"card_switcher"];
    }]]);
    
    // When: Event is received
    [mockKnotapi onEventWithEvent:mockEvent];
    
    // Then: Should set productName and include it in event body
    OCMVerifyAll(mockKnotapi);
    XCTAssertEqualObjects(self.knotapi.productName, @"card_switcher", @"Product name should be set from event");
}

#pragma mark - Type Sanitization Tests

- (void)testOpenHandlesNSNullParameters {
    // Given: Params with NSNull values
    NSDictionary *params = @{
        @"sessionId": [NSNull null],
        @"clientId": [NSNull null],
        @"environment": [NSNull null],
        @"entryPoint": [NSNull null],
        @"merchantIds": [NSNull null]
    };
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"errorCode"] isEqualToString:@"INVALID_PARAMS"] &&
               [body[@"errorDescription"] isEqualToString:@"Missing required parameters (sessionId or clientId)"];
    }]]);
    
    // When: Opening with NSNull params
    // Then: Should not crash and should send error event
    XCTAssertNoThrow([mockKnotapi open:params], @"Open should handle NSNull params gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with NSNull params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Verify error event was sent
    OCMVerifyAll(mockKnotapi);
}

- (void)testOpenHandlesWrongTypeParameters {
    // Given: Params with wrong types
    NSDictionary *params = @{
        @"sessionId": @123,  // NSNumber instead of NSString
        @"clientId": @456,   // NSNumber instead of NSString
        @"environment": @789, // NSNumber instead of NSString
        @"entryPoint": @{@"key": @"value"}, // NSDictionary instead of NSString
        @"merchantIds": @"not_an_array" // NSString instead of NSArray
    };
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"errorCode"] isEqualToString:@"INVALID_PARAMS"] &&
               [body[@"errorDescription"] isEqualToString:@"Missing required parameters (sessionId or clientId)"];
    }]]);
    
    // When: Opening with wrong type params
    // Then: Should not crash and should send error event
    XCTAssertNoThrow([mockKnotapi open:params], @"Open should handle wrong type params gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with wrong type params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Verify error event was sent
    OCMVerifyAll(mockKnotapi);
}

- (void)testOpenHandlesEmptyStringParameters {
    // Given: Params with empty strings
    NSDictionary *params = @{
        @"sessionId": @"",
        @"clientId": @"",
        @"environment": @"sandbox"
    };
    
    // Create a partial mock to spy on sendEventWithName:body:
    Knotapi *mockKnotapi = OCMPartialMock(self.knotapi);
    OCMExpect([mockKnotapi sendEventWithName:@"knot:onError" body:[OCMArg checkWithBlock:^BOOL(NSDictionary *body) {
        return [body[@"errorCode"] isEqualToString:@"INVALID_PARAMS"] &&
               [body[@"errorDescription"] isEqualToString:@"Missing required parameters (sessionId or clientId)"];
    }]]);
    
    // When: Opening with empty string params
    // Then: Should not crash and should send error event
    XCTAssertNoThrow([mockKnotapi open:params], @"Open should handle empty string params gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with empty string params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Verify error event was sent
    OCMVerifyAll(mockKnotapi);
}

- (void)testOpenHandlesMixedTypeMerchantIds {
    // Given: Params with mixed types in merchantIds array
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox",
        @"merchantIds": @[@123, @"invalid", @456, [NSNull null], @789] // Mixed valid/invalid types
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with mixed type merchantIds
    // Then: Should not crash and should filter out invalid types
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle mixed type merchantIds gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with mixed type merchantIds completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesValidParametersWithTypeSanitization {
    // Given: Valid params that should work after type sanitization
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox",
        @"entryPoint": @"test-entry",
        @"merchantIds": @[@123, @456, @789]
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with valid params
    // Then: Should not crash and should proceed normally
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle valid params successfully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with valid params completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesNSNullEnvironmentAndEntryPoint {
    // Given: Params with NSNull for environment and entryPoint
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": [NSNull null],
        @"entryPoint": [NSNull null]
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with NSNull environment and entryPoint
    // Then: Should not crash and should default to production environment
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle NSNull environment and entryPoint gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with NSNull environment completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesComprehensiveNSNullValues {
    // Given: Params with NSNull for all optional parameters
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": [NSNull null],
        @"entryPoint": [NSNull null],
        @"merchantIds": [NSNull null],
        @"useCategories": [NSNull null],
        @"useSearch": [NSNull null],
        @"customerConfig": [NSNull null]
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with comprehensive NSNull values
    // Then: Should not crash and should use default values
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle comprehensive NSNull values gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with comprehensive NSNull values completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

- (void)testOpenHandlesNSNullInCustomerConfig {
    // Given: Params with NSNull values inside customerConfig
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"customerConfig": @{
            @"cardName": [NSNull null],
            @"customerName": [NSNull null],
            @"logoId": [NSNull null]
        }
    };
    
    // Mock the Knot.openWithConfiguration call
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Opening with NSNull values in customerConfig
    // Then: Should not crash and should handle NSNull gracefully
    XCTAssertNoThrow([self.knotapi open:params], @"Open should handle NSNull in customerConfig gracefully");
    
    // Wait for async dispatch
    XCTestExpectation *expectation = [self expectationWithDescription:@"Open with NSNull customerConfig completes"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

#pragma mark - Integration Tests

- (void)testOpenCloseOpenCycle {
    // Given: Initial state
    XCTAssertTrue(self.knotapi.isBridgeActive, @"Bridge should be active initially");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing initially");
    XCTAssertNil(self.knotapi.productName, @"Product name should be nil initially");
    
    // When: Opening
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox"
    };
    
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    [self.knotapi open:params];
    
    // Then: Bridge should be active and reset after open
    XCTAssertTrue(self.knotapi.isBridgeActive, @"Bridge should be active after open");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after open");
    XCTAssertNil(self.knotapi.productName, @"Product name should be reset to nil after open");
    
    // When: Closing
    [self.knotapi close];
    
    // Then: Bridge should be marked as closing initially
    XCTAssertTrue(self.knotapi.isClosing, @"Bridge should be marked as closing immediately");
    
    // Wait for close to complete (using proper async handling)
    XCTestExpectation *closeExpectation = [self expectationWithDescription:@"Close completes"];
    dispatch_async(dispatch_get_main_queue(), ^{
        // Give the close operation time to complete
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [closeExpectation fulfill];
        });
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    // Then: Bridge should be inactive and not closing after close completes
    XCTAssertFalse(self.knotapi.isBridgeActive, @"Bridge should be inactive after close");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after close completes");
    
    // When: Opening again
    [self.knotapi open:params];
    
    // Then: Bridge should be active and reset again after second open
    XCTAssertTrue(self.knotapi.isBridgeActive, @"Bridge should be active after second open");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after second open");
    XCTAssertNil(self.knotapi.productName, @"Product name should be reset to nil after second open");
}

- (void)testRapidOpenCloseCycle {
    // Given: Initial state
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    NSDictionary *params = @{
        @"sessionId": @"test-session",
        @"clientId": @"test-client",
        @"environment": @"sandbox"
    };
    
    OCMStub([self.mockKnot openWithConfiguration:[OCMArg any] delegate:[OCMArg any]]);
    
    // When: Rapid open/close/open cycle
    [self.knotapi open:params];
    [self.knotapi close];
    [self.knotapi open:params];
    
    // Then: Bridge should be in correct final state
    XCTAssertTrue(self.knotapi.isBridgeActive, @"Bridge should be active after rapid cycle");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after rapid cycle");
}

- (void)testMultipleCloseCallsInCycle {
    // Given: Bridge is active
    XCTAssertTrue(self.knotapi.isBridgeActive);
    
    // When: Multiple close calls
    [self.knotapi close];
    [self.knotapi close];
    [self.knotapi close];
    
    // Then: Only first close should be processed
    XCTAssertTrue(self.knotapi.isClosing, @"Bridge should be closing after first close call");
    
    // Wait for close to complete
    XCTestExpectation *closeExpectation = [self expectationWithDescription:@"Close completes"];
    dispatch_async(dispatch_get_main_queue(), ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [closeExpectation fulfill];
        });
    });
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
    
    XCTAssertFalse(self.knotapi.isBridgeActive, @"Bridge should be inactive after multiple close calls");
    XCTAssertFalse(self.knotapi.isClosing, @"Bridge should not be closing after close completes");
}

@end
