/*
 *  Copyright 2016 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 <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#include "rtc_base/thread.h"
#include "test/gtest.h"

#include <vector>

#include "rtc_base/event.h"

#import "components/audio/RTCAudioSession+Private.h"

#import "components/audio/RTCAudioSession.h"
#import "components/audio/RTCAudioSessionConfiguration.h"

@interface RTC_OBJC_TYPE (RTCAudioSession)
(UnitTesting)

    @property(nonatomic, readonly)
        std::vector<__weak id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)> >
            delegates;

- (instancetype)initWithAudioSession:(id)audioSession;

@end

@interface MockAVAudioSession : NSObject

@property(nonatomic, readwrite, assign) float outputVolume;

@end

@implementation MockAVAudioSession
@synthesize outputVolume = _outputVolume;
@end

@interface RTCAudioSessionTestDelegate
    : NSObject <RTC_OBJC_TYPE (RTCAudioSessionDelegate)>

@property(nonatomic, readonly) float outputVolume;

@end

@implementation RTCAudioSessionTestDelegate

@synthesize outputVolume = _outputVolume;

- (instancetype)init {
  self = [super init];
  if (self) {
    _outputVolume = -1;
  }
  return self;
}

- (void)audioSessionDidBeginInterruption:
    (RTC_OBJC_TYPE(RTCAudioSession) *)session {
}

- (void)audioSessionDidEndInterruption:(RTC_OBJC_TYPE(RTCAudioSession) *)session
                   shouldResumeSession:(BOOL)shouldResumeSession {
}

- (void)audioSessionDidChangeRoute:(RTC_OBJC_TYPE(RTCAudioSession) *)session
                            reason:(AVAudioSessionRouteChangeReason)reason
                     previousRoute:
                         (AVAudioSessionRouteDescription *)previousRoute {
}

- (void)audioSessionMediaServerTerminated:
    (RTC_OBJC_TYPE(RTCAudioSession) *)session {
}

- (void)audioSessionMediaServerReset:(RTC_OBJC_TYPE(RTCAudioSession) *)session {
}

- (void)audioSessionShouldConfigure:(RTC_OBJC_TYPE(RTCAudioSession) *)session {
}

- (void)audioSessionShouldUnconfigure:
    (RTC_OBJC_TYPE(RTCAudioSession) *)session {
}

- (void)audioSession:(RTC_OBJC_TYPE(RTCAudioSession) *)audioSession
    didChangeOutputVolume:(float)outputVolume {
  _outputVolume = outputVolume;
}

@end

// A delegate that adds itself to the audio session on init and removes itself
// in its dealloc.
@interface RTCTestRemoveOnDeallocDelegate : RTCAudioSessionTestDelegate
@end

@implementation RTCTestRemoveOnDeallocDelegate

- (instancetype)init {
  self = [super init];
  if (self) {
    RTC_OBJC_TYPE(RTCAudioSession) *session =
        [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
    [session addDelegate:self];
  }
  return self;
}

- (void)dealloc {
  RTC_OBJC_TYPE(RTCAudioSession) *session =
      [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
  [session removeDelegate:self];
}

@end

@interface RTCAudioSessionTest : XCTestCase

@end

@implementation RTCAudioSessionTest

- (void)testAddAndRemoveDelegates {
  RTC_OBJC_TYPE(RTCAudioSession) *session =
      [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
  NSMutableArray *delegates = [NSMutableArray array];
  const size_t count = 5;
  for (size_t i = 0; i < count; ++i) {
    RTCAudioSessionTestDelegate *delegate =
        [[RTCAudioSessionTestDelegate alloc] init];
    [session addDelegate:delegate];
    [delegates addObject:delegate];
    EXPECT_EQ(i + 1, session.delegates.size());
  }
  [delegates enumerateObjectsUsingBlock:^(
                 RTCAudioSessionTestDelegate *obj, NSUInteger idx, BOOL *stop) {
    [session removeDelegate:obj];
  }];
  EXPECT_EQ(0u, session.delegates.size());
}

- (void)testPushDelegate {
  RTC_OBJC_TYPE(RTCAudioSession) *session =
      [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
  NSMutableArray *delegates = [NSMutableArray array];
  const size_t count = 2;
  for (size_t i = 0; i < count; ++i) {
    RTCAudioSessionTestDelegate *delegate =
        [[RTCAudioSessionTestDelegate alloc] init];
    [session addDelegate:delegate];
    [delegates addObject:delegate];
  }
  // Test that it gets added to the front of the list.
  RTCAudioSessionTestDelegate *pushedDelegate =
      [[RTCAudioSessionTestDelegate alloc] init];
  [session pushDelegate:pushedDelegate];
  EXPECT_TRUE(pushedDelegate == session.delegates[0]);

  // Test that it stays at the front of the list.
  for (size_t i = 0; i < count; ++i) {
    RTCAudioSessionTestDelegate *delegate =
        [[RTCAudioSessionTestDelegate alloc] init];
    [session addDelegate:delegate];
    [delegates addObject:delegate];
  }
  EXPECT_TRUE(pushedDelegate == session.delegates[0]);

  // Test that the next one goes to the front too.
  pushedDelegate = [[RTCAudioSessionTestDelegate alloc] init];
  [session pushDelegate:pushedDelegate];
  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
}

// Tests that delegates added to the audio session properly zero out. This is
// checking an implementation detail (that vectors of __weak work as expected).
- (void)testZeroingWeakDelegate {
  RTC_OBJC_TYPE(RTCAudioSession) *session =
      [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
  @autoreleasepool {
    // Add a delegate to the session. There should be one delegate at this
    // point.
    RTCAudioSessionTestDelegate *delegate =
        [[RTCAudioSessionTestDelegate alloc] init];
    [session addDelegate:delegate];
    EXPECT_EQ(1u, session.delegates.size());
    EXPECT_TRUE(session.delegates[0]);
  }
  // The previously created delegate should've de-alloced, leaving a nil ptr.
  EXPECT_FALSE(session.delegates[0]);
  RTCAudioSessionTestDelegate *delegate =
      [[RTCAudioSessionTestDelegate alloc] init];
  [session addDelegate:delegate];
  // On adding a new delegate, nil ptrs should've been cleared.
  EXPECT_EQ(1u, session.delegates.size());
  EXPECT_TRUE(session.delegates[0]);
}

// Tests that we don't crash when removing delegates in dealloc.
// Added as a regression test.
- (void)testRemoveDelegateOnDealloc {
  @autoreleasepool {
    RTCTestRemoveOnDeallocDelegate *delegate =
        [[RTCTestRemoveOnDeallocDelegate alloc] init];
    EXPECT_TRUE(delegate);
  }
  RTC_OBJC_TYPE(RTCAudioSession) *session =
      [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
  EXPECT_EQ(0u, session.delegates.size());
}

- (void)testAudioSessionActivation {
  RTC_OBJC_TYPE(RTCAudioSession) *audioSession =
      [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
  EXPECT_EQ(0, audioSession.activationCount);
  [audioSession audioSessionDidActivate:[AVAudioSession sharedInstance]];
  EXPECT_EQ(1, audioSession.activationCount);
  [audioSession audioSessionDidDeactivate:[AVAudioSession sharedInstance]];
  EXPECT_EQ(0, audioSession.activationCount);
}

// TODO(b/298960678): Fix crash when running the test on simulators.
- (void)DISABLED_testConfigureWebRTCSession {
  NSError *error = nil;

  void (^setActiveBlock)(NSInvocation *invocation) =
      ^(NSInvocation *invocation) {
        __autoreleasing NSError **retError;
        [invocation getArgument:&retError atIndex:4];
        *retError = [NSError
            errorWithDomain:@"AVAudioSession"
                       code:AVAudioSessionErrorCodeCannotInterruptOthers
                   userInfo:nil];
        BOOL failure = NO;
        [invocation setReturnValue:&failure];
      };

  id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]);
  OCMStub([[mockAVAudioSession ignoringNonObjectArgs]
                setActive:YES
              withOptions:0
                    error:([OCMArg anyObjectRef])])
      .andDo(setActiveBlock);

  id mockAudioSession =
      OCMPartialMock([RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]);
  OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession);

  RTC_OBJC_TYPE(RTCAudioSession) *audioSession = mockAudioSession;
  EXPECT_EQ(0, audioSession.activationCount);
  [audioSession lockForConfiguration];
  // configureWebRTCSession is forced to fail in the above mock interface,
  // so activationCount should remain 0
  OCMExpect([[mockAVAudioSession ignoringNonObjectArgs]
                  setActive:YES
                withOptions:0
                      error:([OCMArg anyObjectRef])])
      .andDo(setActiveBlock);
  OCMExpect([mockAudioSession session]).andReturn(mockAVAudioSession);
  EXPECT_FALSE([audioSession configureWebRTCSession:&error]);
  EXPECT_EQ(0, audioSession.activationCount);

  id session = audioSession.session;
  EXPECT_EQ(session, mockAVAudioSession);
  EXPECT_EQ(NO, [mockAVAudioSession setActive:YES withOptions:0 error:&error]);
  [audioSession unlockForConfiguration];

  // The -Wunused-value is a workaround for
  // https://bugs.llvm.org/show_bug.cgi?id=45245
  _Pragma("clang diagnostic push")
      _Pragma("clang diagnostic ignored \"-Wunused-value\"");
  OCMVerify([mockAudioSession session]);
  OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES
                                                      withOptions:0
                                                            error:&error]);
  OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:NO
                                                      withOptions:0
                                                            error:&error]);
  _Pragma("clang diagnostic pop");

  [mockAVAudioSession stopMocking];
  [mockAudioSession stopMocking];
}

// TODO(b/298960678): Fix crash when running the test on simulators.
- (void)DISABLED_testConfigureWebRTCSessionWithoutLocking {
  NSError *error = nil;

  id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]);
  id mockAudioSession =
      OCMPartialMock([RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]);
  OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession);

  RTC_OBJC_TYPE(RTCAudioSession) *audioSession = mockAudioSession;

  std::unique_ptr<webrtc::Thread> thread = webrtc::Thread::Create();
  EXPECT_TRUE(thread);
  EXPECT_TRUE(thread->Start());

  webrtc::Event waitLock;
  webrtc::Event waitCleanup;
  constexpr webrtc::TimeDelta timeout = webrtc::TimeDelta::Seconds(5);
  thread->PostTask([audioSession, &waitLock, &waitCleanup, timeout] {
    [audioSession lockForConfiguration];
    waitLock.Set();
    waitCleanup.Wait(timeout);
    [audioSession unlockForConfiguration];
  });

  waitLock.Wait(timeout);
  [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
                withOptions:0
                      error:&error];
  EXPECT_TRUE(error != nil);
  EXPECT_EQ(error.domain, kRTCAudioSessionErrorDomain);
  EXPECT_EQ(error.code, kRTCAudioSessionErrorLockRequired);
  waitCleanup.Set();
  thread->Stop();

  [mockAVAudioSession stopMocking];
  [mockAudioSession stopMocking];
}

- (void)testAudioVolumeDidNotify {
  MockAVAudioSession *mockAVAudioSession = [[MockAVAudioSession alloc] init];
  RTC_OBJC_TYPE(RTCAudioSession) *session = [[RTC_OBJC_TYPE(RTCAudioSession)
      alloc] initWithAudioSession:mockAVAudioSession];
  RTCAudioSessionTestDelegate *delegate =
      [[RTCAudioSessionTestDelegate alloc] init];
  [session addDelegate:delegate];

  float expectedVolume = 0.75;
  mockAVAudioSession.outputVolume = expectedVolume;

  EXPECT_EQ(expectedVolume, delegate.outputVolume);
}

@end
