/*
 *  Copyright 2018 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>

#if defined(WEBRTC_IOS)
#import "sdk/objc/native/api/audio_device_module.h"
#endif

#include "api/scoped_refptr.h"

typedef int32_t(^NeedMorePlayDataBlock)(const size_t nSamples,
                                        const size_t nBytesPerSample,
                                        const size_t nChannels,
                                        const uint32_t samplesPerSec,
                                        void* audioSamples,
                                        size_t& nSamplesOut,
                                        int64_t* elapsed_time_ms,
                                        int64_t* ntp_time_ms);

typedef int32_t(^RecordedDataIsAvailableBlock)(const void* audioSamples,
                                               const size_t nSamples,
                                               const size_t nBytesPerSample,
                                               const size_t nChannels,
                                               const uint32_t samplesPerSec,
                                               const uint32_t totalDelayMS,
                                               const int32_t clockDrift,
                                               const uint32_t currentMicLevel,
                                               const bool keyPressed,
                                               uint32_t& newMicLevel);


// This class implements the AudioTransport API and forwards all methods to the appropriate blocks.
class MockAudioTransport : public webrtc::AudioTransport {
public:
  MockAudioTransport() {}
  ~MockAudioTransport() override {}

  void expectNeedMorePlayData(NeedMorePlayDataBlock block) {
    needMorePlayDataBlock = block;
  }

  void expectRecordedDataIsAvailable(RecordedDataIsAvailableBlock block) {
    recordedDataIsAvailableBlock = block;
  }

  int32_t NeedMorePlayData(const size_t nSamples,
                           const size_t nBytesPerSample,
                           const size_t nChannels,
                           const uint32_t samplesPerSec,
                           void* audioSamples,
                           size_t& nSamplesOut,
                           int64_t* elapsed_time_ms,
                           int64_t* ntp_time_ms) override {
    return needMorePlayDataBlock(nSamples,
                                 nBytesPerSample,
                                 nChannels,
                                 samplesPerSec,
                                 audioSamples,
                                 nSamplesOut,
                                 elapsed_time_ms,
                                 ntp_time_ms);
  }

  int32_t RecordedDataIsAvailable(const void* audioSamples,
                                  const size_t nSamples,
                                  const size_t nBytesPerSample,
                                  const size_t nChannels,
                                  const uint32_t samplesPerSec,
                                  const uint32_t totalDelayMS,
                                  const int32_t clockDrift,
                                  const uint32_t currentMicLevel,
                                  const bool keyPressed,
                                  uint32_t& newMicLevel) override {
    return recordedDataIsAvailableBlock(audioSamples,
                                        nSamples,
                                        nBytesPerSample,
                                        nChannels,
                                        samplesPerSec,
                                        totalDelayMS,
                                        clockDrift,
                                        currentMicLevel,
                                        keyPressed,
                                        newMicLevel);
  }

  void PullRenderData(int bits_per_sample,
                      int sample_rate,
                      size_t number_of_channels,
                      size_t number_of_frames,
                      void* audio_data,
                      int64_t* elapsed_time_ms,
                      int64_t* ntp_time_ms) override {}

 private:
  NeedMorePlayDataBlock needMorePlayDataBlock;
  RecordedDataIsAvailableBlock recordedDataIsAvailableBlock;
};

// Number of callbacks (input or output) the tests waits for before we set
// an event indicating that the test was OK.
static const NSUInteger kNumCallbacks = 10;
// Max amount of time we wait for an event to be set while counting callbacks.
static const NSTimeInterval kTestTimeOutInSec = 20.0;
// Number of bits per PCM audio sample.
static const NSUInteger kBitsPerSample = 16;
// Number of bytes per PCM audio sample.
static const NSUInteger kBytesPerSample = kBitsPerSample / 8;
// Average number of audio callbacks per second assuming 10ms packet size.
static const NSUInteger kNumCallbacksPerSecond = 100;
// Play out a test file during this time (unit is in seconds).
static const NSUInteger kFilePlayTimeInSec = 15;
// Run the full-duplex test during this time (unit is in seconds).
// Note that first `kNumIgnoreFirstCallbacks` are ignored.
static const NSUInteger kFullDuplexTimeInSec = 10;
// Wait for the callback sequence to stabilize by ignoring this amount of the
// initial callbacks (avoids initial FIFO access).
// Only used in the RunPlayoutAndRecordingInFullDuplex test.
static const NSUInteger kNumIgnoreFirstCallbacks = 50;

@interface RTCAudioDeviceModuleTests : XCTestCase {
  rtc::scoped_refptr<webrtc::AudioDeviceModule> audioDeviceModule;
  MockAudioTransport mock;
}

@property(nonatomic, assign) webrtc::AudioParameters playoutParameters;
@property(nonatomic, assign) webrtc::AudioParameters recordParameters;

@end

@implementation RTCAudioDeviceModuleTests

@synthesize playoutParameters;
@synthesize recordParameters;

- (void)setUp {
  [super setUp];
  audioDeviceModule = webrtc::CreateAudioDeviceModule();
  XCTAssertEqual(0, audioDeviceModule->Init());
  XCTAssertEqual(0, audioDeviceModule->GetPlayoutAudioParameters(&playoutParameters));
  XCTAssertEqual(0, audioDeviceModule->GetRecordAudioParameters(&recordParameters));
}

- (void)tearDown {
  XCTAssertEqual(0, audioDeviceModule->Terminate());
  audioDeviceModule = nullptr;
  [super tearDown];
}

- (void)startPlayout {
  XCTAssertFalse(audioDeviceModule->Playing());
  XCTAssertEqual(0, audioDeviceModule->InitPlayout());
  XCTAssertTrue(audioDeviceModule->PlayoutIsInitialized());
  XCTAssertEqual(0, audioDeviceModule->StartPlayout());
  XCTAssertTrue(audioDeviceModule->Playing());
}

- (void)stopPlayout {
  XCTAssertEqual(0, audioDeviceModule->StopPlayout());
  XCTAssertFalse(audioDeviceModule->Playing());
}

- (void)startRecording{
  XCTAssertFalse(audioDeviceModule->Recording());
  XCTAssertEqual(0, audioDeviceModule->InitRecording());
  XCTAssertTrue(audioDeviceModule->RecordingIsInitialized());
  XCTAssertEqual(0, audioDeviceModule->StartRecording());
  XCTAssertTrue(audioDeviceModule->Recording());
}

- (void)stopRecording{
  XCTAssertEqual(0, audioDeviceModule->StopRecording());
  XCTAssertFalse(audioDeviceModule->Recording());
}

- (NSURL*)fileURLForSampleRate:(int)sampleRate {
  XCTAssertTrue(sampleRate == 48000 || sampleRate == 44100 || sampleRate == 16000);
  NSString *filename = [NSString stringWithFormat:@"audio_short%d", sampleRate / 1000];
  NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:@"pcm"];
  XCTAssertNotNil(url);

  return url;
}

#pragma mark - Tests

- (void)testConstructDestruct {
  // Using the test fixture to create and destruct the audio device module.
}

- (void)testInitTerminate {
  // Initialization is part of the test fixture.
  XCTAssertTrue(audioDeviceModule->Initialized());
  XCTAssertEqual(0, audioDeviceModule->Terminate());
  XCTAssertFalse(audioDeviceModule->Initialized());
}

// Tests that playout can be initiated, started and stopped. No audio callback
// is registered in this test.
- (void)testStartStopPlayout {
  [self startPlayout];
  [self stopPlayout];
  [self startPlayout];
  [self stopPlayout];
}

// Tests that recording can be initiated, started and stopped. No audio callback
// is registered in this test.
- (void)testStartStopRecording {
  [self startRecording];
  [self stopRecording];
  [self startRecording];
  [self stopRecording];
}
// Verify that calling StopPlayout() will leave us in an uninitialized state
// which will require a new call to InitPlayout(). This test does not call
// StartPlayout() while being uninitialized since doing so will hit a
// RTC_DCHECK.
- (void)testStopPlayoutRequiresInitToRestart {
  XCTAssertEqual(0, audioDeviceModule->InitPlayout());
  XCTAssertEqual(0, audioDeviceModule->StartPlayout());
  XCTAssertEqual(0, audioDeviceModule->StopPlayout());
  XCTAssertFalse(audioDeviceModule->PlayoutIsInitialized());
}

// Verify that we can create two ADMs and start playing on the second ADM.
// Only the first active instance shall activate an audio session and the
// last active instance shall deactivate the audio session. The test does not
// explicitly verify correct audio session calls but instead focuses on
// ensuring that audio starts for both ADMs.
- (void)testStartPlayoutOnTwoInstances {
  // Create and initialize a second/extra ADM instance. The default ADM is
  // created by the test harness.
  rtc::scoped_refptr<webrtc::AudioDeviceModule> secondAudioDeviceModule =
      webrtc::CreateAudioDeviceModule();
  XCTAssertNotEqual(secondAudioDeviceModule.get(), nullptr);
  XCTAssertEqual(0, secondAudioDeviceModule->Init());

  // Start playout for the default ADM but don't wait here. Instead use the
  // upcoming second stream for that. We set the same expectation on number
  // of callbacks as for the second stream.
  mock.expectNeedMorePlayData(^int32_t(const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       void *audioSamples,
                                       size_t &nSamplesOut,
                                       int64_t *elapsed_time_ms,
                                       int64_t *ntp_time_ms) {
    nSamplesOut = nSamples;
    XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer());
    XCTAssertEqual(nBytesPerSample, kBytesPerSample);
    XCTAssertEqual(nChannels, self.playoutParameters.channels());
    XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate());
    XCTAssertNotEqual((void*)NULL, audioSamples);

    return 0;
  });

  XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock));
  [self startPlayout];

  // Initialize playout for the second ADM. If all is OK, the second ADM shall
  // reuse the audio session activated when the first ADM started playing.
  // This call will also ensure that we avoid a problem related to initializing
  // two different audio unit instances back to back (see webrtc:5166 for
  // details).
  XCTAssertEqual(0, secondAudioDeviceModule->InitPlayout());
  XCTAssertTrue(secondAudioDeviceModule->PlayoutIsInitialized());

  // Start playout for the second ADM and verify that it starts as intended.
  // Passing this test ensures that initialization of the second audio unit
  // has been done successfully and that there is no conflict with the already
  // playing first ADM.
  XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"];
  __block int num_callbacks = 0;

  MockAudioTransport mock2;
  mock2.expectNeedMorePlayData(^int32_t(const size_t nSamples,
                                        const size_t nBytesPerSample,
                                        const size_t nChannels,
                                        const uint32_t samplesPerSec,
                                        void *audioSamples,
                                        size_t &nSamplesOut,
                                        int64_t *elapsed_time_ms,
                                        int64_t *ntp_time_ms) {
    nSamplesOut = nSamples;
    XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer());
    XCTAssertEqual(nBytesPerSample, kBytesPerSample);
    XCTAssertEqual(nChannels, self.playoutParameters.channels());
    XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate());
    XCTAssertNotEqual((void*)NULL, audioSamples);
    if (++num_callbacks == kNumCallbacks) {
      [playoutExpectation fulfill];
    }

    return 0;
  });

  XCTAssertEqual(0, secondAudioDeviceModule->RegisterAudioCallback(&mock2));
  XCTAssertEqual(0, secondAudioDeviceModule->StartPlayout());
  XCTAssertTrue(secondAudioDeviceModule->Playing());
  [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil];
  [self stopPlayout];
  XCTAssertEqual(0, secondAudioDeviceModule->StopPlayout());
  XCTAssertFalse(secondAudioDeviceModule->Playing());
  XCTAssertFalse(secondAudioDeviceModule->PlayoutIsInitialized());

  XCTAssertEqual(0, secondAudioDeviceModule->Terminate());
}

// Start playout and verify that the native audio layer starts asking for real
// audio samples to play out using the NeedMorePlayData callback.
- (void)testStartPlayoutVerifyCallbacks {

  XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"];
  __block int num_callbacks = 0;
  mock.expectNeedMorePlayData(^int32_t(const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       void *audioSamples,
                                       size_t &nSamplesOut,
                                       int64_t *elapsed_time_ms,
                                       int64_t *ntp_time_ms) {
    nSamplesOut = nSamples;
    XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer());
    XCTAssertEqual(nBytesPerSample, kBytesPerSample);
    XCTAssertEqual(nChannels, self.playoutParameters.channels());
    XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate());
    XCTAssertNotEqual((void*)NULL, audioSamples);
    if (++num_callbacks == kNumCallbacks) {
      [playoutExpectation fulfill];
    }
    return 0;
  });

  XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock));

  [self startPlayout];
  [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil];
  [self stopPlayout];
}

// Start recording and verify that the native audio layer starts feeding real
// audio samples via the RecordedDataIsAvailable callback.
- (void)testStartRecordingVerifyCallbacks {
  XCTestExpectation *recordExpectation =
  [self expectationWithDescription:@"RecordedDataIsAvailable"];
  __block int num_callbacks = 0;

  mock.expectRecordedDataIsAvailable(^(const void* audioSamples,
                                       const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       const uint32_t totalDelayMS,
                                       const int32_t clockDrift,
                                       const uint32_t currentMicLevel,
                                       const bool keyPressed,
                                       uint32_t& newMicLevel) {
    XCTAssertNotEqual((void*)NULL, audioSamples);
    XCTAssertEqual(nSamples, self.recordParameters.frames_per_10ms_buffer());
    XCTAssertEqual(nBytesPerSample, kBytesPerSample);
    XCTAssertEqual(nChannels, self.recordParameters.channels());
    XCTAssertEqual((int)samplesPerSec, self.recordParameters.sample_rate());
    XCTAssertEqual(0, clockDrift);
    XCTAssertEqual(0u, currentMicLevel);
    XCTAssertFalse(keyPressed);
    if (++num_callbacks == kNumCallbacks) {
      [recordExpectation fulfill];
    }

    return 0;
  });

  XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock));
  [self startRecording];
  [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil];
  [self stopRecording];
}

// Start playout and recording (full-duplex audio) and verify that audio is
// active in both directions.
- (void)testStartPlayoutAndRecordingVerifyCallbacks {
  XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"];
  __block NSUInteger callbackCount = 0;

  XCTestExpectation *recordExpectation =
  [self expectationWithDescription:@"RecordedDataIsAvailable"];
  recordExpectation.expectedFulfillmentCount = kNumCallbacks;

  mock.expectNeedMorePlayData(^int32_t(const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       void *audioSamples,
                                       size_t &nSamplesOut,
                                       int64_t *elapsed_time_ms,
                                       int64_t *ntp_time_ms) {
    nSamplesOut = nSamples;
    XCTAssertEqual(nSamples, self.playoutParameters.frames_per_10ms_buffer());
    XCTAssertEqual(nBytesPerSample, kBytesPerSample);
    XCTAssertEqual(nChannels, self.playoutParameters.channels());
    XCTAssertEqual((int)samplesPerSec, self.playoutParameters.sample_rate());
    XCTAssertNotEqual((void*)NULL, audioSamples);
    if (callbackCount++ >= kNumCallbacks) {
      [playoutExpectation fulfill];
    }

    return 0;
  });

  mock.expectRecordedDataIsAvailable(^(const void* audioSamples,
                                       const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       const uint32_t totalDelayMS,
                                       const int32_t clockDrift,
                                       const uint32_t currentMicLevel,
                                       const bool keyPressed,
                                       uint32_t& newMicLevel) {
    XCTAssertNotEqual((void*)NULL, audioSamples);
    XCTAssertEqual(nSamples, self.recordParameters.frames_per_10ms_buffer());
    XCTAssertEqual(nBytesPerSample, kBytesPerSample);
    XCTAssertEqual(nChannels, self.recordParameters.channels());
    XCTAssertEqual((int)samplesPerSec, self.recordParameters.sample_rate());
    XCTAssertEqual(0, clockDrift);
    XCTAssertEqual(0u, currentMicLevel);
    XCTAssertFalse(keyPressed);
    [recordExpectation fulfill];

    return 0;
  });

  XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock));
  [self startPlayout];
  [self startRecording];
  [self waitForExpectationsWithTimeout:kTestTimeOutInSec handler:nil];
  [self stopRecording];
  [self stopPlayout];
}

// Start playout and read audio from an external PCM file when the audio layer
// asks for data to play out. Real audio is played out in this test but it does
// not contain any explicit verification that the audio quality is perfect.
- (void)testRunPlayoutWithFileAsSource {
  XCTAssertEqual(1u, playoutParameters.channels());

  // Using XCTestExpectation to count callbacks is very slow.
  XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"];
  const int expectedCallbackCount = kFilePlayTimeInSec * kNumCallbacksPerSecond;
  __block int callbackCount = 0;

  NSURL *fileURL = [self fileURLForSampleRate:playoutParameters.sample_rate()];
  NSInputStream *inputStream = [[NSInputStream alloc] initWithURL:fileURL];

  mock.expectNeedMorePlayData(^int32_t(const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       void *audioSamples,
                                       size_t &nSamplesOut,
                                       int64_t *elapsed_time_ms,
                                       int64_t *ntp_time_ms) {
    [inputStream read:(uint8_t *)audioSamples maxLength:nSamples*nBytesPerSample*nChannels];
    nSamplesOut = nSamples;
    if (callbackCount++ == expectedCallbackCount) {
      [playoutExpectation fulfill];
    }

    return 0;
  });

  XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock));
  [self startPlayout];
  NSTimeInterval waitTimeout = kFilePlayTimeInSec * 2.0;
  [self waitForExpectationsWithTimeout:waitTimeout handler:nil];
  [self stopPlayout];
}

- (void)testDevices {
  // Device enumeration is not supported. Verify fixed values only.
  XCTAssertEqual(1, audioDeviceModule->PlayoutDevices());
  XCTAssertEqual(1, audioDeviceModule->RecordingDevices());
}

// Start playout and recording and store recorded data in an intermediate FIFO
// buffer from which the playout side then reads its samples in the same order
// as they were stored. Under ideal circumstances, a callback sequence would
// look like: ...+-+-+-+-+-+-+-..., where '+' means 'packet recorded' and '-'
// means 'packet played'. Under such conditions, the FIFO would only contain
// one packet on average. However, under more realistic conditions, the size
// of the FIFO will vary more due to an unbalance between the two sides.
// This test tries to verify that the device maintains a balanced callback-
// sequence by running in loopback for ten seconds while measuring the size
// (max and average) of the FIFO. The size of the FIFO is increased by the
// recording side and decreased by the playout side.
// TODO(henrika): tune the final test parameters after running tests on several
// different devices.
- (void)testRunPlayoutAndRecordingInFullDuplex {
  XCTAssertEqual(recordParameters.channels(), playoutParameters.channels());
  XCTAssertEqual(recordParameters.sample_rate(), playoutParameters.sample_rate());

  XCTestExpectation *playoutExpectation = [self expectationWithDescription:@"NeedMorePlayoutData"];
  __block NSUInteger playoutCallbacks = 0;
  NSUInteger expectedPlayoutCallbacks = kFullDuplexTimeInSec * kNumCallbacksPerSecond;

  // FIFO queue and measurements
  NSMutableArray *fifoBuffer = [NSMutableArray arrayWithCapacity:20];
  __block NSUInteger fifoMaxSize = 0;
  __block NSUInteger fifoTotalWrittenElements = 0;
  __block NSUInteger fifoWriteCount = 0;

  mock.expectRecordedDataIsAvailable(^(const void* audioSamples,
                                       const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       const uint32_t totalDelayMS,
                                       const int32_t clockDrift,
                                       const uint32_t currentMicLevel,
                                       const bool keyPressed,
                                       uint32_t& newMicLevel) {
    if (fifoWriteCount++ < kNumIgnoreFirstCallbacks) {
      return 0;
    }

    NSData *data = [NSData dataWithBytes:audioSamples length:nSamples*nBytesPerSample*nChannels];
    @synchronized(fifoBuffer) {
      [fifoBuffer addObject:data];
      fifoMaxSize = MAX(fifoMaxSize, fifoBuffer.count);
      fifoTotalWrittenElements += fifoBuffer.count;
    }

    return 0;
  });

  mock.expectNeedMorePlayData(^int32_t(const size_t nSamples,
                                       const size_t nBytesPerSample,
                                       const size_t nChannels,
                                       const uint32_t samplesPerSec,
                                       void *audioSamples,
                                       size_t &nSamplesOut,
                                       int64_t *elapsed_time_ms,
                                       int64_t *ntp_time_ms) {
    nSamplesOut = nSamples;
    NSData *data;
    @synchronized(fifoBuffer) {
      data = fifoBuffer.firstObject;
      if (data) {
        [fifoBuffer removeObjectAtIndex:0];
      }
    }

    if (data) {
      memcpy(audioSamples, (char*) data.bytes, data.length);
    } else {
      memset(audioSamples, 0, nSamples*nBytesPerSample*nChannels);
    }

    if (playoutCallbacks++ == expectedPlayoutCallbacks) {
      [playoutExpectation fulfill];
    }
    return 0;
  });

  XCTAssertEqual(0, audioDeviceModule->RegisterAudioCallback(&mock));
  [self startRecording];
  [self startPlayout];
  NSTimeInterval waitTimeout = kFullDuplexTimeInSec * 2.0;
  [self waitForExpectationsWithTimeout:waitTimeout handler:nil];

  size_t fifoAverageSize =
      (fifoTotalWrittenElements == 0)
        ? 0.0
        : 0.5 + (double)fifoTotalWrittenElements / (fifoWriteCount - kNumIgnoreFirstCallbacks);

  [self stopPlayout];
  [self stopRecording];
  XCTAssertLessThan(fifoAverageSize, 10u);
  XCTAssertLessThan(fifoMaxSize, 20u);
}

@end
