/*
 *  Copyright 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#import <XCTest/XCTest.h>

#import <Foundation/Foundation.h>
#import <MetalKit/MetalKit.h>
#import <OCMock/OCMock.h>

#import "components/renderer/metal/RTCMTLVideoView.h"

#import "api/video_frame_buffer/RTCNativeI420Buffer.h"
#import "base/RTCVideoFrameBuffer.h"
#import "components/renderer/metal/RTCMTLNV12Renderer.h"
#import "components/video_frame_buffer/RTCCVPixelBuffer.h"

static size_t kBufferWidth = 200;
static size_t kBufferHeight = 200;

// Extension of RTC_OBJC_TYPE(RTCMTLVideoView) for testing purposes.
@interface RTC_OBJC_TYPE (RTCMTLVideoView)
(Testing)

    @property(nonatomic, readonly) MTKView *metalView;

+ (BOOL)isMetalAvailable;
+ (UIView *)createMetalView:(CGRect)frame;
+ (id<RTC_OBJC_TYPE(RTCMTLRenderer)>)createNV12Renderer;
+ (id<RTC_OBJC_TYPE(RTCMTLRenderer)>)createI420Renderer;
- (void)drawInMTKView:(id)view;
@end

@interface RTCMTLVideoViewTests : XCTestCase
@property(nonatomic, strong) id classMock;
@property(nonatomic, strong) id rendererNV12Mock;
@property(nonatomic, strong) id rendererI420Mock;
@property(nonatomic, strong) id frameMock;
@end

@implementation RTCMTLVideoViewTests

@synthesize classMock = _classMock;
@synthesize rendererNV12Mock = _rendererNV12Mock;
@synthesize rendererI420Mock = _rendererI420Mock;
@synthesize frameMock = _frameMock;

- (void)setUp {
  self.classMock = OCMClassMock([RTC_OBJC_TYPE(RTCMTLVideoView) class]);
  [self startMockingNilView];
}

- (void)tearDown {
  [self.classMock stopMocking];
  [self.rendererI420Mock stopMocking];
  [self.rendererNV12Mock stopMocking];
  [self.frameMock stopMocking];
  self.classMock = nil;
  self.rendererI420Mock = nil;
  self.rendererNV12Mock = nil;
  self.frameMock = nil;
}

- (id)frameMockWithCVPixelBuffer:(BOOL)hasCVPixelBuffer {
  id frameMock = OCMClassMock([RTC_OBJC_TYPE(RTCVideoFrame) class]);
  if (hasCVPixelBuffer) {
    CVPixelBufferRef pixelBufferRef;
    CVPixelBufferCreate(kCFAllocatorDefault,
                        kBufferWidth,
                        kBufferHeight,
                        kCVPixelFormatType_420YpCbCr8Planar,
                        nil,
                        &pixelBufferRef);
    OCMStub([frameMock buffer])
        .andReturn([[RTC_OBJC_TYPE(RTCCVPixelBuffer) alloc]
            initWithPixelBuffer:pixelBufferRef]);
  } else {
    OCMStub([frameMock buffer])
        .andReturn([[RTC_OBJC_TYPE(RTCI420Buffer) alloc]
            initWithWidth:kBufferWidth
                   height:kBufferHeight]);
  }
  OCMStub([((RTC_OBJC_TYPE(RTCVideoFrame) *)frameMock) width])
      .andReturn(kBufferWidth);
  OCMStub([((RTC_OBJC_TYPE(RTCVideoFrame) *)frameMock) height])
      .andReturn(kBufferHeight);
  OCMStub([frameMock timeStampNs]).andReturn(arc4random_uniform(INT_MAX));
  return frameMock;
}

- (id)rendererMockWithSuccessfulSetup:(BOOL)success {
  id rendererMock = OCMClassMock([RTC_OBJC_TYPE(RTCMTLRenderer) class]);
  OCMStub([rendererMock addRenderingDestination:[OCMArg any]])
      .andReturn(success);
  return rendererMock;
}

- (void)startMockingNilView {
  // Use OCMock 2 syntax here until OCMock is upgraded to 3.4
  [[[self.classMock stub] andReturn:nil] createMetalView:CGRectZero];
}

#pragma mark - Test cases

- (void)testInitAssertsIfMetalUnavailabe {
  // given
  OCMStub([self.classMock isMetalAvailable]).andReturn(NO);

  // when
  BOOL asserts = NO;
  @try {
    RTC_OBJC_TYPE(RTCMTLVideoView) *realView =
        [[RTC_OBJC_TYPE(RTCMTLVideoView) alloc] initWithFrame:CGRectZero];
    (void)realView;
  } @catch (NSException *ex) {
    asserts = YES;
  }

  XCTAssertTrue(asserts);
}

- (void)testRTCVideoRenderNilFrameCallback {
  // given
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];
  self.frameMock = OCMClassMock([RTC_OBJC_TYPE(RTCVideoFrame) class]);

  [[self.frameMock reject] buffer];
  [[self.classMock reject] createNV12Renderer];
  [[self.classMock reject] createI420Renderer];

  // when
  [realView renderFrame:nil];
  [realView drawInMTKView:realView.metalView];

  // then
  [self.frameMock verify];
  [self.classMock verify];
}

- (void)testRTCVideoRenderFrameCallbackI420 {
  // given
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);
  self.rendererI420Mock = [self rendererMockWithSuccessfulSetup:YES];
  self.frameMock = [self frameMockWithCVPixelBuffer:NO];

  OCMExpect([self.rendererI420Mock drawFrame:self.frameMock]);
  OCMExpect([self.classMock createI420Renderer])
      .andReturn(self.rendererI420Mock);
  [[self.classMock reject] createNV12Renderer];

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];

  // when
  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];

  // then
  [self.rendererI420Mock verify];
  [self.classMock verify];
}

- (void)testRTCVideoRenderFrameCallbackNV12 {
  // given
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);
  self.rendererNV12Mock = [self rendererMockWithSuccessfulSetup:YES];
  self.frameMock = [self frameMockWithCVPixelBuffer:YES];

  OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]);
  OCMExpect([self.classMock createNV12Renderer])
      .andReturn(self.rendererNV12Mock);
  [[self.classMock reject] createI420Renderer];

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];

  // when
  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];

  // then
  [self.rendererNV12Mock verify];
  [self.classMock verify];
}

- (void)testRTCVideoRenderWorksAfterReconstruction {
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);
  self.rendererNV12Mock = [self rendererMockWithSuccessfulSetup:YES];
  self.frameMock = [self frameMockWithCVPixelBuffer:YES];

  OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]);
  OCMExpect([self.classMock createNV12Renderer])
      .andReturn(self.rendererNV12Mock);
  [[self.classMock reject] createI420Renderer];

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];

  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];
  [self.rendererNV12Mock verify];
  [self.classMock verify];

  // Recreate view.
  realView = [[RTC_OBJC_TYPE(RTCMTLVideoView) alloc]
      initWithFrame:CGRectMake(0, 0, 640, 480)];
  OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]);
  // View hould reinit renderer.
  OCMExpect([self.classMock createNV12Renderer])
      .andReturn(self.rendererNV12Mock);

  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];
  [self.rendererNV12Mock verify];
  [self.classMock verify];
}

- (void)testDontRedrawOldFrame {
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);
  self.rendererNV12Mock = [self rendererMockWithSuccessfulSetup:YES];
  self.frameMock = [self frameMockWithCVPixelBuffer:YES];

  OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]);
  OCMExpect([self.classMock createNV12Renderer])
      .andReturn(self.rendererNV12Mock);
  [[self.classMock reject] createI420Renderer];

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];
  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];

  [self.rendererNV12Mock verify];
  [self.classMock verify];

  [[self.rendererNV12Mock reject] drawFrame:[OCMArg any]];

  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];

  [self.rendererNV12Mock verify];
}

- (void)testDoDrawNewFrame {
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);
  self.rendererNV12Mock = [self rendererMockWithSuccessfulSetup:YES];
  self.frameMock = [self frameMockWithCVPixelBuffer:YES];

  OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]);
  OCMExpect([self.classMock createNV12Renderer])
      .andReturn(self.rendererNV12Mock);
  [[self.classMock reject] createI420Renderer];

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];
  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];

  [self.rendererNV12Mock verify];
  [self.classMock verify];

  // Get new frame.
  self.frameMock = [self frameMockWithCVPixelBuffer:YES];
  OCMExpect([self.rendererNV12Mock drawFrame:self.frameMock]);

  [realView renderFrame:self.frameMock];
  [realView drawInMTKView:realView.metalView];

  [self.rendererNV12Mock verify];
}

- (void)testReportsSizeChangesToDelegate {
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);

  id delegateMock =
      OCMProtocolMock(@protocol(RTC_OBJC_TYPE(RTCVideoViewDelegate)));
  CGSize size = CGSizeMake(640, 480);
  OCMExpect([delegateMock videoView:[OCMArg any] didChangeVideoSize:size]);

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView = [[RTC_OBJC_TYPE(RTCMTLVideoView)
      alloc] initWithFrame:CGRectMake(0, 0, 640, 480)];
  realView.delegate = delegateMock;
  [realView setSize:size];

  // Delegate method is invoked with a dispatch_async.
  OCMVerifyAllWithDelay(delegateMock, 1);
}

// TODO(b/298960678): Fix test expectations.
- (void)DISABLED_testSetContentMode {
  OCMStub([self.classMock isMetalAvailable]).andReturn(YES);
  id metalKitView = OCMClassMock([MTKView class]);
  [[[[self.classMock stub] ignoringNonObjectArgs] andReturn:metalKitView]
      createMetalView:CGRectZero];
  OCMExpect([metalKitView setContentMode:UIViewContentModeScaleAspectFill]);

  RTC_OBJC_TYPE(RTCMTLVideoView) *realView =
      [[RTC_OBJC_TYPE(RTCMTLVideoView) alloc] init];
  [realView setVideoContentMode:UIViewContentModeScaleAspectFill];

  OCMVerifyAll(metalKitView);
}

@end
