// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import <XCTest/XCTest.h>

#import <GoogleUtilities/GULUserDefaults.h>

#import "FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.h"
#import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
#import "FirebasePerformance/Sources/Common/FPRConstants.h"
#import "FirebasePerformance/Sources/Configurations/FPRConfigurations+Private.h"
#import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
#import "FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags+Private.h"
#import "FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.h"
#import "FirebasePerformance/Sources/FPRClient.h"
#import "FirebasePerformance/Sources/Public/FirebasePerformance/FIRPerformance.h"
#import "FirebasePerformance/Sources/Public/FirebasePerformance/FIRTrace.h"
#import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
#import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"

#import "FirebasePerformance/Tests/Unit/Configurations/FPRFakeRemoteConfig.h"
#import "FirebasePerformance/Tests/Unit/FPRTestCase.h"

#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"

#import <OCMock/OCMock.h>

@interface FIRTraceTest : FPRTestCase

@end

@implementation FIRTraceTest

- (void)setUp {
  [super setUp];
  FIRPerformance *performance = [FIRPerformance sharedInstance];
  [performance setDataCollectionEnabled:YES];
  [[FPRClient sharedInstance] disableInstrumentation];
}

- (void)tearDown {
  [super tearDown];
  FIRPerformance *performance = [FIRPerformance sharedInstance];
  [performance setDataCollectionEnabled:NO];
  [[FPRClient sharedInstance] disableInstrumentation];
}

/** Validates that init with a valid name returns a trace. */
- (void)testInit {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  XCTAssertNotNil(trace);
}

/** Validates that init with an empty name throws exception. */
- (void)testInitWithEmptyName {
  XCTAssertThrows([[FIRTrace alloc] initWithName:@""]);
}

#pragma mark - Trace creation tests

/** Validates if trace creation fails when SDK flag is disabled in remote config. */
- (void)testTraceCreationWhenSDKFlagDisabled {
  FPRConfigurations *configurations = [FPRConfigurations sharedInstance];
  FPRFakeRemoteConfig *remoteConfig = [[FPRFakeRemoteConfig alloc] init];

  FPRRemoteConfigFlags *configFlags =
      [[FPRRemoteConfigFlags alloc] initWithRemoteConfig:(FIRRemoteConfig *)remoteConfig];
  configFlags.appStartConfigFetchDelayInSeconds = 0.0;
  configurations.remoteConfigFlags = configFlags;

  NSData *valueData = [@"false" dataUsingEncoding:NSUTF8StringEncoding];
  FIRRemoteConfigValue *value =
      [[FIRRemoteConfigValue alloc] initWithData:valueData source:FIRRemoteConfigSourceRemote];
  [remoteConfig.configValues setObject:value forKey:@"fpr_enabled"];

  // Trigger the RC config fetch
  remoteConfig.lastFetchTime = nil;
  configFlags.appStartConfigFetchDelayInSeconds = 0.0;
  [configFlags update];

  XCTAssertNil([[FIRTrace alloc] initWithName:@"Random"]);
}

/** Validates if trace creation succeeds when SDK flag is enabled in remote config. */
- (void)testTraceCreationWhenSDKFlagEnabled {
  FPRConfigurations *configurations = [FPRConfigurations sharedInstance];
  FPRFakeRemoteConfig *remoteConfig = [[FPRFakeRemoteConfig alloc] init];

  FPRRemoteConfigFlags *configFlags =
      [[FPRRemoteConfigFlags alloc] initWithRemoteConfig:(FIRRemoteConfig *)remoteConfig];
  configurations.remoteConfigFlags = configFlags;

  GULUserDefaults *userDefaults = [[GULUserDefaults alloc] init];
  configFlags.userDefaults = userDefaults;

  NSString *configKey = [NSString stringWithFormat:@"%@.%@", kFPRConfigPrefix, @"fpr_enabled"];
  [userDefaults setObject:@(TRUE) forKey:configKey];

  XCTAssertNotNil([[FIRTrace alloc] initWithName:@"Random"]);
}

/** Validates if trace creation fails when SDK flag is enabled in remote config, but data collection
 * disabled. */
- (void)testTraceCreationWhenSDKFlagEnabledWithDataCollectionDisabled {
  [[FIRPerformance sharedInstance] setDataCollectionEnabled:NO];
  FPRConfigurations *configurations = [FPRConfigurations sharedInstance];
  FPRFakeRemoteConfig *remoteConfig = [[FPRFakeRemoteConfig alloc] init];

  FPRRemoteConfigFlags *configFlags =
      [[FPRRemoteConfigFlags alloc] initWithRemoteConfig:(FIRRemoteConfig *)remoteConfig];
  configurations.remoteConfigFlags = configFlags;

  NSData *valueData = [@"true" dataUsingEncoding:NSUTF8StringEncoding];
  FIRRemoteConfigValue *value =
      [[FIRRemoteConfigValue alloc] initWithData:valueData source:FIRRemoteConfigSourceRemote];
  [remoteConfig.configValues setObject:value forKey:@"fpr_enabled"];

  XCTAssertNil([[FIRTrace alloc] initWithName:@"Random"]);
}

#pragma mark - Stages related testing

/** Validates that stages are created. */
- (void)testStaging {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace startStageNamed:@"1"];
  [trace startStageNamed:@"2"];
  [trace stop];
  XCTAssertEqual(trace.stages.count, 2);
}

/** Validates that stages are not created without calling a start on the trace. */
- (void)testStageWithoutStart {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace startStageNamed:@"1"];
  XCTAssertEqual(trace.stages.count, 0);
}

/** Validates that stages are not created without calling a start on the trace, but calling stop. */
- (void)testStageWithoutStartWithStop {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace startStageNamed:@"1"];
  [trace stop];
  XCTAssertEqual(trace.stages.count, 0);
}

/** Validates that stages are not created after calling stop on the trace. */
- (void)testStageAfterStop {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace startStageNamed:@"1"];
  [trace startStageNamed:@"2"];
  [trace stop];
  [trace startStageNamed:@"3"];
  XCTAssertEqual(trace.stages.count, 2);
}

/** Validates that stopping a stage does not trigger an event being sent to Fll */
- (void)testStageStopDoesNotTriggerEventSend {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  id mock = [OCMockObject partialMockForObject:[FPRClient sharedInstance]];
  OCMStub([mock logTrace:[OCMArg any]]).andDo(nil);
  [trace start];
  [trace startStageNamed:@"1"];
  [[mock reject] logTrace:trace.activeStage];
  [trace startStageNamed:@"2"];
  [trace stop];
}

/** Validates that stopping a trace does trigger an event being sent to Fll. */
- (void)testTraceStopDoesTriggerEventSend {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  id mock = [OCMockObject partialMockForObject:[FPRClient sharedInstance]];
  OCMStub([mock logTrace:[OCMArg any]]).andDo(nil);
  [trace start];
  [trace startStageNamed:@"1"];
  [trace startStageNamed:@"2"];
  [trace stop];
  OCMVerify([mock logTrace:trace]);
}

/** Validates that the name of the trace is dropped if its length is above max admissible length. */
- (void)testNameLengthMax {
  NSString *testName = [@"abc" stringByPaddingToLength:kFPRMaxNameLength + 1
                                            withString:@"-"
                                       startingAtIndex:0];
  XCTAssertThrows([[FIRTrace alloc] initWithName:testName]);
}

/** Validates that the name cannot have a prefix of underscore. */
- (void)testNamePrefixSrtipped {
  NSString *testName = [NSString stringWithFormat:@"%@test", kFPRInternalNamePrefix];
  XCTAssertThrows([[FIRTrace alloc] initWithName:testName]);
}

#pragma mark - Metric related testing

/** Validates that metric with greater than max length is not created on setMetric. */
- (void)testSetMetricNameLengthMax {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  NSString *testName = [@"abc" stringByPaddingToLength:kFPRMaxNameLength + 1
                                            withString:@"-"
                                       startingAtIndex:0];
  [trace start];
  [trace setIntValue:10 forMetric:testName];
  [trace stop];
  XCTAssertNil([trace.counters objectForKey:testName]);
}

/** Validates that metric with empty name is not created on setMetric. */
- (void)testSetOrIncrementMetricNameLengthZero {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  NSString *testName = @"";
  [trace start];
  [trace setIntValue:10 forMetric:testName];
  [trace incrementMetric:testName byInt:10];
  [trace stop];
  XCTAssertNil([trace.counters objectForKey:testName]);
}

/** Validates that metrics are not set when a trace is not started. */
- (void)testSetOrIncrementMetricWithoutStart {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setIntValue:10 forMetric:@"testing"];
  [trace incrementMetric:@"testing" byInt:10];
  [trace stop];
  XCTAssertNil([trace.counters objectForKey:@"testing"]);
}

/** Validates that calling get on a metric returns 0 if it hasn't been reviously set. */
- (void)testGetMetricWhenSetHasntBeenCalledReturnsZero {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  int64_t metricValue = [trace valueForIntMetric:@"testing"];
  [trace stop];
  XCTAssertEqual(metricValue, 0);
}

/** Validates that calling get on a metric without calling set doesn't create a new metric. */
- (void)testGetMetricWhenSetHasntBeenCalledDoesntCreateMetric {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace valueForIntMetric:@"testing"];
  [trace stop];
  id metricValue = [trace.counters objectForKey:@"testing"];
  XCTAssertNil(metricValue);
}

/** Tests that calling set multiple times on a metric results in it holding just the last value. */
- (void)testMultipleSetsOnAMetricResultInHoldingJustTheLastValue {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setIntValue:10 forMetric:@"testing"];
  [trace setIntValue:100 forMetric:@"testing"];
  [trace stop];
  int64_t metricValue = [trace valueForIntMetric:@"testing"];
  XCTAssertEqual(metricValue, 100);
}

/** Validates that incrementing a metric that has been previously set increments previous value. */
- (void)testIncrementingAfterSettingMetric {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setIntValue:10 forMetric:@"testing"];
  [trace incrementMetric:@"testing" byInt:25];
  [trace stop];
  int64_t metricValue = [trace valueForIntMetric:@"testing"];
  XCTAssertEqual(metricValue, 35);
}

/** Validates that calling setMetric on a trace also sets it on the active stage. */
- (void)testSetMetricCalledOnTraceAlsoSetsMetricOnStage {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace startStageNamed:@"stage 1"];
  [trace setIntValue:35 forMetric:@"testing"];
  [trace startStageNamed:@"stage 2"];
  [trace setIntValue:40 forMetric:@"testing"];
  [trace stop];
  XCTAssertEqual([trace valueForIntMetric:@"testing"], 40);
  for (FIRTrace *stage in trace.stages) {
    if ([stage.name isEqualToString:@"stage 1"]) {
      XCTAssertEqual([stage valueForIntMetric:@"testing"], 35);
    } else if ([stage.name isEqualToString:@"stage 2"]) {
      XCTAssertEqual([stage valueForIntMetric:@"testing"], 40);
    }
  }
}

/** Validates that deleting a metric deletes it. */
- (void)testDeleteMetricDeletesAMetric {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setIntValue:1 forMetric:@"testing"];
  [trace deleteMetric:@"testing"];
  [trace stop];
  XCTAssertNil(trace.counters[@"testing"]);
}

/** Validates that deleting a metric doesnt affect other metrics. */
- (void)testDeleteMetricDoesntDeleteAnotherMetric {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setIntValue:1 forMetric:@"testing"];
  [trace setIntValue:1 forMetric:@"testing2"];
  [trace deleteMetric:@"testing"];
  [trace stop];
  XCTAssertNil(trace.counters[@"testing"]);
  XCTAssertEqual([trace valueForIntMetric:@"testing2"], 1);
}

/** Validates that trying to delete a non-existent metric doesnt affect anything. */
- (void)testDeletingMetricThatDoesntExistDoesntDoAnything {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setIntValue:1 forMetric:@"testing"];
  [trace deleteMetric:@"testing2"];
  [trace stop];
  XCTAssertEqual([trace valueForIntMetric:@"testing"], 1);
}

/** Tests deleting a metric also deletes it from the active stage if it exists. */
- (void)testDeleteMetricAlsoDeletesItFromActiveStage {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace startStageNamed:@"stage 1"];
  FIRTrace *activeStage = trace.activeStage;
  [trace setIntValue:1 forMetric:@"testing"];
  [trace setIntValue:1 forMetric:@"testing2"];
  [trace deleteMetric:@"testing"];
  [trace stop];
  XCTAssertEqual([trace valueForIntMetric:@"testing2"], 1);
  XCTAssertEqual([activeStage valueForIntMetric:@"testing2"], 1);
  XCTAssertNil(trace.counters[@"testing"]);
  XCTAssertNil(activeStage.counters[@"testing"]);
}

/** Tests that deleteMetric has no effect after the trace has been stopped. */
- (void)testDeleteMetricDoesNothingAfterTraceHasBeenStopped {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setIntValue:1 forMetric:@"testing"];
  [trace stop];
  [trace deleteMetric:@"testing"];
  XCTAssertEqual([trace valueForIntMetric:@"testing"], 1);
}

#pragma mark - Metrics related testing

/** Validates that counters are incremented. */
- (void)testMetricNameLengthMax {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  NSString *testName = [@"abc" stringByPaddingToLength:kFPRMaxNameLength + 1
                                            withString:@"-"
                                       startingAtIndex:0];
  [trace start];
  [trace incrementMetric:testName byInt:5];
  [trace stop];
  XCTAssertNil([trace.counters objectForKey:testName]);
}

/** Validates that traces could start with a custom start time. */
- (void)testStartTraceWithStartTimeAndStageDefined {
  NSDate *traceStartTime = [NSDate date];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"testTrace"];
  [trace startWithStartTime:traceStartTime];
  XCTAssertEqual(trace.startTimeSinceEpoch, [traceStartTime timeIntervalSince1970]);
  [trace startStageNamed:@"testStage" startTime:traceStartTime];
  [trace stop];
  XCTAssertEqual(trace.stages.count, 1);
  FIRTrace *stage = trace.stages.lastObject;
  XCTAssertEqual(stage.startTimeSinceEpoch, [traceStartTime timeIntervalSince1970]);
}

- (void)testMetrics {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace incrementMetric:@"testing" byInt:3];
  [trace incrementMetric:@"testing" byInt:2];
  [trace stop];
  NSUInteger metricValue = [[trace.counters objectForKey:@"testing"] integerValue];
  XCTAssertEqual(metricValue, 5);
}

/** Validates that metrics are not incremented when a trace is not started. */
- (void)testMetricsWithoutStart {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace incrementMetric:@"testing" byInt:3];
  [trace incrementMetric:@"testing" byInt:2];
  [trace stop];
  NSUInteger metricValue = [[trace.counters objectForKey:@"testing"] integerValue];
  XCTAssertEqual(metricValue, 0);
}

/** Validates that trace without complete data is invalid. */
- (void)testInvalidTraceValidationCheck {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace stop];
  XCTAssertFalse([trace isCompleteAndValid]);
}

/** Validates that valid traces with stages and metrics are marked as valid. */
- (void)testValidTraceWithStageAndMetrics {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace incrementMetric:@"counter 1" byInt:1];
  [trace incrementMetric:@"counter 2" byInt:1];
  [trace startStageNamed:@"1"];
  [trace incrementMetric:@"counter 1" byInt:1];
  [trace incrementMetric:@"counter 2" byInt:1];
  [trace startStageNamed:@"2"];
  [trace incrementMetric:@"counter 1" byInt:1];
  [trace incrementMetric:@"counter 2" byInt:1];
  [trace stop];
  XCTAssertTrue([trace isCompleteAndValid]);
}

/** Validates the value of background state when the app is backgrounded. */
- (void)testValidTraceWithBackgrounding {
  XCTestExpectation *expectation = [self expectationWithDescription:@"Application state change"];
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  dispatch_async(dispatch_get_main_queue(), ^{
    [defaultCenter postNotificationName:UIApplicationDidEnterBackgroundNotification
                                 object:[UIApplication sharedApplication]];
    [defaultCenter postNotificationName:UIApplicationDidBecomeActiveNotification
                                 object:[UIApplication sharedApplication]];
    [expectation fulfill];
  });

  [self waitForExpectationsWithTimeout:5.0
                               handler:^(NSError *_Nullable error) {
                                 if (error) {
                                   XCTFail(@"Expectation failed with error: %@", error);
                                 } else {
                                   XCTAssertEqual(trace.backgroundTraceState,
                                                  FPRTraceStateBackgroundAndForeground);
                                   [trace stop];
                                 }
                               }];
}

/** Validates the value of background state when trace is not started. */
- (void)testValidTraceWithoutStart {
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [defaultCenter postNotificationName:UIApplicationDidEnterBackgroundNotification
                               object:[UIApplication sharedApplication]];
  [defaultCenter postNotificationName:UIApplicationDidBecomeActiveNotification
                               object:[UIApplication sharedApplication]];
  [trace stop];
  XCTAssertEqual(trace.backgroundTraceState, FPRTraceStateUnknown);
}

/** Validates the value of background state is available after trace is stopped. */
- (void)testBackgroundStateAfterTraceStop {
  XCTestExpectation *expectation = [self expectationWithDescription:@"Application state change"];

  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  dispatch_async(dispatch_get_main_queue(), ^{
    [defaultCenter postNotificationName:UIApplicationDidEnterBackgroundNotification
                                 object:[UIApplication sharedApplication]];
    [defaultCenter postNotificationName:UIApplicationDidBecomeActiveNotification
                                 object:[UIApplication sharedApplication]];
    [expectation fulfill];
  });

  [self waitForExpectationsWithTimeout:5.0
                               handler:^(NSError *_Nullable error) {
                                 if (error) {
                                   XCTFail(@"Expectation failed with error: %@", error);
                                 } else {
                                   [trace stop];
                                   XCTAssertEqual(trace.backgroundTraceState,
                                                  FPRTraceStateBackgroundAndForeground);
                                 }
                               }];
}

/** Validates that stages do not have any valid background state. */
- (void)testValidTraceWithActiveStageHavingNoBackgroundState {
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace startStageNamed:@"RandomStage"];
  [defaultCenter postNotificationName:UIApplicationDidEnterBackgroundNotification
                               object:[UIApplication sharedApplication]];
  [defaultCenter postNotificationName:UIApplicationDidBecomeActiveNotification
                               object:[UIApplication sharedApplication]];
  [trace stop];
  XCTAssertEqual(trace.stages.count, 1);
  FIRTrace *activeStage = [trace.stages lastObject];
  XCTAssertEqual(activeStage.backgroundTraceState, FPRTraceStateUnknown);
}

/** Validates that internal trace names allow the reserved prefix value. */
- (void)testInternalTraceCreationWithInternalPrefix {
  FIRTrace *trace = [[FIRTrace alloc] initInternalTraceWithName:@"_Random"];
  XCTAssertNotNil(trace);
  NSString *metricName = @"_counter";
  [trace start];
  [trace startStageNamed:@"_1"];
  [trace incrementMetric:metricName byInt:5];
  [trace stop];

  XCTAssertEqual(trace.stages.count, 1);
  FIRTrace *stage1 = [trace.stages lastObject];
  XCTAssertEqual(stage1.name, @"_1");
  NSUInteger metricValue = [[trace.counters objectForKey:metricName] integerValue];
  XCTAssertEqual(metricValue, 5);
}

/** Validates if the metric is incremented if a trace is started but not stopped. */
- (void)testTraceStartedNotStoppedIncrementsAMetric {
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  [defaultCenter postNotificationName:UIWindowDidBecomeVisibleNotification
                               object:[UIApplication sharedApplication]];
  [defaultCenter postNotificationName:UIApplicationDidBecomeActiveNotification
                               object:[UIApplication sharedApplication]];
  FIRTrace *activeTrace = [FPRAppActivityTracker sharedInstance].activeTrace;
  NSNumber *metric = [activeTrace.counters objectForKey:kFPRAppCounterNameTraceNotStopped];
  NSInteger metricValue = [metric integerValue];
  __weak FIRTrace *weakReferencedTrace;
  @autoreleasepool {
    FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
    weakReferencedTrace = trace;
    [trace start];
    [trace startStageNamed:@"1"];
  }

  XCTestExpectation *expectation = [self expectationWithDescription:@"Expectation - Wait for 2s"];
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
                 dispatch_get_main_queue(), ^{
                   [expectation fulfill];
                   XCTAssertNil(weakReferencedTrace);
                 });
  [self waitForExpectationsWithTimeout:10.0 handler:nil];

  NSNumber *updatedMetric = [activeTrace.counters objectForKey:kFPRAppCounterNameTraceNotStopped];
  NSInteger updatedMetricValue = [updatedMetric integerValue];
  XCTAssertEqual(updatedMetricValue - metricValue, 1);
  [defaultCenter postNotificationName:UIApplicationWillResignActiveNotification
                               object:[UIApplication sharedApplication]];
}

/** Validates if the metric is not incremented if a trace is started and stopped. */
- (void)testTraceStartedAndStoppedDoesNotIncrementAMetric {
  FIRTrace *activeTrace = [FPRAppActivityTracker sharedInstance].activeTrace;
  NSNumber *metric = [activeTrace.counters objectForKey:kFPRAppCounterNameTraceNotStopped];
  NSInteger metricValue = [metric integerValue];
  __weak FIRTrace *weakReferencedTrace;
  @autoreleasepool {
    FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
    weakReferencedTrace = trace;
    [trace start];
    [trace startStageNamed:@"1"];
    [trace stop];
  }
  NSNumber *updatedMetric = [activeTrace.counters objectForKey:kFPRAppCounterNameTraceNotStopped];
  NSInteger updatedMetricValue = [updatedMetric integerValue];
  XCTAssertEqual(updatedMetricValue - metricValue, 0);
}

#pragma mark - Custom attribute related testing

/** Validates if setting a valid attribute before calling start works. */
- (void)testSettingValidAttributeBeforeStart {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  XCTAssertEqual([trace valueForAttribute:@"foo"], @"bar");
}

/** Validates if setting a valid attribute works between start/stop works. */
- (void)testSettingValidAttributeBetweenStartAndStop {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace setValue:@"bar" forAttribute:@"foo"];
  XCTAssertEqual([trace valueForAttribute:@"foo"], @"bar");
  [trace stop];
}

/** Validates if setting a valid attribute works after stop is a no-op. */
- (void)testSettingValidAttributeBetweenAfterStop {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace stop];
  [trace setValue:@"bar" forAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
}

/** Validates if attributes property access works. */
- (void)testReadingAttributesFromProperty {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  XCTAssertNotNil(trace.attributes);
  XCTAssertEqual(trace.attributes.count, 0);
  [trace setValue:@"bar" forAttribute:@"foo"];
  NSDictionary<NSString *, NSString *> *attributes = trace.attributes;
  XCTAssertEqual(attributes.allKeys.count, 1);
}

/** Validates if attributes property is immutable. */
- (void)testImmutablityOfAttributesProperty {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  NSMutableDictionary<NSString *, NSString *> *attributes =
      (NSMutableDictionary<NSString *, NSString *> *)trace.attributes;
  XCTAssertThrows([attributes setValue:@"bar1" forKey:@"foo"]);
}

/** Validates if updating attribute value works. */
- (void)testUpdatingAttributeValue {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  [trace setValue:@"baz" forAttribute:@"foo"];
  XCTAssertEqual([trace valueForAttribute:@"foo"], @"baz");
  [trace setValue:@"qux" forAttribute:@"foo"];
  XCTAssertEqual([trace valueForAttribute:@"foo"], @"qux");
}

/** Validates if removing attributes work before call to start. */
- (void)testRemovingAttributeBeforeStart {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  [trace removeAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
  [trace removeAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
}

/** Validates if removing attributes work between start and stop calls. */
- (void)testRemovingAttributeBetweenStartStop {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  [trace start];
  [trace removeAttribute:@"foo"];
  [trace stop];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
}

/** Validates if removing attributes is a no-op after stop. */
- (void)testRemovingAttributeBetweenAfterStop {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  [trace start];
  [trace stop];
  [trace removeAttribute:@"foo"];
  XCTAssertEqual([trace valueForAttribute:@"foo"], @"bar");
}

/** Validates if removing non-existing attributes works. */
- (void)testRemovingNonExistingAttribute {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace removeAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
  [trace removeAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
}

/** Validates if using reserved prefix in attribute prefix will drop the attribute. */
- (void)testAttributeNamePrefixSrtipped {
  NSArray<NSString *> *reservedPrefix = @[ @"firebase_", @"google_", @"ga_" ];

  [reservedPrefix enumerateObjectsUsingBlock:^(NSString *prefix, NSUInteger idx, BOOL *stop) {
    NSString *attributeName = [NSString stringWithFormat:@"%@name", prefix];
    NSString *attributeValue = @"value";

    FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
    [trace setValue:attributeValue forAttribute:attributeName];
    XCTAssertNil([trace valueForAttribute:attributeName]);
  }];
}

/** Validates if long attribute names gets dropped. */
- (void)testMaxLengthForAttributeName {
  NSString *testName = [@"abc" stringByPaddingToLength:kFPRMaxAttributeNameLength + 1
                                            withString:@"-"
                                       startingAtIndex:0];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:testName];
  XCTAssertNil([trace valueForAttribute:testName]);
}

/** Validates if attribute names with illegal characters gets dropped. */
- (void)testIllegalCharactersInAttributeName {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"bar" forAttribute:@"foo_"];
  XCTAssertEqual([trace valueForAttribute:@"foo_"], @"bar");
  [trace setValue:@"bar" forAttribute:@"foo_$"];
  XCTAssertNil([trace valueForAttribute:@"foo_$"]);
  [trace setValue:@"bar" forAttribute:@"FOO_$"];
  XCTAssertNil([trace valueForAttribute:@"FOO_$"]);
  [trace setValue:@"bar" forAttribute:@"FOO_"];
  XCTAssertEqual([trace valueForAttribute:@"FOO_"], @"bar");
}

/** Validates if long attribute values gets truncated. */
- (void)testMaxLengthForAttributeValue {
  NSString *testValue = [@"abc" stringByPaddingToLength:kFPRMaxAttributeValueLength + 1
                                             withString:@"-"
                                        startingAtIndex:0];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:testValue forAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
}

/** Validates if empty name or value of the attributes are getting dropped. */
- (void)testAttributesWithEmptyValues {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace setValue:@"" forAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
  [trace setValue:@"bar" forAttribute:@""];
  XCTAssertNil([trace valueForAttribute:@""]);
}

/** Validates if the limit the maximum number of attributes work. */
- (void)testMaximumNumberOfAttributes {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  for (int i = 0; i < kFPRMaxGlobalCustomAttributesCount; i++) {
    NSString *attributeName = [NSString stringWithFormat:@"dim%d", i];
    [trace setValue:@"bar" forAttribute:attributeName];
    XCTAssertEqual([trace valueForAttribute:attributeName], @"bar");
  }
  [trace setValue:@"bar" forAttribute:@"foo"];
  XCTAssertNil([trace valueForAttribute:@"foo"]);
}

/** Validates if removing old attributes and adding new attributes work. */
- (void)testRemovingAndAddingAttributes {
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  for (int i = 0; i < kFPRMaxGlobalCustomAttributesCount; i++) {
    NSString *attributeName = [NSString stringWithFormat:@"dim%d", i];
    [trace setValue:@"bar" forAttribute:attributeName];
    XCTAssertEqual([trace valueForAttribute:attributeName], @"bar");
  }
  [trace removeAttribute:@"dim1"];
  [trace setValue:@"bar" forAttribute:@"foo"];
  XCTAssertEqual([trace valueForAttribute:@"foo"], @"bar");
}

/** Validates if every trace contains a session Id. */
- (void)testSessionId {
  [[FPRSessionManager sharedInstance] updateSessionId:@"testSessionId"];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [trace stop];
  XCTAssertNotNil(trace.sessions);
  XCTAssertTrue(trace.sessions.count > 0);
}

/** Validates if every trace contains multiple session Ids on changing app state. */
- (void)testMultipleSessionIds {
  [[FPRSessionManager sharedInstance] updateSessionId:@"testSessionId"];
  FIRTrace *trace = [[FIRTrace alloc] initWithName:@"Random"];
  [trace start];
  [[FPRSessionManager sharedInstance] updateSessionId:@"testSessionId2"];
  [[FPRSessionManager sharedInstance] updateSessionId:@"testSessionId3"];

  XCTestExpectation *expectation = [self expectationWithDescription:@"Expectation - Wait for 2s"];
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
                 dispatch_get_main_queue(), ^{
                   [expectation fulfill];
                   [trace stop];
                   XCTAssertNotNil(trace.sessions);
                   XCTAssertTrue(trace.sessions.count >= 2);
                 });
  [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

@end
