/*
 *
 *    Copyright (c) 2022 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

// module headers
#import <Matter/Matter.h>

#import "MTRDeviceTestDelegate.h"
#import "MTRErrorTestUtils.h"
#import "MTRTestCase+ServerAppRunner.h"
#import "MTRTestCase.h"
#import "MTRTestKeys.h"
#import "MTRTestResetCommissioneeHelper.h"
#import "MTRTestStorage.h"

// system dependencies
#import <XCTest/XCTest.h>

#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR
#define ENABLE_OTA_TESTS 0
#else
#define ENABLE_OTA_TESTS 1
#endif

// TODO: Disable test005_DoBDXTransferAllowUpdateRequest,
// test006_DoBDXTransferWithTwoOTARequesters and
// test007_DoBDXTransferIncrementalOtaUpdate until PR #26040 is merged.
// Currently the poll interval causes delays in the BDX transfer and
// results in the test taking a long time.
#ifdef ENABLE_REAL_OTA_UPDATE_TESTS
#undef ENABLE_REAL_OTA_UPDATE_TESTS
#endif

#if ENABLE_OTA_TESTS

static const uint16_t kPairingTimeoutInSeconds = 30;
static const uint16_t kTimeoutInSeconds = 3;
static const uint16_t kTimeoutWithUpdateInSeconds = 60;
static const uint64_t kDeviceId1 = 0x12341234;
static const uint64_t kDeviceId2 = 0x12341235;
#ifdef ENABLE_REAL_OTA_UPDATE_TESTS
static const uint64_t kDeviceId3 = 0x12341236;
#endif // ENABLE_REAL_OTA_UPDATE_TESTS
// NOTE: These onboarding payloads are for the chip-ota-requestor-app, not chip-all-clusters-app
static NSString * kOnboardingPayload1 = @"MT:-24J0SO527K10648G00"; // Discriminator: 1111
static NSString * kOnboardingPayload2 = @"MT:-24J0AFN00L10648G00"; // Discriminator: 1112
#ifdef ENABLE_REAL_OTA_UPDATE_TESTS
static NSString * kOnboardingPayload3 = @"MT:-24J0IRV01L10648G00"; // Discriminator: 1113
#endif // ENABLE_REAL_OTA_UPDATE_TESTS

static const uint16_t kLocalPort = 5541;
static const uint16_t kTestVendorId = 0xFFF1u;
static const uint16_t kOTAProviderEndpointId = 0;

static MTRDeviceController * sController = nil;

static NSNumber * kUpdatedSoftwareVersion_5 = @5;

static NSString * kUpdatedSoftwareVersionString_5 = @"5.0";

static NSNumber * kUpdatedSoftwareVersion_10 = @10;

static NSString * kUpdatedSoftwareVersionString_10 = @"10.0";

@interface MTROTAProviderTests : MTRTestCase
- (NSString *)createImageFromRawImage:(NSString *)rawImage withVersion:(NSNumber *)version;
- (MTRDevice *)commissionDeviceWithPayload:(NSString *)payloadString nodeID:(NSNumber *)nodeID;
@end

@interface MTROTARequestorAppRunner : NSObject
@property (nonatomic, copy) NSString * downloadFilePath;

- (instancetype)initWithPayload:(NSString *)payload testcase:(MTROTAProviderTests *)testcase;
- (MTRDevice *)commissionWithNodeID:(NSNumber *)nodeID;
@end

@implementation MTROTARequestorAppRunner {
    MTROTAProviderTests * _testcase;
    NSString * _payload;
}

- (MTRDevice *)commissionWithNodeID:(NSNumber *)nodeID
{
    return [_testcase commissionDeviceWithPayload:_payload nodeID:nodeID];
}

- (instancetype)initWithPayload:(NSString *)payload testcase:(MTROTAProviderTests *)testcase
{
    __auto_type * downloadFilePath = [NSString stringWithFormat:@"/tmp/chip-ota-requestor-downloaded-image%u", [MTROTAProviderTests nextUniqueIndex]];
    __auto_type * extraArguments = @[
        @"--otaDownloadPath",
        downloadFilePath,
        @"--autoApplyImage",
    ];

    BOOL started = [testcase startAppWithName:@"ota-requestor" arguments:extraArguments payload:payload];
    if (!started) {
        return nil;
    }

    _testcase = testcase;
    _payload = payload;
    _downloadFilePath = downloadFilePath;

    return self;
}

@end

@interface MTROTAProviderTestControllerDelegate : NSObject <MTRDeviceControllerDelegate>
@property (nonatomic, readonly) XCTestExpectation * expectation;
@property (nonatomic, readonly) NSNumber * commissioneeNodeID;
@end

@implementation MTROTAProviderTestControllerDelegate
- (id)initWithExpectation:(XCTestExpectation *)expectation commissioneeNodeID:(NSNumber *)nodeID
{
    self = [super init];
    if (self) {
        _expectation = expectation;
        _commissioneeNodeID = nodeID;
    }
    return self;
}

- (void)controller:(MTRDeviceController *)controller commissioningSessionEstablishmentDone:(NSError * _Nullable)error
{
    XCTAssertEqual(error.code, 0);

    NSError * commissionError = nil;
    [sController commissionNodeWithID:self.commissioneeNodeID
                  commissioningParams:[[MTRCommissioningParameters alloc] init]
                                error:&commissionError];
    XCTAssertNil(commissionError);

    // Keep waiting for onCommissioningComplete
}

- (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSError *)error
{
    XCTAssertEqual(error.code, 0);
    [_expectation fulfill];
    _expectation = nil;
}

@end

typedef void (^QueryImageCompletion)(
    MTROTASoftwareUpdateProviderClusterQueryImageResponseParams * _Nullable data, NSError * _Nullable error);
typedef void (^ApplyUpdateRequestCompletion)(
    MTROTASoftwareUpdateProviderClusterApplyUpdateResponseParams * _Nullable data, NSError * _Nullable error);
typedef void (^BlockQueryCompletion)(NSData * _Nullable data, BOOL isEOF);

typedef void (^QueryImageHandler)(NSNumber * nodeID, MTRDeviceController * controller,
    MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion);
typedef void (^ApplyUpdateRequestHandler)(NSNumber * nodeID, MTRDeviceController * controller,
    MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion);
typedef void (^NotifyUpdateAppliedHandler)(NSNumber * nodeID, MTRDeviceController * controller,
    MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams * params, MTRStatusCompletion completion);
typedef void (^BDXTransferBeginHandler)(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator,
    NSNumber * offset, MTRStatusCompletion completion);
typedef void (^BDXQueryHandler)(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize, NSNumber * blockIndex,
    NSNumber * bytesToSkip, BlockQueryCompletion completion);
typedef void (^BDXTransferEndHandler)(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error);

@interface MTROTAProviderDelegateImpl : NSObject <MTROTAProviderDelegate>
@property (nonatomic, nullable) QueryImageHandler queryImageHandler;
@property (nonatomic, nullable) ApplyUpdateRequestHandler applyUpdateRequestHandler;
@property (nonatomic, nullable) NotifyUpdateAppliedHandler notifyUpdateAppliedHandler;
@property (nonatomic, nullable) BDXTransferBeginHandler transferBeginHandler;
@property (nonatomic, nullable) BDXQueryHandler blockQueryHandler;
@property (nonatomic, nullable) BDXTransferEndHandler transferEndHandler;
@end

@implementation MTROTAProviderDelegateImpl
- (void)handleQueryImageForNodeID:(NSNumber *)nodeID
                       controller:(MTRDeviceController *)controller
                           params:(MTROTASoftwareUpdateProviderClusterQueryImageParams *)params
                       completion:(QueryImageCompletion)completion
{
    XCTAssertEqual(controller, sController);

    if (self.queryImageHandler) {
        self.queryImageHandler(nodeID, controller, params, completion);
    } else {
        XCTFail(@"Unexpected attempt to query for an image");
        [self respondNotAvailableWithCompletion:completion];
    }
}

- (void)handleApplyUpdateRequestForNodeID:(NSNumber *)nodeID
                               controller:(MTRDeviceController *)controller
                                   params:(MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams *)params
                               completion:(ApplyUpdateRequestCompletion)completion
{
    XCTAssertEqual(controller, sController);

    if (self.applyUpdateRequestHandler) {
        self.applyUpdateRequestHandler(nodeID, controller, params, completion);
    } else {
        XCTFail(@"Unexpected attempt to apply an update");
        [self respondWithErrorToApplyUpdateRequestWithCompletion:completion];
    }
}

- (void)handleNotifyUpdateAppliedForNodeID:(NSNumber *)nodeID
                                controller:(MTRDeviceController *)controller
                                    params:(MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams *)params
                                completion:(MTRStatusCompletion)completion
{
    XCTAssertEqual(controller, sController);

    if (self.notifyUpdateAppliedHandler) {
        self.notifyUpdateAppliedHandler(nodeID, controller, params, completion);
    } else {
        XCTFail(@"Unexpected update application");
        [self respondErrorWithCompletion:completion];
    }
}

- (void)handleBDXTransferSessionBeginForNodeID:(NSNumber *)nodeID
                                    controller:(MTRDeviceController *)controller
                                fileDesignator:(NSString *)fileDesignator
                                        offset:(NSNumber *)offset
                                    completion:(MTRStatusCompletion)completion
{
    XCTAssertEqual(controller, sController);

    if (self.transferBeginHandler) {
        self.transferBeginHandler(nodeID, controller, fileDesignator, offset, completion);
    } else {
        XCTFail(@"Unexpected attempt to begin BDX transfer");
        [self respondErrorWithCompletion:completion];
    }
}

- (void)handleBDXQueryForNodeID:(NSNumber *)nodeID
                     controller:(MTRDeviceController *)controller
                      blockSize:(NSNumber *)blockSize
                     blockIndex:(NSNumber *)blockIndex
                    bytesToSkip:(NSNumber *)bytesToSkip
                     completion:(BlockQueryCompletion)completion
{
    XCTAssertEqual(controller, sController);

    if (self.blockQueryHandler) {
        self.blockQueryHandler(nodeID, controller, blockSize, blockIndex, bytesToSkip, completion);
    } else {
        XCTFail(@"Unexpected attempt to get BDX block");
        completion(nil, YES);
    }
}

- (void)handleBDXTransferSessionEndForNodeID:(NSNumber *)nodeID
                                  controller:(MTRDeviceController *)controller
                                       error:(NSError * _Nullable)error
{
    if (self.transferEndHandler) {
        self.transferEndHandler(nodeID, controller, error);
    } else {
        XCTFail(@"Unexpected end of BDX transfer");
    }
}

- (void)respondNotAvailableWithCompletion:(QueryImageCompletion)completion
{
    __auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init];
    responseParams.status = @(MTROTASoftwareUpdateProviderStatusNotAvailable);
    completion(responseParams, nil);
}

- (void)respondBusyWithDelay:(NSNumber *)delay completion:(QueryImageCompletion)completion
{
    __auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init];
    responseParams.status = @(MTROTASoftwareUpdateProviderStatusBusy);
    responseParams.delayedActionTime = delay;
    completion(responseParams, nil);
}

- (void)respondAvailableWithDelay:(NSNumber *)delay
                              uri:(NSString *)uri
                      updateToken:(NSData *)updateToken
                  softwareVersion:(NSNumber *)softwareVersion
            softwareVersionString:(NSString *)softwareVersionString
                       completion:(QueryImageCompletion)completion
{
    __auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init];
    responseParams.status = @(MTROTASoftwareUpdateProviderStatusUpdateAvailable);
    responseParams.delayedActionTime = delay;
    responseParams.imageURI = uri;
    // TODO: Figure out whether we need better
    // SoftwareVersion/SoftwareVersionString/UpdateToken bits.
    responseParams.softwareVersion = softwareVersion;
    responseParams.softwareVersionString = softwareVersionString;
    responseParams.updateToken = updateToken;
    completion(responseParams, nil);
}

- (void)respondWithErrorToApplyUpdateRequestWithCompletion:(ApplyUpdateRequestCompletion)completion
{
    [self respondErrorWithCompletion:^(NSError * _Nullable error) {
        completion(nil, error);
    }];
}

- (void)respondToApplyUpdateRequestWithAction:(MTROTASoftwareUpdateProviderApplyUpdateAction)action
                                   completion:(ApplyUpdateRequestCompletion)completion
{
    __auto_type * params = [[MTROTASoftwareUpdateProviderClusterApplyUpdateResponseParams alloc] init];
    params.action = @(action);
    params.delayedActionTime = @(0);
    completion(params, nil);
}

- (void)respondErrorWithCompletion:(MTRStatusCompletion)completion
{
    [self respondErrorWithCode:MTRErrorCodeGeneralError completion:completion];
}

- (void)respondErrorWithCode:(MTRErrorCode)code completion:(MTRStatusCompletion)completion
{
    [self respondError:[NSError errorWithDomain:MTRErrorDomain code:code userInfo:nil] completion:completion];
}

- (void)respondError:(NSError *)error completion:(MTRStatusCompletion)completion
{
    completion(error);
}

- (void)respondSuccess:(MTRStatusCompletion)completion
{
    completion(nil);
}

- (NSData *)generateUpdateToken
{
    const size_t dataSize = 16;
    const size_t randomBytesAtOnce = sizeof(uint32_t);
    XCTAssertEqual(dataSize % randomBytesAtOnce, 0);
    NSMutableData * data = [NSMutableData dataWithCapacity:16];
    for (unsigned i = 0; i < dataSize / randomBytesAtOnce; ++i) {
        uint32_t randomBytes = arc4random();
        [data appendBytes:&randomBytes length:randomBytesAtOnce];
    }
    return [NSData dataWithData:data];
}

@end

static MTROTAProviderDelegateImpl * sOTAProviderDelegate;

/**
 * Helper that, given a raw image, creates an image-with-header from it, drives
 * a BDX transfer, etc.
 */
@interface MTROTAProviderTransferChecker : NSObject

- (instancetype)initWithRawImagePath:(NSString *)rawImagePath
            otaImageDownloadFilePath:(NSString *)otaImageDownloadFilePath
                              nodeID:(NSNumber *)nodeID
                     softwareVersion:(NSNumber *)softwareVersion
               softwareVersionString:(NSString *)softwareVersionString
                   applyUpdateAction:(MTROTASoftwareUpdateProviderApplyUpdateAction)applyUpdateAction
                            testcase:(XCTestCase *)testcase;

@property (nonatomic, readonly) XCTestExpectation * queryExpectation;
@property (nonatomic, readonly) XCTestExpectation * bdxBeginExpectation;
@property (nonatomic, readonly) XCTestExpectation * bdxQueryExpectation;
@property (nonatomic, readonly) XCTestExpectation * bdxEndExpectation;
@property (nonatomic, readonly) XCTestExpectation * applyUpdateRequestExpectation;
@property (nonatomic, readonly) XCTestExpectation * notifyUpdateAppliedExpectation;
@end

@implementation MTROTAProviderTransferChecker

- (instancetype)initWithRawImagePath:(NSString *)rawImagePath
            otaImageDownloadFilePath:(NSString *)otaImageDownloadFilePath
                              nodeID:(NSNumber *)nodeID
                     softwareVersion:(NSNumber *)softwareVersion
               softwareVersionString:(NSString *)softwareVersionString
                   applyUpdateAction:(MTROTASoftwareUpdateProviderApplyUpdateAction)applyUpdateAction
                            testcase:(MTROTAProviderTests *)testcase
{
    if (!(self = [super init])) {
        return nil;
    }

    _queryExpectation = [testcase expectationWithDescription:@"handleQueryImageForNodeID called"];
    _bdxBeginExpectation = [testcase expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called"];
    _bdxQueryExpectation = [testcase expectationWithDescription:@"handleBDXQueryForNodeID called"];
    _bdxEndExpectation = [testcase expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called"];
    _applyUpdateRequestExpectation = [testcase expectationWithDescription:@"handleApplyUpdateRequestForNodeID called"];
    _notifyUpdateAppliedExpectation = [testcase expectationWithDescription:@"handleNotifyUpdateAppliedForNodeID called"];

    NSString * imagePath = [testcase createImageFromRawImage:rawImagePath withVersion:softwareVersion];

    NSData * updateToken = [sOTAProviderDelegate generateUpdateToken];

    __block NSFileHandle * readHandle;
    __block uint64_t imageSize;
    __block uint32_t lastBlockIndex = UINT32_MAX;

    sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        XCTAssertEqualObjects(nodeID, nodeID);
        XCTAssertEqual(controller, sController);

        sOTAProviderDelegate.queryImageHandler = nil;
        [sOTAProviderDelegate respondAvailableWithDelay:@(0)
                                                    uri:imagePath
                                            updateToken:updateToken
                                        softwareVersion:softwareVersion
                                  softwareVersionString:softwareVersionString
                                             completion:completion];
        [self.queryExpectation fulfill];
    };
    sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator,
        NSNumber * offset, MTRStatusCompletion completion) {
        XCTAssertEqualObjects(nodeID, nodeID);
        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(fileDesignator, imagePath);
        XCTAssertEqualObjects(offset, @(0));

        readHandle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator];
        XCTAssertNotNil(readHandle);

        NSError * endSeekError;
        XCTAssertTrue([readHandle seekToEndReturningOffset:&imageSize error:&endSeekError]);
        XCTAssertNil(endSeekError);

        sOTAProviderDelegate.transferBeginHandler = nil;
        [sOTAProviderDelegate respondSuccess:completion];
        [self.bdxBeginExpectation fulfill];
    };
    sOTAProviderDelegate.blockQueryHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize,
        NSNumber * blockIndex, NSNumber * bytesToSkip, BlockQueryCompletion completion) {
        XCTAssertEqualObjects(nodeID, nodeID);
        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(blockSize, @(1024)); // Seems to always be 1024.
        XCTAssertEqualObjects(blockIndex, @(lastBlockIndex + 1));
        XCTAssertEqualObjects(bytesToSkip, @(0)); // Don't expect to see skips here.
        // Make sure we actually end up with multiple blocks.
        XCTAssertTrue(blockSize.unsignedLongLongValue < imageSize);

        XCTAssertNotNil(readHandle);
        uint64_t offset = blockSize.unsignedLongLongValue * blockIndex.unsignedLongLongValue;
        NSError * seekError = nil;
        [readHandle seekToOffset:offset error:&seekError];
        XCTAssertNil(seekError);

        NSError * readError = nil;
        NSData * data = [readHandle readDataUpToLength:blockSize.unsignedLongValue error:&readError];
        XCTAssertNil(readError);
        XCTAssertNotNil(data);

        BOOL isEOF = offset + blockSize.unsignedLongValue >= imageSize;

        ++lastBlockIndex;

        if (isEOF) {
            sOTAProviderDelegate.blockQueryHandler = nil;
        }

        completion(data, isEOF);

        if (isEOF) {
            [self.bdxQueryExpectation fulfill];
        }
    };
    sOTAProviderDelegate.transferEndHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) {
        XCTAssertEqualObjects(nodeID, nodeID);
        XCTAssertEqual(controller, sController);
        XCTAssertNil(error);

        sOTAProviderDelegate.transferEndHandler = nil;
        [self.bdxEndExpectation fulfill];
    };
    sOTAProviderDelegate.applyUpdateRequestHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion) {
        XCTAssertEqualObjects(nodeID, nodeID);
        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(params.updateToken, updateToken);
        XCTAssertEqualObjects(params.newVersion, softwareVersion);

        XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:rawImagePath andPath:otaImageDownloadFilePath]);

        sOTAProviderDelegate.applyUpdateRequestHandler = nil;
        [sOTAProviderDelegate respondToApplyUpdateRequestWithAction:applyUpdateAction completion:completion];
        [self.applyUpdateRequestExpectation fulfill];
    };

    if (applyUpdateAction == MTROTASoftwareUpdateProviderApplyUpdateActionProceed) {
        sOTAProviderDelegate.notifyUpdateAppliedHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
            MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams * params, MTRStatusCompletion completion) {
            XCTAssertEqualObjects(nodeID, nodeID);
            XCTAssertEqual(controller, sController);
            XCTAssertEqualObjects(params.updateToken, updateToken);
            XCTAssertEqualObjects(params.softwareVersion, softwareVersion);

            sOTAProviderDelegate.notifyUpdateAppliedHandler = nil;
            [sOTAProviderDelegate respondSuccess:completion];
            [self.notifyUpdateAppliedExpectation fulfill];
        };
    }

    return self;
}
@end

static BOOL sStackInitRan = NO;
static BOOL sNeedsStackShutdown = YES;

@implementation MTROTAProviderTests {
    NSMutableSet<NSNumber *> * _commissionedNodeIDs;
}

+ (void)tearDown
{
    // Global teardown, runs once
    if (sNeedsStackShutdown) {
        // We don't need to worry about ResetCommissionee.  If we get here,
        // we're running only one of our test methods (using
        // -only-testing:MatterTests/MTROTAProviderTests/testMethodName), since
        // we did not run test999_TearDown.
        [self shutdownStack];
    }
}

- (void)setUp
{
    // Per-test setup, runs before each test.
    [super setUp];
    [self setContinueAfterFailure:NO];

    if (sStackInitRan == NO) {
        [self initStack];
    }

    _commissionedNodeIDs = [[NSMutableSet alloc] init];

    XCTAssertNil(sOTAProviderDelegate.queryImageHandler);
    XCTAssertNil(sOTAProviderDelegate.applyUpdateRequestHandler);
    XCTAssertNil(sOTAProviderDelegate.notifyUpdateAppliedHandler);
    XCTAssertNil(sOTAProviderDelegate.transferBeginHandler);
    XCTAssertNil(sOTAProviderDelegate.blockQueryHandler);
    XCTAssertNil(sOTAProviderDelegate.transferEndHandler);

    // Start a new controller for each test, with a new fabric.  Otherwise
    // reusing the same node id for our commissionee devices will cause us to
    // try to reuse sessions in ways that fail.
    __auto_type * testKeys = [[MTRTestKeys alloc] init];
    XCTAssertNotNil(testKeys);

    __auto_type * params = [[MTRDeviceControllerStartupParams alloc] initWithIPK:testKeys.ipk fabricID:@(1) nocSigner:testKeys];
    params.vendorID = @(kTestVendorId);

    MTRDeviceController * controller = [[MTRDeviceControllerFactory sharedInstance] createControllerOnNewFabric:params error:nil];
    XCTAssertNotNil(controller);
    XCTAssertTrue([controller isRunning]);

    sController = controller;
}

- (void)tearDown
{
    for (NSNumber * nodeID in _commissionedNodeIDs) {
        __auto_type * device = [MTRBaseDevice deviceWithNodeID:nodeID controller:sController];
        ResetCommissionee(device, dispatch_get_main_queue(), self, kTimeoutInSeconds);
    }

    if (sController != nil) {
        [sController shutdown];
        XCTAssertFalse([sController isRunning]);
        sController = nil;
    }

    // Per-test teardown, runs after each test.
    [super tearDown];

    // Reset all handlers so tests don't interfere with each other.
    sOTAProviderDelegate.queryImageHandler = nil;
    sOTAProviderDelegate.applyUpdateRequestHandler = nil;
    sOTAProviderDelegate.notifyUpdateAppliedHandler = nil;
    sOTAProviderDelegate.transferBeginHandler = nil;
    sOTAProviderDelegate.blockQueryHandler = nil;
    sOTAProviderDelegate.transferEndHandler = nil;
}

- (MTRDevice *)commissionDeviceWithPayload:(NSString *)payloadString nodeID:(NSNumber *)nodeID
{
    XCTestExpectation * expectation =
        [self expectationWithDescription:[NSString stringWithFormat:@"Commissioning Complete for %@", nodeID]];
    __auto_type * deviceControllerDelegate = [[MTROTAProviderTestControllerDelegate alloc] initWithExpectation:expectation
                                                                                            commissioneeNodeID:nodeID];
    dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip.device_controller_delegate", DISPATCH_QUEUE_SERIAL);

    [sController setDeviceControllerDelegate:deviceControllerDelegate queue:callbackQueue];

    NSError * error;
    __auto_type * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:payloadString error:&error];
    XCTAssertNotNil(payload);
    XCTAssertNil(error);

    [sController setupCommissioningSessionWithPayload:payload newNodeID:nodeID error:&error];
    XCTAssertNil(error);

    [self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds];

    [_commissionedNodeIDs addObject:nodeID];

    return [MTRDevice deviceWithNodeID:nodeID controller:sController];
}

- (void)initStack
{
    sStackInitRan = YES;

    __auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
    XCTAssertNotNil(factory);

    __auto_type * storage = [[MTRTestStorage alloc] init];
    sOTAProviderDelegate = [[MTROTAProviderDelegateImpl alloc] init];

    __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage];
    factoryParams.port = @(kLocalPort);
    factoryParams.otaProviderDelegate = sOTAProviderDelegate;
    factoryParams.shouldStartServer = YES;

    BOOL ok = [factory startControllerFactory:factoryParams error:nil];
    XCTAssertTrue(ok);
}

+ (void)shutdownStack
{
    sNeedsStackShutdown = NO;

    [[MTRDeviceControllerFactory sharedInstance] stopControllerFactory];
}

/**
 * Returns path to the raw image.
 */
- (NSString *)createRawImageWithVersion:(NSNumber *)version
{
    NSTask * buildTask = [self createTaskForPath:@"scripts/examples/gn_build_example.sh"];
    NSString * objdir =
        [self absolutePathFor:[NSString stringWithFormat:@"out/debug/ota-requestor-app-v%u", version.unsignedIntValue]];
    [buildTask setArguments:@[
        [self absolutePathFor:@"examples/ota-requestor-app/linux"],
        objdir,
        @"chip_config_network_layer_ble=false",
        @"non_spec_compliant_ota_action_delay_floor=0",
        [NSString stringWithFormat:@"chip_device_config_device_software_version=%u", version.unsignedIntValue],
        [NSString stringWithFormat:@"chip_device_config_device_software_version_string=\"%u.0\"", version.unsignedIntValue],
    ]];

    [self runTask:buildTask];

    NSString * sourcePath = [NSString pathWithComponents:@[ objdir, @"chip-ota-requestor-app" ]];
    NSString * destPath = [NSString stringWithFormat:@"/tmp/ota-raw-image-v%u", version.unsignedIntValue];

    // We don't care about error on remove; the file might not be there.  But if
    // it _is_ there, we have to remove, or the copy will fail.
    [[NSFileManager defaultManager] removeItemAtPath:destPath error:nil];

    NSError * copyError;
    BOOL ok = [[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destPath error:&copyError];
    XCTAssertNil(copyError);
    XCTAssertTrue(ok);

    return destPath;
}

/**
 * Returns path to the created image.
 */
- (NSString *)createImageFromRawImage:(NSString *)rawImage withVersion:(NSNumber *)version
{
    NSString * image = [rawImage stringByReplacingOccurrencesOfString:@"raw-image" withString:@"image"];

    NSTask * task = [self createTaskForPath:@"src/app/ota_image_tool.py"];
    [task setArguments:@[
        @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", version.stringValue, @"-vs",
        [NSString stringWithFormat:@"%.1f", version.floatValue], @"-da", @"sha256", rawImage, image
    ]];

    [self runTask:task];

    return image;
}

- (void)test000_SetUp
{
    // Nothing to do here; our setUp method handled this already.  This test
    // just exists to make the setup not look like it's happening inside other
    // tests.
}

- (XCTestExpectation *)announceProviderToDevice:(MTRDevice *)device
{
    dispatch_queue_t queue = dispatch_get_main_queue();

    XCTestExpectation * responseExpectation =
        [self expectationWithDescription:[NSString stringWithFormat:@"AnnounceOTAProvider to %@ succeeded", device]];

    __auto_type * params = [[MTROTASoftwareUpdateRequestorClusterAnnounceOTAProviderParams alloc] init];
    params.providerNodeID = [sController controllerNodeID];
    params.vendorID = @(kTestVendorId);
    params.announcementReason = @(MTROTASoftwareUpdateRequestorAnnouncementReasonSimpleAnnouncement);
    params.endpoint = @(kOTAProviderEndpointId);

    __auto_type * cluster = [[MTRClusterOTASoftwareUpdateRequestor alloc] initWithDevice:device endpointID:@(0) queue:queue];
    [cluster announceOTAProviderWithParams:params
                            expectedValues:nil
                     expectedValueInterval:nil
                                completion:^(NSError * _Nullable error) {
                                    XCTAssertNil(error);
                                    [responseExpectation fulfill];
                                }];

    return responseExpectation;
}

- (void)test001_ReceiveQueryImageRequest_RespondUpdateNotAvailable
{
    // Test that if we advertise ourselves as a provider we end up getting a
    // QueryImage callbacks that we can respond to.
    __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    __auto_type * device = [runner commissionWithNodeID:@(kDeviceId1)];

    XCTestExpectation * queryExpectation = [self expectationWithDescription:@"handleQueryImageForNodeID called"];
    sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        [sOTAProviderDelegate respondNotAvailableWithCompletion:completion];
        [queryExpectation fulfill];
    };

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];

    [self waitForExpectations:@[ queryExpectation, announceResponseExpectation ] timeout:kTimeoutInSeconds];

    sOTAProviderDelegate.queryImageHandler = nil;
}

- (void)test002_ReceiveTwoQueryImageRequests_RespondExplicitBusy
{
    // Test that if we advertise ourselves as a provider and respond BUSY to
    // QueryImage callback, then we get a second QueryImage callback later on
    // that we can then respond to however we wish.
    __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    __auto_type * device = [runner commissionWithNodeID:@(kDeviceId1)];

    XCTestExpectation * queryExpectation1 = [self expectationWithDescription:@"handleQueryImageForNodeID called first time"];
    XCTestExpectation * queryExpectation2 = [self expectationWithDescription:@"handleQueryImageForNodeID called second time"];
    const uint16_t busyDelay = 1; // 1 second

    __block QueryImageHandler handleSecondQuery;
    sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        sOTAProviderDelegate.queryImageHandler = handleSecondQuery;
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        [sOTAProviderDelegate respondBusyWithDelay:@(busyDelay) completion:completion];
        [queryExpectation1 fulfill];
    };

    handleSecondQuery = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        [sOTAProviderDelegate respondNotAvailableWithCompletion:completion];
        [queryExpectation2 fulfill];
    };

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];

    // Make sure we get our queries in order.  Give it a bit more time, because
    // there will be a delay between the two queries.
    [self waitForExpectations:@[ queryExpectation1, queryExpectation2 ] timeout:(kTimeoutInSeconds + busyDelay) enforceOrder:YES];

    [self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds];
}

- (void)test003_ReceiveQueryImageRequestWhileHandlingBDX_RespondImplicitBusy
{
    // In this test we do the following:
    //
    // 1) Advertise ourselves to device1.
    // 2) When device1 queries for an image, claim to have one.
    // 3) When device1 tries to start a bdx transfer, stall it and advertise to device2.
    // 4) When device2 queries for an image, claim to have one. Since we are
    //    in the middle of doing BDX with device1, this actually responds with Busy.
    // 5) Error out of the device1 transfer.
    // 6) Wait for device2 to query us again.
    __auto_type * runner1 = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    __auto_type * device1 = [runner1 commissionWithNodeID:@(kDeviceId1)];

    __auto_type * runner2 = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload2 testcase:self];
    __auto_type * device2 = [runner2 commissionWithNodeID:@(kDeviceId2)];

    __block XCTestExpectation * announceResponseExpectation2;
    XCTestExpectation * queryExpectation1 = [self expectationWithDescription:@"handleQueryImageForNodeID called first time"];
    XCTestExpectation * queryExpectation2 = [self expectationWithDescription:@"handleQueryImageForNodeID called second time"];
    XCTestExpectation * queryExpectation3 = [self expectationWithDescription:@"handleQueryImageForNodeID called third time"];
    XCTestExpectation * transferEndExpectation = [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called"];

    const uint16_t busyDelay = 1; // 1 second
    NSString * fakeImageURI = @"No such image, really";

    __block QueryImageHandler handleThirdQuery;
    sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        [queryExpectation1 fulfill];
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        [sOTAProviderDelegate respondAvailableWithDelay:@(0)
                                                    uri:fakeImageURI
                                            updateToken:[sOTAProviderDelegate generateUpdateToken]
                                        softwareVersion:kUpdatedSoftwareVersion_5
                                  softwareVersionString:kUpdatedSoftwareVersionString_5
                                             completion:completion];
    };
    sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator,
        NSNumber * offset, MTRStatusCompletion outerCompletion) {
        sOTAProviderDelegate.transferBeginHandler = nil;
        // Now that we've begun a transfer, we expect to be told when it ends, even if it's due to an error
        sOTAProviderDelegate.transferEndHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) {
            [transferEndExpectation fulfill];
            sOTAProviderDelegate.transferEndHandler = nil;
            XCTAssertEqualObjects(nodeID, @(kDeviceId1));
            XCTAssertIdentical(controller, sController);
            XCTAssertNotNil(error); // we cancelled the transfer, so there should be an error
        };

        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertIdentical(controller, sController);

        // Don't actually respond until the second requestor has queried us for
        // an image.  We need to reset queryImageHandler here, so we can close
        // over outerCompletion.
        sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
            MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion innerCompletion) {
            [queryExpectation2 fulfill];
            sOTAProviderDelegate.queryImageHandler = handleThirdQuery;

            XCTAssertEqualObjects(nodeID, @(kDeviceId2));
            XCTAssertIdentical(controller, sController);

            // We respond UpdateAvailable, but since we are in the middle of
            // handling OTA for device1 we expect the requestor to get Busy and
            // try again.
            [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay)
                                                        uri:fakeImageURI
                                                updateToken:[sOTAProviderDelegate generateUpdateToken]
                                            softwareVersion:kUpdatedSoftwareVersion_5
                                      softwareVersionString:kUpdatedSoftwareVersionString_5
                                                 completion:innerCompletion];

            // Cancel the transfer with device1
            [sOTAProviderDelegate respondErrorWithCompletion:outerCompletion];
        };

        announceResponseExpectation2 = [self announceProviderToDevice:device2];
    };

    handleThirdQuery = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        [queryExpectation3 fulfill];
        XCTAssertEqualObjects(nodeID, @(kDeviceId2));
        XCTAssertIdentical(controller, sController);

        [sOTAProviderDelegate respondNotAvailableWithCompletion:completion];
    };

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation1 = [self announceProviderToDevice:device1];

    // Make sure we get our queries in order.  Give it a bit more time, because
    // there will be a delay between the two queries.
    [self waitForExpectations:@[ queryExpectation1, queryExpectation2, transferEndExpectation, queryExpectation3 ]
                      timeout:(kTimeoutInSeconds + busyDelay * 3)
                 enforceOrder:YES];

    [self waitForExpectations:@[ announceResponseExpectation1, announceResponseExpectation2 ] timeout:kTimeoutInSeconds];
}

- (void)test004_DoBDXTransferDenyUpdateRequest
{
    // In this test we do the following:
    //
    // 1) Create an actual image we can send to the device, with a valid header
    //    but garbage data.
    // 2) Advertise ourselves to device.
    // 3) When device queries for an image, claim to have one.
    // 4) When device tries to start a bdx transfer, respond with success.
    // 5) Send the data as the BDX transfer proceeds.
    // 6) When device invokes ApplyUpdateRequest, respond with Discontinue so
    //    that the update does not actually proceed.
    __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    __auto_type * device = [runner commissionWithNodeID:@(kDeviceId1)];

    // First, create an image.  Make it at least 4096 bytes long, so we get
    // multiple BDX blocks going.
    const size_t rawImageSize = 4112;
    NSData * rawImagePiece = [@"1234567890abcdef" dataUsingEncoding:NSUTF8StringEncoding];
    XCTAssertEqual(rawImageSize % rawImagePiece.length, 0);
    NSMutableData * fakeImage = [NSMutableData dataWithCapacity:rawImageSize];
    while (fakeImage.length < rawImageSize) {
        [fakeImage appendData:rawImagePiece];
    }
    NSString * rawImagePath = @"/tmp/ota-test004-raw-image";

    [[NSFileManager defaultManager] createFileAtPath:rawImagePath contents:fakeImage attributes:nil];

    __auto_type * checker =
        [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:rawImagePath
                                           otaImageDownloadFilePath:runner.downloadFilePath
                                                             nodeID:@(kDeviceId1)
                                                    softwareVersion:kUpdatedSoftwareVersion_5
                                              softwareVersionString:kUpdatedSoftwareVersionString_5
                                                  applyUpdateAction:MTROTASoftwareUpdateProviderApplyUpdateActionDiscontinue
                                                           testcase:self];
    // We do not expect the update to actually be applied here.
    checker.notifyUpdateAppliedExpectation.inverted = YES;

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];

    // Make sure we get our callbacks in order.  Give it a bit more time, because
    // we want to allow time for the BDX download.
    [self waitForExpectations:@[ checker.queryExpectation, checker.bdxBeginExpectation, checker.bdxQueryExpectation ]
                      timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES];

    // Nothing really defines the ordering of bdxEndExpectation and
    // applyUpdateRequestExpectation with respect to each other, and nothing
    // defines the ordering of announceResponseExpectation with respect to _any_
    // of the above expectations.
    [self waitForExpectations:@[ checker.bdxEndExpectation, checker.applyUpdateRequestExpectation, announceResponseExpectation ]
                      timeout:kTimeoutInSeconds];

    // We are not expecting checker.notifyUpdateAppliedExpectation to actually
    // be called fulfilled, but we still need to wait on it here.  Since we set
    // inverted = YES, on it, this is basically a no-op, except for making
    // XCTest not complain about unwaited expectations.
    [self waitForExpectations:@[ checker.notifyUpdateAppliedExpectation ] timeout:kTimeoutInSeconds];
}

// TODO: Enable tests 005, 006 and 007 when PR #26040 is merged. Currently the poll interval causes delays in the BDX transfer
// and results in the tests taking a long time. With PR #26040 we eliminate the poll interval completely and hence the tests can run
// in a short time.
#ifdef ENABLE_REAL_OTA_UPDATE_TESTS
- (void)test005_DoBDXTransferAllowUpdateRequest
{
    // In this test we do the following:
    //
    // 1) Check if the ota image file and raw image file required for this test exist.
    // 2) Advertise ourselves to device.
    // 3) When device queries for an image, pass the image path for the ota file generated beforehand as a pre-requisite
    // 4) When device tries to start a bdx transfer, respond with success.
    // 5) Send the data as the BDX transfer proceeds.
    // 6) Confirm the downloaded ota image matches the raw image file that was generated before the test was run as a pre-requisite
    // 7) When device invokes ApplyUpdateRequest, respond with Proceed so that the update proceeds
    // 8) Wait for the app to restart and wait for the NotifyUpdateApplied message to confirm the app has updated to the new version

    NSString * otaRawImagePath = [self createRawImageWithVersion:kUpdatedSoftwareVersion_5];

    // Check whether the ota raw image exists at otaRawImagePath
    XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath]);

    __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    __auto_type * device = [runner commissionWithNodeID:@(kDeviceId1)];

    __auto_type * checker =
        [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:otaRawImagePath
                                           otaImageDownloadFilePath:runner.downloadFilePath
                                                             nodeID:@(kDeviceId1)
                                                    softwareVersion:kUpdatedSoftwareVersion_5
                                              softwareVersionString:kUpdatedSoftwareVersionString_5
                                                  applyUpdateAction:MTROTASoftwareUpdateProviderApplyUpdateActionProceed
                                                           testcase:self];

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];

    // Make sure we get our callbacks in order.  Give it a bit more time, because
    // we want to allow time for the BDX download.
    [self waitForExpectations:@[
        checker.queryExpectation, checker.bdxBeginExpectation, checker.bdxQueryExpectation, checker.bdxEndExpectation
    ]
                      timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES];

    // Nothing really defines the ordering of bdxEndExpectation and
    // applyUpdateRequestExpectation with respect to each other.
    [self waitForExpectations:@[ checker.applyUpdateRequestExpectation, checker.notifyUpdateAppliedExpectation ]
                      timeout:kTimeoutInSeconds
                 enforceOrder:YES];

    // Nothing defines the ordering of announceResponseExpectation with respect
    // to _any_ of the above expectations.
    [self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds];
}

- (void)test006_DoBDXTransferWithTwoOTARequesters
{
    // In this test, we test BDX transfers between one provider and two OTA requestors device1 and device2.
    //
    // 1) We announce ourselves to device1 first.
    // 2) When device1 queries, we return image available at imagePath1.
    // 3) We set the BDX begin and end handlers for device1 and device1 starts the BDX transfer.
    // 4) At this time, we announce ourselves to device2.
    // 5) When device2 queries, we return image available with a busy delay of 30 secs.
    // 6) When device1 completes the BDX transfer, we wait for device2 to query.
    // 7) When device 2 queries again, we return image available at imagePath2.
    // 8) We set the BDX begin and end handlers for device2 and device2 starts the BDX transfer.
    // 9) Device1 applies the update independently and reboots with the new image.
    // 10) At this point, we set the apply update handlers for device2.
    // 11) Device2 applies the update and reboots with its new image.

    NSString * otaRawImagePath1 = [self createRawImageWithVersion:kUpdatedSoftwareVersion_10];
    NSString * otaRawImagePath2 = [self createRawImageWithVersion:kUpdatedSoftwareVersion_5];

    // Check whether the ota raw image exists at otaRawImagePath1 and otaRawImagePath2
    XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath1]);
    XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath2]);

    XCTestExpectation * queryExpectation1 = [self expectationWithDescription:@"handleQueryImageForNodeID called for device1"];
    XCTestExpectation * queryExpectation2 =
        [self expectationWithDescription:@"handleQueryImageForNodeID called for device2 - busy"];
    XCTestExpectation * queryExpectation3 =
        [self expectationWithDescription:@"handleQueryImageForNodeID called for device2 - available"];

    XCTestExpectation * bdxBeginExpectation1 =
        [self expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called for device1"];
    XCTestExpectation * bdxBeginExpectation2 =
        [self expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called for device2"];

    XCTestExpectation * bdxQueryExpectation1 = [self expectationWithDescription:@"handleBDXQueryForNodeID called for device1"];
    XCTestExpectation * bdxQueryExpectation2 = [self expectationWithDescription:@"handleBDXQueryForNodeID called for device2"];

    XCTestExpectation * bdxEndExpectation1 =
        [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called for device1"];
    XCTestExpectation * bdxEndExpectation2 =
        [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called for device2"];

    XCTestExpectation * applyUpdateRequestExpectation1 =
        [self expectationWithDescription:@"handleApplyUpdateRequestForNodeID called for device1"];
    XCTestExpectation * applyUpdateRequestExpectation2 =
        [self expectationWithDescription:@"handleApplyUpdateRequestForNodeID called for device2"];

    XCTestExpectation * notifyUpdateAppliedExpectation1 =
        [self expectationWithDescription:@"handleNotifyUpdateAppliedForNodeID called for device1"];
    XCTestExpectation * notifyUpdateAppliedExpectation2 =
        [self expectationWithDescription:@"handleNotifyUpdateAppliedForNodeID called for device2"];

    __block XCTestExpectation * announceResponseExpectation2;

    NSString * imagePath1 = [self createImageFromRawImage:otaRawImagePath1 withVersion:kUpdatedSoftwareVersion_10];
    NSString * imagePath2 = [self createImageFromRawImage:otaRawImagePath2 withVersion:kUpdatedSoftwareVersion_5];

    NSData * updateToken1 = [sOTAProviderDelegate generateUpdateToken];
    NSData * updateToken2 = [sOTAProviderDelegate generateUpdateToken];

    __block NSFileHandle * readHandle;
    __block uint64_t imageSize;
    __block uint32_t lastBlockIndex = UINT32_MAX;
    const uint16_t busyDelay = 30; // 30 second

    __auto_type * runner1 = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    __auto_type * device1 = [runner1 commissionWithNodeID:@(kDeviceId1)];

    __auto_type * runner2 = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload2 testcase:self];
    __auto_type * device2 = [runner2 commissionWithNodeID:@(kDeviceId2)];

    // This to keep track of whether queryImageHandler for device 2 was called or not. The first time it's called we will
    // fulfill queryExpectation2 and proceed with BDX for device 1.
    __block bool firstQueryImageForDevice2Received = false;

    // Set up the query handler for device 1 to return image available at imagePath1
    sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);

        sOTAProviderDelegate.queryImageHandler = nil;
        [sOTAProviderDelegate respondAvailableWithDelay:@(0)
                                                    uri:imagePath1
                                            updateToken:updateToken1
                                        softwareVersion:kUpdatedSoftwareVersion_10
                                  softwareVersionString:kUpdatedSoftwareVersionString_10
                                             completion:completion];
        [queryExpectation1 fulfill];
    };

    // Set up the BDX transfer begin, block query and transfer end handlers for device1 so it can go ahead with the BDX transfer
    sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator,
        NSNumber * offset, MTRStatusCompletion outerCompletion) {
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(fileDesignator, imagePath1);
        XCTAssertEqualObjects(offset, @(0));

        readHandle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator];
        XCTAssertNotNil(readHandle);

        NSError * endSeekError;
        XCTAssertTrue([readHandle seekToEndReturningOffset:&imageSize error:&endSeekError]);
        XCTAssertNil(endSeekError);
        sOTAProviderDelegate.transferBeginHandler = nil;

        // Don't actually respond until the second requestor has queried us for
        // an image to ensure BDX for device1 starts only after device2 has queried us once.

        // Set up the query handler for device2 to return image available at imagePath2 but a busy delay of 30 secs
        sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
            MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion innerCompletion) {
            XCTAssertEqualObjects(nodeID, @(kDeviceId2));
            XCTAssertEqual(controller, sController);

            [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay)
                                                        uri:imagePath2
                                                updateToken:updateToken2
                                            softwareVersion:kUpdatedSoftwareVersion_5
                                      softwareVersionString:kUpdatedSoftwareVersionString_5
                                                 completion:innerCompletion];
            if (!firstQueryImageForDevice2Received) {
                [queryExpectation2 fulfill];

                // Respond with success for the tranfer begin completion for device1
                [sOTAProviderDelegate respondSuccess:outerCompletion];
                [bdxBeginExpectation1 fulfill];
            }
            firstQueryImageForDevice2Received = true;
        };

        // Announce ourselves to device2
        announceResponseExpectation2 = [self announceProviderToDevice:device2];
    };
    sOTAProviderDelegate.blockQueryHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize,
        NSNumber * blockIndex, NSNumber * bytesToSkip, BlockQueryCompletion completion) {
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(blockSize, @(1024)); // Seems to always be 1024.
        XCTAssertEqualObjects(blockIndex, @(lastBlockIndex + 1));
        XCTAssertEqualObjects(bytesToSkip, @(0)); // Don't expect to see skips here.
        // Make sure we actually end up with multiple blocks.
        XCTAssertTrue(blockSize.unsignedLongLongValue < imageSize);

        XCTAssertNotNil(readHandle);
        uint64_t offset = blockSize.unsignedLongLongValue * blockIndex.unsignedLongLongValue;
        NSError * seekError = nil;
        [readHandle seekToOffset:offset error:&seekError];
        XCTAssertNil(seekError);

        NSError * readError = nil;
        NSData * data = [readHandle readDataUpToLength:blockSize.unsignedLongValue error:&readError];
        XCTAssertNil(readError);
        XCTAssertNotNil(data);

        BOOL isEOF = offset + blockSize.unsignedLongValue >= imageSize;

        ++lastBlockIndex;

        if (isEOF) {
            sOTAProviderDelegate.blockQueryHandler = nil;
        }

        completion(data, isEOF);

        if (isEOF) {
            [bdxQueryExpectation1 fulfill];
        }
    };
    sOTAProviderDelegate.transferEndHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) {
        XCTAssertEqualObjects(nodeID, @(kDeviceId1));
        XCTAssertEqual(controller, sController);
        XCTAssertNil(error);
        sOTAProviderDelegate.transferEndHandler = nil;
        [bdxEndExpectation1 fulfill];

        // BDX transfer with device1 has completed
        // Set up the query handler for device2 to return image available at imagePath2
        sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
            MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
            XCTAssertEqualObjects(nodeID, @(kDeviceId2));
            XCTAssertEqual(controller, sController);

            sOTAProviderDelegate.queryImageHandler = nil;
            [sOTAProviderDelegate respondAvailableWithDelay:@(0)
                                                        uri:imagePath2
                                                updateToken:updateToken2
                                            softwareVersion:kUpdatedSoftwareVersion_5
                                      softwareVersionString:kUpdatedSoftwareVersionString_5
                                                 completion:completion];
            NSLog(@"OTA queryExpectation3");
            [queryExpectation3 fulfill];
        };

        // Set up the BDX transfer begin, block query and transfer end handlers for device2 so it can go ahead with the BDX transfer
        sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
            NSString * fileDesignator, NSNumber * offset, MTRStatusCompletion completion) {
            XCTAssertEqualObjects(nodeID, @(kDeviceId2));
            XCTAssertEqual(controller, sController);
            XCTAssertEqualObjects(fileDesignator, imagePath2);
            XCTAssertEqualObjects(offset, @(0));

            readHandle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator];
            XCTAssertNotNil(readHandle);

            NSError * endSeekError;
            XCTAssertTrue([readHandle seekToEndReturningOffset:&imageSize error:&endSeekError]);
            XCTAssertNil(endSeekError);
            sOTAProviderDelegate.transferBeginHandler = nil;
            [sOTAProviderDelegate respondSuccess:completion];
            lastBlockIndex = UINT32_MAX;
            [bdxBeginExpectation2 fulfill];
        };
        sOTAProviderDelegate.blockQueryHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize,
            NSNumber * blockIndex, NSNumber * bytesToSkip, BlockQueryCompletion completion) {
            XCTAssertEqualObjects(nodeID, @(kDeviceId2));
            XCTAssertEqual(controller, sController);
            XCTAssertEqualObjects(blockSize, @(1024)); // Seems to always be 1024.
            XCTAssertEqualObjects(blockIndex, @(lastBlockIndex + 1));
            XCTAssertEqualObjects(bytesToSkip, @(0)); // Don't expect to see skips here.
            // Make sure we actually end up with multiple blocks.
            XCTAssertTrue(blockSize.unsignedLongLongValue < imageSize);

            XCTAssertNotNil(readHandle);
            uint64_t offset = blockSize.unsignedLongLongValue * blockIndex.unsignedLongLongValue;
            NSError * seekError = nil;
            [readHandle seekToOffset:offset error:&seekError];
            XCTAssertNil(seekError);

            NSError * readError = nil;
            NSData * data = [readHandle readDataUpToLength:blockSize.unsignedLongValue error:&readError];
            XCTAssertNil(readError);
            XCTAssertNotNil(data);

            BOOL isEOF = offset + blockSize.unsignedLongValue >= imageSize;

            ++lastBlockIndex;

            if (isEOF) {
                sOTAProviderDelegate.blockQueryHandler = nil;
            }

            completion(data, isEOF);

            if (isEOF) {
                [bdxQueryExpectation2 fulfill];
            }
        };
        sOTAProviderDelegate.transferEndHandler
            = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) {
                  XCTAssertEqualObjects(nodeID, @(kDeviceId2));
                  XCTAssertEqual(controller, sController);
                  XCTAssertNil(error);
                  sOTAProviderDelegate.transferEndHandler = nil;
                  [bdxEndExpectation2 fulfill];
              };
    };

    // Flags to track if the applyUpdateRequest from both device1 and device2 has been handled.
    __block bool device1HasHandledApplyUpdateRequest = false;
    __block bool device2HasHandledApplyUpdateRequest = false;

    // Flags to track if device1 and device have notified that the update has been applied.
    __block bool device1HasNotifiedUpdateApplied = false;
    __block bool device2HasNotifiedUpdateApplied = false;

    // Set up the apply update request and update applied handlers for device1 and device2. Use the nodeID to verify which
    // device sent the applyUpdateRequest and subsequent notifyUpdateApplied commands.
    sOTAProviderDelegate.applyUpdateRequestHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion) {
        XCTAssertTrue([nodeID isEqualToNumber:@(kDeviceId1)] || [nodeID isEqualToNumber:@(kDeviceId2)]);

        bool isDeviceID1 = [nodeID isEqualToNumber:@(kDeviceId1)];
        NSData * updateToken = (isDeviceID1) ? updateToken1 : updateToken2;

        // Device1 is updated to version 10 and device2 to version 5.
        NSNumber * kSoftwareVersion = (isDeviceID1) ? kUpdatedSoftwareVersion_10 : kUpdatedSoftwareVersion_5;
        NSString * otaImageFilePath = (isDeviceID1) ? otaRawImagePath1 : otaRawImagePath2;
        NSString * otaDownloadedFilePath = (isDeviceID1) ? runner1.downloadFilePath : runner2.downloadFilePath;

        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(params.updateToken, updateToken);
        XCTAssertEqualObjects(params.newVersion, kSoftwareVersion);

        XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:otaImageFilePath andPath:otaDownloadedFilePath]);

        [sOTAProviderDelegate respondToApplyUpdateRequestWithAction:MTROTASoftwareUpdateProviderApplyUpdateActionProceed
                                                         completion:completion];

        if (isDeviceID1) {
            device1HasHandledApplyUpdateRequest = true;
            [applyUpdateRequestExpectation1 fulfill];
        } else {
            device2HasHandledApplyUpdateRequest = true;
            [applyUpdateRequestExpectation2 fulfill];
        }

        // If the applyUpdateRequest from both device1 and device2 has been handled, reset the
        // applyUpdateRequestHandler to nil
        if (device1HasHandledApplyUpdateRequest && device2HasHandledApplyUpdateRequest) {
            sOTAProviderDelegate.applyUpdateRequestHandler = nil;
        }
    };

    sOTAProviderDelegate.notifyUpdateAppliedHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
        MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams * params, MTRStatusCompletion completion) {
        XCTAssertTrue([nodeID isEqualToNumber:@(kDeviceId1)] || [nodeID isEqualToNumber:@(kDeviceId2)]);

        bool isDeviceID1 = [nodeID isEqualToNumber:@(kDeviceId1)];
        NSData * updateToken = (isDeviceID1) ? updateToken1 : updateToken2;

        // Device1 is updated to version 10 and device2 to version 5.
        NSNumber * kSoftwareVersion = (isDeviceID1) ? kUpdatedSoftwareVersion_10 : kUpdatedSoftwareVersion_5;

        XCTAssertEqual(controller, sController);
        XCTAssertEqualObjects(params.updateToken, updateToken);
        XCTAssertEqualObjects(params.softwareVersion, kSoftwareVersion);

        [sOTAProviderDelegate respondSuccess:completion];

        if (isDeviceID1) {
            device1HasNotifiedUpdateApplied = true;
            [notifyUpdateAppliedExpectation1 fulfill];
        } else {
            device2HasNotifiedUpdateApplied = true;
            [notifyUpdateAppliedExpectation2 fulfill];
        }

        // If both device1 and device2 have notified that the update has been applied, reset the
        // notifyUpdateAppliedHandler to nil
        if (device1HasNotifiedUpdateApplied && device2HasNotifiedUpdateApplied) {
            sOTAProviderDelegate.notifyUpdateAppliedHandler = nil;
        }
    };

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation1 = [self announceProviderToDevice:device1];

    // Make sure we get our callbacks in order for both device1 and device2.  Since we do not
    // send image available to device2 until BDX for device1 has ended, queryExpectation3 must follow
    // bdxEndExpectation1.
    //
    // Give it a bit more time, because we want to allow time for the BDX downloads.
    [self waitForExpectations:@[
        queryExpectation1, bdxBeginExpectation1, bdxQueryExpectation1, bdxEndExpectation1, queryExpectation3, bdxBeginExpectation2,
        bdxQueryExpectation2, bdxEndExpectation2
    ]
                      timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES];

    // Make sure we get our query expectation for device2 fulfilled when we send busy for device2.
    [self waitForExpectations:@[ queryExpectation2 ] timeout:(kTimeoutInSeconds)];

    // Nothing really defines the ordering of bdxEndExpectation and
    // applyUpdateRequestExpectation with respect to each other.
    [self waitForExpectations:@[
        applyUpdateRequestExpectation1, notifyUpdateAppliedExpectation1, applyUpdateRequestExpectation2,
        notifyUpdateAppliedExpectation2
    ]
                      timeout:kTimeoutInSeconds
                 enforceOrder:NO];

    // Nothing defines the ordering of announceResponseExpectation with respect
    // to _any_ of the above expectations.
    [self waitForExpectations:@[ announceResponseExpectation1, announceResponseExpectation2 ] timeout:kTimeoutInSeconds];
}

- (void)test007_DoBDXTransferIncrementalOtaUpdate
{
    // In this test, we test incremental OTA update with device3. First we update device3
    // to version 5 using image at imagePath1. Once device3 updates and reboots, we update
    // it to version 10 using image at imagePath2.
    //
    // 1) We announce ourselves to device3.
    // 2) When device3 queries, we return image available at imagePath1 with version number 5.
    // 3) We set the BDX begin and end handlers for device3 and device3 starts the BDX transfer.
    // 4) Device3 completes the BDX transfer
    // 5) Device3 applies the update and reboots with the new image with version number 5
    // 6) We announce ourselves to device 3 again.
    // 7) When device3 queries again, we return image available at imagePath2 with version number 10.
    // 8) We set the BDX begin and end handlers for device3 and device3 starts the BDX transfer.
    // 4) Device3 completes the BDX transfer
    // 5) Device3 applies the update and reboots with the new image with version number 10

    NSString * otaRawImagePath1 = [self createRawImageWithVersion:kUpdatedSoftwareVersion_5];
    NSString * otaRawImagePath2 = [self createRawImageWithVersion:kUpdatedSoftwareVersion_10];

    // Check whether the ota raw image exists at otaRawImagePath1 and otaRawImagePath2
    XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath1]);
    XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:otaRawImagePath2]);

    __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload3 testcase:self];
    __auto_type * device = [runner commissionWithNodeID:@(kDeviceId3)];

    __auto_type * checker =
        [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:otaRawImagePath1
                                           otaImageDownloadFilePath:runner.downloadFilePath
                                                             nodeID:@(kDeviceId3)
                                                    softwareVersion:kUpdatedSoftwareVersion_5
                                              softwareVersionString:kUpdatedSoftwareVersionString_5
                                                  applyUpdateAction:MTROTASoftwareUpdateProviderApplyUpdateActionProceed
                                                           testcase:self];

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];

    // Make sure we get our callbacks in order.  Give it a bit more time, because
    // we want to allow time for the BDX download.
    [self waitForExpectations:@[
        checker.queryExpectation, checker.bdxBeginExpectation, checker.bdxQueryExpectation, checker.bdxEndExpectation
    ]
                      timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES];

    // Nothing really defines the ordering of bdxEndExpectation and
    // applyUpdateRequestExpectation with respect to each other.
    [self waitForExpectations:@[ checker.applyUpdateRequestExpectation, checker.notifyUpdateAppliedExpectation ]
                      timeout:kTimeoutInSeconds
                 enforceOrder:YES];

    // Nothing defines the ordering of announceResponseExpectation with respect
    // to _any_ of the above expectations.
    [self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds];

    // Provide an incremental update and makes sure the app is updated to the new version

    __auto_type * checker1 =
        [[MTROTAProviderTransferChecker alloc] initWithRawImagePath:otaRawImagePath2
                                           otaImageDownloadFilePath:runner.downloadFilePath
                                                             nodeID:@(kDeviceId3)
                                                    softwareVersion:kUpdatedSoftwareVersion_10
                                              softwareVersionString:kUpdatedSoftwareVersionString_10
                                                  applyUpdateAction:MTROTASoftwareUpdateProviderApplyUpdateActionProceed
                                                           testcase:self];

    // Advertise ourselves as an OTA provider.
    XCTestExpectation * announceResponseExpectation1 = [self announceProviderToDevice:device];

    // Make sure we get our callbacks in order.  Give it a bit more time, because
    // we want to allow time for the BDX download.
    [self waitForExpectations:@[
        checker1.queryExpectation, checker1.bdxBeginExpectation, checker1.bdxQueryExpectation, checker1.bdxEndExpectation
    ]
                      timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES];

    // Nothing really defines the ordering of bdxEndExpectation and
    // applyUpdateRequestExpectation with respect to each other.
    [self waitForExpectations:@[ checker1.applyUpdateRequestExpectation, checker1.notifyUpdateAppliedExpectation ]
                      timeout:kTimeoutInSeconds
                 enforceOrder:YES];

    // Nothing defines the ordering of announceResponseExpectation with respect
    // to _any_ of the above expectations.
    [self waitForExpectations:@[ announceResponseExpectation1 ] timeout:kTimeoutInSeconds];
}
#endif // ENABLE_REAL_OTA_UPDATE_TESTS

- (void)test008_TestWriteDefaultOTAProviders
{
    __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self];
    MTRDevice * device = [runner commissionWithNodeID:@(kDeviceId1)];

    dispatch_queue_t queue = dispatch_get_main_queue();

    __auto_type dataValue = ^(uint16_t endpoint) {
        return @{
            MTRTypeKey : MTRArrayValueType,
            MTRValueKey : @[
                @{
                    MTRDataKey : @ {
                        MTRTypeKey : MTRStructureValueType,
                        MTRValueKey : @[
                            @{
                                MTRContextTagKey : @(1),
                                MTRDataKey : @ {
                                    MTRTypeKey : MTRUnsignedIntegerValueType,
                                    MTRValueKey : @(kDeviceId1),
                                },
                            },
                            @{
                                MTRContextTagKey : @(2),
                                MTRDataKey : @ {
                                    MTRTypeKey : MTRUnsignedIntegerValueType,
                                    MTRValueKey : @(endpoint),
                                },
                            },
                        ],
                    },
                },
            ],
        };
    };

    {
        // Test with MTRBaseDevice first.
        MTRBaseDevice * baseDevice = [MTRBaseDevice deviceWithNodeID:device.nodeID
                                                          controller:device.deviceController];

        __auto_type * cluster = [[MTRBaseClusterOTASoftwareUpdateRequestor alloc] initWithDevice:baseDevice
                                                                                      endpointID:@(0)
                                                                                           queue:queue];
        __auto_type * providerLocation = [[MTROTASoftwareUpdateRequestorClusterProviderLocation alloc] init];
        providerLocation.providerNodeID = @(kDeviceId1);
        providerLocation.endpoint = @(0);
        __auto_type * value = @[ providerLocation ];

        __auto_type * writeBaseClusterExpectation = [self expectationWithDescription:@"Write succeeded via MTRBaseCluster"];
        [cluster writeAttributeDefaultOTAProvidersWithValue:value
                                                 completion:^(NSError * _Nullable error) {
                                                     XCTAssertNil(error);
                                                     [writeBaseClusterExpectation fulfill];
                                                 }];
        [self waitForExpectations:@[ writeBaseClusterExpectation ] timeout:kTimeoutInSeconds];

        __auto_type * writeBaseDeviceExpectation = [self expectationWithDescription:@"Write succeeded via MTRBaseDevice"];
        [baseDevice writeAttributeWithEndpointID:@(0)
                                       clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID)
                                     attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)
                                           value:dataValue(0)
                               timedWriteTimeout:nil
                                           queue:queue
                                      completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
                                          XCTAssertNil(error);
                                          XCTAssertNotNil(values);
                                          XCTAssertEqual(values.count, 1);

                                          for (NSDictionary<NSString *, id> * value in values) {
                                              XCTAssertNil(value[MTRErrorKey]);
                                          }
                                          [writeBaseDeviceExpectation fulfill];
                                      }];
        [self waitForExpectations:@[ writeBaseDeviceExpectation ] timeout:kTimeoutInSeconds];
    }

    {
        // Now test with MTRDevice
        __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
        // Make sure we don't have expected value notifications confusing our
        // attribute reports.
        delegate.skipExpectedValuesForWrite = YES;

        XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Subscription established"];
        delegate.onReportEnd = ^() {
            [gotReportsExpectation fulfill];
        };

        [device setDelegate:delegate queue:queue];

        [self waitForExpectations:@[ gotReportsExpectation ] timeout:60];

        delegate.onReportEnd = nil;

        __auto_type * expectedAttributePath = [MTRAttributePath attributePathWithEndpointID:@(0)
                                                                                  clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID)
                                                                                attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)];

        __block __auto_type * expectedValue = dataValue(1);

        __block __auto_type * writeExpectation = [self expectationWithDescription:@"Write succeeded via MTRCluster"];
        delegate.onAttributeDataReceived = ^(NSArray<NSDictionary<NSString *, id> *> * data) {
            XCTAssertNotNil(data);
            XCTAssertEqual(data.count, 1);
            NSDictionary<NSString *, id> * item = data[0];

            XCTAssertNil(item[MTRErrorKey]);

            MTRAttributePath * path = item[MTRAttributePathKey];
            XCTAssertNotNil(path);

            XCTAssertEqualObjects(path, expectedAttributePath);

            NSDictionary<NSString *, id> * receivedValue = item[MTRDataKey];

            // We can't use XCTAssertEqualObjects to compare receivedValue to
            // expectedValue here, because receivedValue has a DataVersion
            // that's missing from expectedValue, and the struct in it has an
            // extra FabricIndex field.
            XCTAssertEqualObjects(receivedValue[MTRTypeKey], MTRArrayValueType);

            NSArray * receivedArray = receivedValue[MTRValueKey];
            NSArray * expectedArray = expectedValue[MTRValueKey];

            XCTAssertEqual(receivedArray.count, expectedArray.count);

            for (NSUInteger i = 0; i < receivedArray.count; ++i) {
                NSDictionary * receivedItem = receivedArray[i][MTRDataKey];
                NSDictionary * expectedItem = expectedArray[i][MTRDataKey];

                XCTAssertEqual(receivedItem[MTRTypeKey], MTRStructureValueType);
                XCTAssertEqual(expectedItem[MTRTypeKey], MTRStructureValueType);

                NSArray * receivedFields = receivedItem[MTRValueKey];
                NSArray * expectedFields = expectedItem[MTRValueKey];

                // Account for the extra FabricIndex.
                XCTAssertEqual(receivedFields.count, expectedFields.count + 1);
                for (NSUInteger j = 0; j < expectedFields.count; ++j) {
                    XCTAssertEqualObjects(receivedFields[j], expectedFields[j]);
                }
            }

            [writeExpectation fulfill];
        };

        __auto_type * cluster = [[MTRClusterOTASoftwareUpdateRequestor alloc] initWithDevice:device
                                                                                  endpointID:@(0)
                                                                                       queue:queue];
        [cluster writeAttributeDefaultOTAProvidersWithValue:expectedValue
                                      expectedValueInterval:@(0)];
        [self waitForExpectations:@[ writeExpectation ] timeout:kTimeoutInSeconds];

        expectedValue = dataValue(2);
        writeExpectation = [self expectationWithDescription:@"Write succeeded via MTRDevice"];
        [device writeAttributeWithEndpointID:@(0)
                                   clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID)
                                 attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)
                                       value:expectedValue
                       expectedValueInterval:@(0)
                           timedWriteTimeout:nil];
        [self waitForExpectations:@[ writeExpectation ] timeout:kTimeoutInSeconds];
    }
}

- (void)test999_TearDown
{
    [[self class] shutdownStack];
}

@end

#else // ENABLE_OTA_TESTS

@interface MTROTAProviderTests : XCTestCase
@end

@implementation MTROTAProviderTests
@end

#endif // ENABLE_OTA_TESTS
