// 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.

@testable import FirebaseCore
import XCTest

private extension Constants {
  static let testAppName1 = "test_app_name_1"
  static let testAppName2 = "test_app_name_2"
}

class FirebaseAppTests: XCTestCase {
  override func setUp() {
    super.setUp()
    FIROptionsMock.mockFIROptions()
  }

  override func tearDown() {
    super.tearDown()
    FirebaseApp.resetApps()
  }

  func testConfigure() throws {
    expectAppConfigurationNotification(appName: Constants.App.defaultName, isDefaultApp: true)

    let configurationAttempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure()
      }
    }
    XCTAssertNoThrow(try configurationAttempt())

    let app = try XCTUnwrap(FirebaseApp.app(), "Failed to unwrap default app")
    XCTAssertEqual(app.name, Constants.App.defaultName)
    XCTAssertEqual(app.options.clientID, Constants.Options.clientID)
    XCTAssertEqual(FirebaseApp.allApps?.count, 1)

    // TODO: check registered libraries instances available

    waitForExpectations(timeout: 1)
  }

  func testIsDefaultAppConfigured() {
    XCTAssertFalse(FirebaseApp.isDefaultAppConfigured())

    expectAppConfigurationNotification(appName: Constants.App.defaultName, isDefaultApp: true)

    let configurationAttempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure()
      }
    }
    XCTAssertNoThrow(try configurationAttempt())
    XCTAssertTrue(FirebaseApp.isDefaultAppConfigured())

    waitForExpectations(timeout: 1)
  }

  func testConfigureDefaultAppTwice() {
    let firstConfigurationAttempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure()
      }
    }
    XCTAssertNoThrow(try firstConfigurationAttempt())

    let secondConfigurationAttempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure()
      }
    }
    XCTAssertThrowsError(try secondConfigurationAttempt())
  }

  func testConfigureWithOptions() throws {
    expectAppConfigurationNotification(appName: Constants.App.defaultName, isDefaultApp: true)

    let options = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                  gcmSenderID: Constants.Options.gcmSenderID)
    options.clientID = Constants.Options.clientID
    let configurationAttempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure(options: options)
      }
    }
    XCTAssertNoThrow(try configurationAttempt())

    let app = try XCTUnwrap(FirebaseApp.app(), "Failed to unwrap default app")
    XCTAssertEqual(app.name, Constants.App.defaultName)
    XCTAssertEqual(app.options.googleAppID, Constants.Options.googleAppID)
    XCTAssertEqual(app.options.gcmSenderID, Constants.Options.gcmSenderID)
    XCTAssertEqual(app.options.clientID, Constants.Options.clientID)
    XCTAssertTrue(FirebaseApp.allApps?.count == 1)

    waitForExpectations(timeout: 1)
  }

  func testConfigureWithNameAndOptions() throws {
    expectAppConfigurationNotification(appName: Constants.testAppName1, isDefaultApp: false)

    let options = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                  gcmSenderID: Constants.Options.gcmSenderID)
    options.clientID = Constants.Options.clientID

    let configurationAttempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure(name: Constants.testAppName1, options: options)
      }
    }
    XCTAssertNoThrow(try configurationAttempt())

    let app = try XCTUnwrap(
      FirebaseApp.app(name: Constants.testAppName1),
      "Failed to unwrap custom named app"
    )
    XCTAssertEqual(app.name, Constants.testAppName1)
    XCTAssertEqual(app.options.googleAppID, Constants.Options.googleAppID)
    XCTAssertEqual(app.options.gcmSenderID, Constants.Options.gcmSenderID)
    XCTAssertEqual(app.options.clientID, Constants.Options.clientID)
    XCTAssertTrue(FirebaseApp.allApps?.count == 1)

    let configureAppAgain = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure(name: Constants.testAppName1, options: options)
      }
    }

    XCTAssertThrowsError(try configureAppAgain())

    waitForExpectations(timeout: 1)
  }

  func testConfigureMultipleApps() throws {
    let options1 = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                   gcmSenderID: Constants.Options.gcmSenderID)

    expectAppConfigurationNotification(appName: Constants.testAppName1, isDefaultApp: false)

    XCTAssertNoThrow(FirebaseApp.configure(name: Constants.testAppName1, options: options1))

    let app1 = try XCTUnwrap(FirebaseApp.app(name: Constants.testAppName1), "Failed to unwrap app1")
    XCTAssertEqual(app1.name, Constants.testAppName1)
    XCTAssertEqual(app1.options.googleAppID, Constants.Options.googleAppID)
    XCTAssertEqual(app1.options.gcmSenderID, Constants.Options.gcmSenderID)
    XCTAssertTrue(FirebaseApp.allApps?.count == 1)

    // Configure a different app with valid customized options.
    let options2 = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                   gcmSenderID: Constants.Options.gcmSenderID)
    options2.bundleID = Constants.Options.bundleID
    options2.apiKey = Constants.Options.apiKey

    expectAppConfigurationNotification(appName: Constants.testAppName2, isDefaultApp: false)

    let configureApp2Attempt = {
      try ExceptionCatcher.catchException {
        FirebaseApp.configure(name: Constants.testAppName2, options: options2)
      }
    }
    XCTAssertNoThrow(try configureApp2Attempt())

    let app2 = try XCTUnwrap(FirebaseApp.app(name: Constants.testAppName2), "Failed to unwrap app2")
    XCTAssertEqual(app2.name, Constants.testAppName2)
    XCTAssertEqual(app2.options.googleAppID, Constants.Options.googleAppID)
    XCTAssertEqual(app2.options.gcmSenderID, Constants.Options.gcmSenderID)
    XCTAssertEqual(app2.options.bundleID, Constants.Options.bundleID)
    XCTAssertEqual(app2.options.apiKey, Constants.Options.apiKey)
    XCTAssertTrue(FirebaseApp.allApps?.count == 2)

    waitForExpectations(timeout: 1)
  }

  func testGetUninitializedDefaultApp() {
    let app = FirebaseApp.app()
    XCTAssertNil(app)
  }

  func testGetInitializedDefaultApp() {
    FirebaseApp.configure()
    let app = FirebaseApp.app()
    XCTAssertNotNil(app)
  }

  func testGetExistingAppWithName() throws {
    // Configure a different app with valid customized options.
    let options = try XCTUnwrap(FirebaseOptions.defaultOptions(), "Could not load default options")
    FirebaseApp.configure(name: Constants.testAppName1, options: options)
    let app = FirebaseApp.app(name: Constants.testAppName1)
    XCTAssertNotNil(app, "Failed to get app")
  }

  func testAttemptToGetNonExistingAppWithName() {
    let unknownAppName = "The Missing App"
    let app = FirebaseApp.app(name: unknownAppName)
    XCTAssertNil(app)
  }

  func testAllApps() throws {
    XCTAssertNil(FirebaseApp.allApps)

    let options1 = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                   gcmSenderID: Constants.Options.gcmSenderID)
    FirebaseApp.configure(name: Constants.testAppName1, options: options1)
    let app1 = try XCTUnwrap(
      FirebaseApp.app(name: Constants.testAppName1),
      "App1 could not be unwrapped"
    )

    let options2 = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                   gcmSenderID: Constants.Options.gcmSenderID)
    FirebaseApp.configure(name: Constants.testAppName2, options: options2)
    let app2 = try XCTUnwrap(
      FirebaseApp.app(name: Constants.testAppName2),
      "App2 could not be unwrapped"
    )

    let apps = try XCTUnwrap(FirebaseApp.allApps, "Could not retrieve apps")

    XCTAssertEqual(apps.count, 2)
    XCTAssertTrue(apps.keys.contains(Constants.testAppName1))
    XCTAssertEqual(apps[Constants.testAppName1], app1)
    XCTAssertTrue(apps.keys.contains(Constants.testAppName2))
    XCTAssertEqual(apps[Constants.testAppName2], app2)
  }

  func testDeleteApp() throws {
    XCTAssertNil(FirebaseApp.app(name: Constants.testAppName1))
    XCTAssertNil(FirebaseApp.allApps)

    expectAppConfigurationNotification(appName: Constants.testAppName1, isDefaultApp: false)

    let options = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                  gcmSenderID: Constants.Options.gcmSenderID)
    FirebaseApp.configure(name: Constants.testAppName1, options: options)

    let app = try XCTUnwrap(FirebaseApp.app(name: Constants.testAppName1), "Could not unwrap app")
    let apps = try XCTUnwrap(FirebaseApp.allApps, "Could not retrieve app dictionary")
    XCTAssertTrue(apps.keys.contains(app.name))
    let appDeletedExpectation = expectation(description: #function)
    app.delete { success in
      XCTAssertTrue(success)
      XCTAssertFalse(FirebaseApp.allApps?.keys.contains(Constants.testAppName1) ?? false)
      appDeletedExpectation.fulfill()
    }

    waitForExpectations(timeout: 1)
  }

  func testGetNameOfDefaultApp() throws {
    FirebaseApp.configure()

    let defaultApp = try XCTUnwrap(FirebaseApp.app(), "Could not unwrap default app")
    XCTAssertEqual(defaultApp.name, Constants.App.defaultName)
  }

  func testGetNameOfApp() throws {
    XCTAssertNil(FirebaseApp.app(name: Constants.testAppName1))

    let options = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                  gcmSenderID: Constants.Options.gcmSenderID)
    FirebaseApp.configure(name: Constants.testAppName1, options: options)

    let app = try XCTUnwrap(
      FirebaseApp.app(name: Constants.testAppName1),
      "Failed to unwrap custom named app"
    )
    XCTAssertEqual(app.name, Constants.testAppName1)
  }

  func testOptionsForApp() throws {
    FirebaseApp.configure()
    let defaultApp = try XCTUnwrap(FirebaseApp.app(), "Could not unwrap default app")
    let defaultOptions = FirebaseOptions.defaultOptions()
    XCTAssertEqual(defaultApp.options, defaultOptions)

    let options = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                  gcmSenderID: Constants.Options.gcmSenderID)
    FirebaseApp.configure(name: Constants.testAppName1, options: options)

    let app = try XCTUnwrap(
      FirebaseApp.app(name: Constants.testAppName1),
      "Could not unwrap custom named app"
    )
    XCTAssertEqual(app.name, Constants.testAppName1)
    XCTAssertEqual(app.options.googleAppID, Constants.Options.googleAppID)
    XCTAssertEqual(app.options.gcmSenderID, Constants.Options.gcmSenderID)
  }

  func testFirebaseDataCollectionDefaultEnabled() throws {
    let app = FirebaseApp(instanceWithName: "emptyApp",
                          options: FirebaseOptions(googleAppID: Constants.Options.googleAppID,
                                                   gcmSenderID: Constants.Options.gcmSenderID))

    // defaults to true unless otherwise set to no in app's Info.plist
    XCTAssertTrue(app.isDataCollectionDefaultEnabled)

    app.isDataCollectionDefaultEnabled = false
    XCTAssertFalse(app.isDataCollectionDefaultEnabled)

    // Cleanup
    app.isDataCollectionDefaultEnabled = true

    let expectation = expectation(description: #function)
    app.delete { success in
      expectation.fulfill()
    }

    waitForExpectations(timeout: 1)
  }

  // MARK: - Firebase User Agent

  func testUserAgent() {
    let agent = FirebaseApp.firebaseUserAgent()
    XCTAssertTrue(agent.contains("apple-platform"))
    XCTAssertTrue(agent.contains("apple-sdk"))
    XCTAssertTrue(agent.contains("appstore"))
    XCTAssertTrue(agent.contains("deploy"))
    XCTAssertTrue(agent.contains("device"))
    XCTAssertTrue(agent.contains("os-version"))
    XCTAssertTrue(agent.contains("xcode"))
  }

  // MARK: - Helpers

  private func expectAppConfigurationNotification(appName: String, isDefaultApp: Bool) {
    let expectedUserInfo: [String: Any] = [
      "FIRAppNameKey": appName,
      "FIRAppIsDefaultAppKey": NSNumber(value: isDefaultApp),
      "FIRGoogleAppIDKey": Constants.Options.googleAppID,
    ]

    expectation(forNotification: NSNotification.Name.firAppReadyToConfigureSDK,
                object: FirebaseApp.self, handler: { notification -> Bool in
                  guard let userInfo = notification.userInfo else {
                    XCTFail("Failed to unwrap notification user info")
                    return false
                  }
                  return NSDictionary(dictionary: expectedUserInfo) ==
                    NSDictionary(dictionary: userInfo)
                })
  }
}
