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

#import <XCTest/XCTest.h>
#import "TestCategories.h"
#import "IoTClient+Private.h"
#import "IoTPlayer+Private.h"
#import "IoTPlayerGroup+Private.h"
#import "IoTPlaylist+Private.h"
#import "IoTMediaItem+Private.h"
#import "IoTAppListenerProtocol.h"
#import "IoTServiceDiscoveryResult.h"
#import "IoTWrappedRepresentation.h"
#import "MockTransport.h"
#import "IoTGlobals.h"
#import "IoTAllPlayController.h"
#import "IoTResourceAttributes.h"
#import "IoTProperties.h"


#define LOCALHOST @"127.0.0.1"
#define DEVICE_ID @"0"
#define DEVICE_NAME @"MOCKED DEVICE"
#define URI_PREFIX @"/qti/allplay"
#define MEDIA_PLAYER_RESOURCE @"qti.allplay.r.mediaPlayer"
#define MEDIA_PLAYER_URI @"/qti/allplay/mediaPlayer"
#define PLAYLIST_URI @"/qti/allplay/playlist"
#define PLAYLIST_GET_RANGE_URI @"/qti/allplay/playlist/playlistGetRange"
#define PLAYLIST_INSERT_URI @"/qti/allplay/playlist/playlistInsert"
#define PLAYLIST_MOVE_URI @"/qti/allplay/playlist/playlistMove"
#define PLAYLIST_DELETE_URI @"/qti/allplay/playlist/playlistDelete"

#define PLAY_INDEX_URI @"/qti/allplay/mediaPlayer/playIndex"

@interface IoTPlaylistTests : XCTestCase <IoTAppListenerProtocol>

@property (nonatomic, strong) dispatch_queue_t syncQueue;

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

@property (nonatomic, strong) XCTestExpectation *playerGroupExpectation;

@end

@implementation IoTPlaylistTests

- (void)setUp
{
    [super setUp];
    self.syncQueue = dispatch_queue_create("com.qualcomm.qti.IoTPlaylistTests", DISPATCH_QUEUE_CONCURRENT);
    self.discoveredService = [IoTServiceDiscoveryResult
                              resultWithIdentifier:DEVICE_ID
                              name:DEVICE_NAME
                              host:LOCALHOST
                              type: ALLPLAY_DEVICE_TYPE];
    self.transport = [[MockTransport alloc] init];
    self.client = [[IoTClient alloc] initWithApplicationName:@"IoTPlayerGroupTests"
                                                   transport:self.transport];
    self.client.delegate = self;

    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.transport.dispatcherGetCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        if ([dispatcher.uri isEqualToString:MEDIA_PLAYER_URI]) {
            IoTMediaPlayerAttr *mediaPlayerAttr = [[IoTMediaPlayerAttr alloc] initWithStateSyncQueue:self.syncQueue];
            IoTEnabledControlsAttr *enabledControls = [[IoTEnabledControlsAttr alloc] initWithStateSyncQueue:self.syncQueue];
            [enabledControls setLoopMode:YES];
            [enabledControls setNext:NO];
            [enabledControls setPause:YES];
            [enabledControls setPrevious:NO];
            [enabledControls setSeek:YES];
            [enabledControls setShuffleMode:NO];

            IoTPlayStateAttr *playState = [[IoTPlayStateAttr alloc] initWithStateSyncQueue:self.syncQueue];
            playState.playState = IoTPlayStatePaused;
            playState.positionMsecs = 100;
            IoTQueuedItemAttr *firstItem = [[IoTQueuedItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            IoTPlayItemAttr *firstPlayItem = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            firstPlayItem.title = @"ITEM0";
            firstItem.index = 10;
            firstItem.playItem = firstPlayItem;

            IoTQueuedItemAttr *secondItem = [[IoTQueuedItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            IoTPlayItemAttr *secondPlayItem = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            secondPlayItem.title = @"ITEM1";
            secondItem.index = 11;
            secondItem.playItem = secondPlayItem;
            NSArray <IoTQueuedItemAttr *> *queued = @[firstItem,secondItem];
            playState.queuedItems = queued;

            mediaPlayerAttr.capabilities = @[@"audio"];
            mediaPlayerAttr.displayName = DEVICE_NAME;
            mediaPlayerAttr.enabledControls = enabledControls;
            mediaPlayerAttr.loopMode = IoTLoopModeOne;
            mediaPlayerAttr.playState = playState;
            mediaPlayerAttr.shuffleMode = IoTShuffleModeShuffle;
            IoTWrappedRepresentation *wrapped = [mediaPlayerAttr packAll];
            dispatch_async(queue, ^{
                callback(wrapped, 0);
            });
        } else if ([dispatcher.uri isEqualToString:PLAYLIST_URI]) {
            IoTPlaylistAttr *playlistAttr = [[IoTPlaylistAttr alloc] initWithStateSyncQueue:self.syncQueue];
            playlistAttr.userData = @"USERDATA";
            playlistAttr.ownerInfo = @"TESTS";
            IoTWrappedRepresentation *wrapped = [playlistAttr packAll];
            dispatch_async(queue, ^{
                callback(wrapped, 0);
            });
        } else {
            dispatch_async(queue, ^{
                callback([IoTWrappedRepresentation emptyWrapper], 0);
            });
        }
    };
}

- (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)testPlaylistReturnsCorrectValues
{
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        if ([dispatcher.uri isEqualToString:PLAYLIST_GET_RANGE_URI]) {
            IoTPlaylistGetRangeOutAttr *playlistOutAttr = [[IoTPlaylistGetRangeOutAttr alloc] initWithStateSyncQueue:self.syncQueue];
            [playlistOutAttr setLatestSnapshotId:@"VALID"];
            [playlistOutAttr setTotalSize:3];
            IoTPlayItemAttr *item0 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item0.title = @"ITEM0";

            IoTPlayItemAttr *item1 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item1.title = @"ITEM1";

            IoTPlayItemAttr *item2 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item2.title = @"ITEM2";

            [playlistOutAttr setItemsInRange:@[item0, item1, item2]];
            IoTWrappedRepresentation *wrapped = [playlistOutAttr packAll];
            dispatch_async(queue, ^{
                callback(wrapped, 0);
            });
        } else {
            dispatch_async(queue, ^{
                callback([IoTWrappedRepresentation emptyWrapper], 0);
            });
        }
    };

    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    XCTAssertNotNil(group, @"Group should not be nil");
    IoTPlaylist *playlist = [group playlist];
    XCTAssertNotNil(playlist, @"Playlist should not be nil");

    XCTAssertEqualObjects(playlist.userData, @"USERDATA", @"Playlist User Data incorrect");
    XCTAssertEqual(playlist.count, 3, @"Playlist should have 3 items");
    XCTAssertTrue(playlist.isLoopModeEnabled, @"Loop mode should be enabled");
    XCTAssertFalse(playlist.isShuffleModeEnabled, @"Shuffle should not be enabled");
    XCTAssertEqual(playlist.loopMode, IoTLoopModeOne, @"Incorrect loop mode");
    XCTAssertEqual(playlist.shuffleMode, IoTShuffleModeShuffle, @"Incorrect Shuffle Mode");

    IoTMediaItem *current = playlist.currentItem;
    XCTAssertNotNil(current, @"currentitem should not be nil");
    XCTAssertFalse(current.isEmpty, @"currentItem should not be empty");
    XCTAssertEqualObjects(current.title, @"ITEM0", @"currentItem title incorrect");

    XCTAssertEqual(playlist.indexPlaying, 10, @"Playing index should be 10");

    XCTestExpectation *playlistGetExpectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Playlist Get"];
    [playlist getItemsInRange:NSMakeRange(0, 3)
                   completion:^(BOOL success, NSArray<IoTMediaItem *> *items, NSError *error) {
                       XCTAssertTrue(success, @"success should be true");
                       XCTAssertNil(error, @"Error should be nil");
                       XCTAssertEqual(items.count, 3, @"There should be three items");
                       XCTAssertEqualObjects(items[0].title, @"ITEM0", @"Title incorrect for item");
                       XCTAssertEqualObjects(items[1].title, @"ITEM1", @"Title incorrect for item");
                       XCTAssertEqualObjects(items[2].title, @"ITEM2", @"Title incorrect for item");
                       [playlistGetExpectation fulfill];
                   } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    [self waitForExpectations:@[playlistGetExpectation] timeout:5];
}

- (void)testPlaylistSetShuffleMode
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Media Player Post"];
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)
        
        if ([dispatcher.uri isEqualToString:MEDIA_PLAYER_URI]) {
            XCTAssertEqualObjects([wrapped stringValueForKey:[NSString stringWithUTF8String:kShuffleMode]], @"linear", @"Shuffle mode not set as expected");
        }
        dispatch_async(queue, ^{
            callback([IoTWrappedRepresentation emptyWrapper], 0);
        });
    };

    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    IoTPlaylist *playlist = [group playlist];
    
    [playlist setShuffleMode:IoTShuffleModeLinear
                  completion:^(BOOL success, NSError * _Nullable error) {
                      XCTAssertTrue(success);
                      [expectation fulfill];
                  } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    
    [self waitForExpectations:@[expectation] timeout:5];
}

- (void)testPlaylistSetLoopMode
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Media Player Post"];
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)

        if ([dispatcher.uri isEqualToString:MEDIA_PLAYER_URI]) {
            XCTAssertEqualObjects([wrapped stringValueForKey:[NSString stringWithUTF8String:kLoopMode]], @"one", @"Loop mode not set as expected");
        }
        dispatch_async(queue, ^{
            callback([IoTWrappedRepresentation emptyWrapper], 0);
        });
    };

    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    IoTPlaylist *playlist = [group playlist];

    [playlist setLoopMode:IoTLoopModeOne
               completion:^(BOOL success, NSError * _Nullable error) {
                   XCTAssertTrue(success);
                   [expectation fulfill];
               } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];

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

- (void)testPlayItemAtIndex
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Play Index Post"];
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)

        if ([dispatcher.uri isEqualToString:PLAY_INDEX_URI]) {
            XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kItemIndex]],2);
        }
        dispatch_async(queue, ^{
            callback([IoTWrappedRepresentation emptyWrapper], 0);
        });
    };

    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    IoTPlaylist *playlist = [group playlist];

    [playlist playItemAtIndex:2
                   completion:^(BOOL success, NSError * _Nullable error) {
                       XCTAssertTrue(success);
                       [expectation fulfill];
                   } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];

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

- (void)testPlaylistAddItem
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Playlist Add Post"];
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)

        if ([dispatcher.uri isEqualToString:PLAYLIST_GET_RANGE_URI]) {
            IoTPlaylistGetRangeOutAttr *playlistOutAttr = [[IoTPlaylistGetRangeOutAttr alloc] initWithStateSyncQueue:self.syncQueue];
            [playlistOutAttr setLatestSnapshotId:@"VALID"];
            [playlistOutAttr setTotalSize:3];
            IoTPlayItemAttr *item0 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item0.title = @"ITEM0";

            IoTPlayItemAttr *item1 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item1.title = @"ITEM1";

            IoTPlayItemAttr *item2 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item2.title = @"ITEM2";
            
            [playlistOutAttr setItemsInRange:@[item0, item1, item2]];
            IoTWrappedRepresentation *wrapped = [playlistOutAttr packAll];
            dispatch_async(queue, ^{
                callback(wrapped, 0);
            });
        } else if ([dispatcher.uri isEqualToString:PLAYLIST_INSERT_URI]) {
            XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kPosition]],1);
            NSString *snapshotID = [wrapped stringValueForKey:[NSString stringWithUTF8String:kSnapshotId]];
            XCTAssertEqualObjects(snapshotID, @"VALID");
            [expectation fulfill];
        } else {
            dispatch_async(queue, ^{
                callback([IoTWrappedRepresentation emptyWrapper], 0);
            });
        }
    };
    
    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    IoTPlaylist *playlist = [group playlist];

    IoTMediaItem *item = [[IoTMediaItem alloc] init];
    item.title = @"ADDED";

    [playlist addMediaItem:item
                   atIndex:1
                      play:NO
                  userData:@"USERDATA2"
                completion:nil
           completionQueue:nil];

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

- (void)testPlaylistMoveItem
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Playlist Move Post"];
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)

        if ([dispatcher.uri isEqualToString:PLAYLIST_GET_RANGE_URI]) {
            IoTPlaylistGetRangeOutAttr *playlistOutAttr = [[IoTPlaylistGetRangeOutAttr alloc] initWithStateSyncQueue:self.syncQueue];
            [playlistOutAttr setLatestSnapshotId:@"VALID"];
            [playlistOutAttr setTotalSize:3];
            IoTPlayItemAttr *item0 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item0.title = @"ITEM0";

            IoTPlayItemAttr *item1 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item1.title = @"ITEM1";

            IoTPlayItemAttr *item2 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item2.title = @"ITEM2";

            [playlistOutAttr setItemsInRange:@[item0, item1, item2]];
            IoTWrappedRepresentation *wrapped = [playlistOutAttr packAll];
            dispatch_async(queue, ^{
                callback(wrapped, 0);
            });
        } else {
            if ([dispatcher.uri isEqualToString:PLAYLIST_MOVE_URI]) {
                XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kCount]],1);
                XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kStart]],0);
                XCTAssertEqualObjects([wrapped stringValueForKey:[NSString stringWithUTF8String:kSnapshotId]], @"VALID");
                XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kPosition]],2);
            }
            dispatch_async(queue, ^{
                callback([IoTWrappedRepresentation emptyWrapper], 0);
            });
        }
    };

    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    IoTPlaylist *playlist = [group playlist];

    [playlist moveMediaItemsInRange:NSMakeRange(0, 1)
                           position:2
                         completion:^(BOOL success, NSError * _Nullable error) {
                             XCTAssertTrue(success);
                             [expectation fulfill];
                         } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];

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

- (void)testPlaylistClear
{
    XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Wait for Playlist Move Post"];
    makeWeak(self)
    self.transport.dispatcherPostCallback = ^(MockDispatcher *dispatcher, IoTQueryParams params, IoTWrappedRepresentation *wrapped, IoTTransportDispatcherCompletion callback, dispatch_queue_t queue) {
        makeStrongAndReturnIfNil(self)

        if ([dispatcher.uri isEqualToString:PLAYLIST_GET_RANGE_URI]) {
            IoTPlaylistGetRangeOutAttr *playlistOutAttr = [[IoTPlaylistGetRangeOutAttr alloc] initWithStateSyncQueue:self.syncQueue];
            [playlistOutAttr setLatestSnapshotId:@"VALID"];
            [playlistOutAttr setTotalSize:3];
            IoTPlayItemAttr *item0 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item0.title = @"ITEM0";

            IoTPlayItemAttr *item1 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item1.title = @"ITEM1";

            IoTPlayItemAttr *item2 = [[IoTPlayItemAttr alloc] initWithStateSyncQueue:self.syncQueue];
            item2.title = @"ITEM2";

            [playlistOutAttr setItemsInRange:@[item0, item1, item2]];
            IoTWrappedRepresentation *wrapped = [playlistOutAttr packAll];
            dispatch_async(queue, ^{
                callback(wrapped, 0);
            });
        } else {
            if ([dispatcher.uri isEqualToString:PLAYLIST_DELETE_URI]) {
                XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kCount]],3);
                XCTAssertEqual([wrapped intValueForKey:[NSString stringWithUTF8String:kStart]],0);
                XCTAssertEqualObjects([wrapped stringValueForKey:[NSString stringWithUTF8String:kSnapshotId]], @"VALID");
            }
            dispatch_async(queue, ^{
                callback([IoTWrappedRepresentation emptyWrapper], 0);
            });
        }
    };

    [self.client start];
    [self waitForExpectations:@[self.playerGroupExpectation] timeout:5];

    IoTPlayerGroup *group = [[self.client getPlayerGroups] firstObject];
    IoTPlaylist *playlist = [group playlist];

    [playlist clearWithCompletion:^(BOOL success, NSError * _Nullable error) {
        XCTAssertTrue(success);
        [expectation fulfill];
    } completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];

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

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

@end
