//
// © 2018-2020 Qualcomm Technologies, Inc. and/or its subsidiaries. All rights reserved.
//

#import <XCTest/XCTest.h>
#import "TestCategories.h"
#import "IoTClient+Private.h"
#import "IoTAppListenerProtocol.h"
#import "IoTServiceDiscoveryResult.h"
#import "IoTWrappedRepresentation.h"
#import "MockTransport.h"
#import "IoTGlobals.h"

#define LOCALHOST @"127.0.0.1"
#define DEVICE_ID @"0"
#define DEVICE_ID2 @"1"
#define DEVICE_NAME @"MOCKED DEVICE"
#define DEVICE_NAME2 @"MOCKED DEVICE2"
#define URI_PREFIX @"/qti/allplay"
#define MEDIA_PLAYER_RESOURCE @"qti.allplay.r.mediaPlayer"
#define MEDIA_PLAYER_URI @"/qti/allplay/mediaPlayer"
#define SET_GROUP_NAME_URI @"/qti/allplay/group/groupSetGroupName"

@interface IoTClientTests : XCTestCase <IoTAppListenerProtocol>

@property (nonatomic, strong) IoTClient *client;
@property (nonatomic, strong) MockTransport *transport;
@property (nonatomic, strong) IoTServiceDiscoveryResult *discoveredService;
@property (nonatomic, strong) IoTServiceDiscoveryResult *discoveredService2;

@property (nonatomic, strong) XCTestExpectation *playerGroupExpectation;

@end

@implementation IoTClientTests

- (void)setUp
{
    [super setUp];
    self.discoveredService = [IoTServiceDiscoveryResult
                              resultWithIdentifier:DEVICE_ID
                              name:DEVICE_NAME
                              host:LOCALHOST
                              type: ALLPLAY_DEVICE_TYPE];
    
    self.discoveredService = [IoTServiceDiscoveryResult
                              resultWithIdentifier:DEVICE_ID2
                              name:DEVICE_NAME2
                              host:LOCALHOST
                              type: ALLPLAY_DEVICE_TYPE];
    self.transport = [[MockTransport alloc] init];
    self.client = [[IoTClient alloc] initWithApplicationName:@"IoTControllerUnitTests"
                                                   transport:self.transport];
    self.client.delegate = self;
}

- (void)tearDown
{
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
    self.transport = nil;
    self.client = nil;
}

- (void)testClientSetsRunningStateCorrectly {
    [self.client start];
    XCTAssertTrue(self.client.isStarted);
    [self.client stop];
	XCTAssertFalse(self.client.isStarted);
    [self.client start];
    XCTAssertTrue(self.client.isStarted);
}

- (void)testClientStartsDeviceScan
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Device Scan"];
    makeWeak(self)
    self.transport.serviceScanCallback = ^(NSSet<NSString *> *types, IoTTransportServiceDiscoveredCallback callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        NSSet<NSString *> *deviceTypes = [NSSet setWithArray:@[ALLPLAY_DEVICE_TYPE, IOTSYS_DEVICE_TYPE]];
        XCTAssertEqualObjects(types, deviceTypes, @"Devices types should match");
        [expectation fulfill];
    };

    [self.client start];

    [self waitForExpectations:@[expectation] timeout:2];
}

- (void)testClientStartsResourceScan
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Resource Scan"];
    makeWeak(self)
    self.transport.serviceScanCallback = ^(NSSet<NSString *> *types, IoTTransportServiceDiscoveredCallback callback, dispatch_queue_t queue) {
		makeStrongAndReturnIfNil(self)
        dispatch_async(queue, ^{
            callback(self.discoveredService);
        });
    };

    self.transport.resourceScanCallback = ^(NSString *host, NSString *uriPrefix, IoTTransportResourceDiscoveredCallback callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        XCTAssertEqualObjects(host, LOCALHOST, @"Host does not match");
        XCTAssertEqualObjects(uriPrefix, URI_PREFIX, @"Prefix is not correct");
        [expectation fulfill];
    };

    [self.client start];

    [self waitForExpectations:@[expectation] timeout:2];
}

- (void)testClientPlacesMediaPlayerGetRequest
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Media Player Get Request"];
    makeWeak(self)
    self.transport.serviceScanCallback = ^(NSSet<NSString *> *types, IoTTransportServiceDiscoveredCallback callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        dispatch_async(queue, ^{
            callback(self.discoveredService);
        });
    };

    self.transport.resourceScanCallback = ^(NSString *host, NSString *uriPrefix, IoTTransportResourceDiscoveredCallback callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        dispatch_async(queue, ^{
            callback(MEDIA_PLAYER_RESOURCE);
        });
    };

    self.transport.dispatcherGetCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        if ([dispatcher.uri isEqualToString:MEDIA_PLAYER_URI]) {
            [expectation fulfill];
        }
    };

    [self.client start];

    [self waitForExpectations:@[expectation] timeout:2];
}

- (void)awaitPlayerGroup
{
    self.playerGroupExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group Creation"];
    makeWeak(self)
    self.transport.serviceScanCallback = ^(NSSet<NSString *> *types, IoTTransportServiceDiscoveredCallback callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        dispatch_async(queue, ^{
            callback(self.discoveredService);
        });
    };

    self.transport.resourceScanCallback = ^(NSString *host, NSString *uriPrefix, IoTTransportResourceDiscoveredCallback callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        dispatch_async(queue, ^{
            callback(MEDIA_PLAYER_RESOURCE);
        });
    };

    [self.client start];

    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];
}

- (void)testClientCreatesPlayerGroup
{
    [self awaitPlayerGroup];
    IoTPlayerGroup *playerGroup = [[self.client getPlayerGroups] firstObject];
    XCTAssertTrue(playerGroup.isConvenienceGroup);
}

- (void)playerGroupAdded:(IoTPlayerGroup *)group
{
    XCTAssertEqual([[self.client getPlayerGroups] count], 1, @"Should be one group");
    [self.playerGroupExpectation fulfill];
}

- (void)testClientReturnsErrorWhenRenamingConvenienceGroup
{
    [self awaitPlayerGroup];
	IoTPlayerGroup *playerGroup = [[self.client getPlayerGroups] firstObject];

    XCTestExpectation *returnExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group mod return"];
    [self.client renamePlayerGroupWithID:playerGroup.groupID
                                 newName:@"NEWNAME"
                              completion:^(BOOL success, NSError * _Nullable error) {
                                  XCTAssertFalse(success, @"Rename of convenience group should fail");
                                  XCTAssertNotNil(error, @"Rename of convenience group should return non-nil error");
                                  [returnExpectation fulfill];
                              } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self waitForExpectations:@[returnExpectation] timeout:2];
}

- (void)testClientReturnsErrorWhenDeletingConvenienceGroup
{
    [self awaitPlayerGroup];
    IoTPlayerGroup *playerGroup = [[self.client getPlayerGroups] firstObject];

    XCTestExpectation *returnExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group mod return"];
    [self.client deletePlayerGroupWithID:playerGroup.groupID
                              completion:^(BOOL success, NSError * _Nullable error) {
                                  XCTAssertFalse(success, @"Delete of convenience group should fail");
                                  XCTAssertNotNil(error, @"Delete of convenience group should return non-nil error");
                                  [returnExpectation fulfill];
                              } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self waitForExpectations:@[returnExpectation] timeout:2];
}

- (void)testClientReturnsErrorWhenRemovingPlayersFromConvenienceGroup
{
    [self awaitPlayerGroup];
    XCTestExpectation *returnExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group mod return"];
    IoTPlayer *player = [[self.client getPlayers] firstObject];
    [self.client removePlayersWithIDs:[NSSet setWithObject:player.playerID]
        fromPlayerGroupWithCompletion:^(BOOL success, NSError * _Nullable error) {
            XCTAssertFalse(success, @"Remove from convenience group should fail");
            XCTAssertNotNil(error, @"Rename of convenience group should return non-nil error");
            [returnExpectation fulfill];
        } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self waitForExpectations:@[returnExpectation] timeout:2];
}

- (void)testClientReturnsErrorWhenAddingPlayersToConvenienceGroup
{
    [self awaitPlayerGroup];
    XCTestExpectation *returnExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group mod return"];
    IoTPlayer *player = [[self.client getPlayers] firstObject];
    IoTPlayerGroup *playerGroup = [[self.client getPlayerGroups] firstObject];

    makeWeak(self)
    [self.client addPlayersWithIDs:[NSSet setWithObject:player.playerID]
               toPlayerGroupWithID:playerGroup.groupID
                        completion:^(BOOL success, NSError * _Nullable error) {
                            makeStrongAndReturnIfNil(self)
                            XCTAssertFalse(success, @"Add to convenience group should fail");
                            XCTAssertNotNil(error, @"Add to convenience group should return non-nil error");
                            [returnExpectation fulfill];
                        } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self waitForExpectations:@[returnExpectation] timeout:2];
}

- (void)testClientReturnsSuccessWhenRenamingNonConvenienceGroup
{
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        if ([dispatcher.uri isEqualToString:SET_GROUP_NAME_URI]) {
			XCTAssertEqualObjects([wrapped stringValueForKey:@"groupName"], @"NEWNAME");
        }

        dispatch_async(queue, ^{
            callback([IoTWrappedRepresentation emptyWrapper], 0);
        });
    };
    
    [self awaitPlayerGroup];
    IoTPlayerGroup *playerGroup = [[self.client getPlayerGroups] firstObject];
    playerGroup.isConvenienceGroup = NO;

    XCTestExpectation *returnExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group mod return"];
    [self.client renamePlayerGroupWithID:playerGroup.groupID
                                 newName:@"NEWNAME"
                              completion:^(BOOL success, NSError * _Nullable error) {
                                  XCTAssertTrue(success, @"Rename of convenience group should not fail");
                                  XCTAssertNil(error, @"Rename of non-convenience group should return nil error");
                                  [returnExpectation fulfill];
                              } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self waitForExpectations:@[returnExpectation] timeout:2];
}

- (void)testClientReturnsErrorWhenAddingGroupWithNoPlayersPresent
{
    XCTestExpectation *returnExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Player Group mod return"];
    [self.client createPlayerGroupWithName:@"NEWGROUP"
                                completion:^(BOOL success, NSString * _Nullable groupID, NSError * _Nullable error) {
                                    XCTAssertFalse(success, @"Create of group should fail");
                                    XCTAssertNotNil(error, @"Create of group should return non-nil error");
                                    [returnExpectation fulfill];
                                } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];

    [self waitForExpectations:@[returnExpectation] timeout:2];
}

#pragma mark - Group Management

@end
