//
//  KeymanPackageTests.swift
//  KeymanEngineTests
//
//  Created by Joshua Horton on 6/3/20.
//  Copyright © 2020 SIL International. All rights reserved.
//

import XCTest
@testable import KeymanEngine

class KeymanPackageTests: XCTestCase {
  var mockedURLSession: TestUtils.Downloading.URLSessionMock!

  override func setUp() {
    mockedURLSession = TestUtils.Downloading.URLSessionMock()
  }

  override func tearDownWithError() throws {
    TestUtils.standardTearDown()

    let queueWasCleared = mockedURLSession!.queueIsEmpty
    mockedURLSession = nil

    if !queueWasCleared {
      throw NSError(domain: "Keyman",
                    code: 4,
                    userInfo: [NSLocalizedDescriptionKey: "A test did not fully utilize its queued mock results!"])
    }
  }

  func testKeyboardPackageExtraction() throws {
    let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
    let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp.zip")
    try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip)

    let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor")

    // Requires that the source file is already .zip, not .kmp.  It's a ZipUtils limitation.
    do {
      if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) {
        // Run assertions on the package's kmp.info.
        // Assumes the KMP used for testing here has the same kmp.info used for those tests.
        let kmp_json_testcase = KMPJSONTests()
        kmp_json_testcase.kmp_info_khmer_angkor_assertions(kmp.metadata)

        XCTAssertNotNil(kmp as? KeyboardKeymanPackage, "Keyboard KMP test extraction did not yield a keyboard package!")
        XCTAssertTrue(kmp.isKeyboard(), "Keyboard KMP test extraction did not yield a keyboard package!")
        XCTAssertEqual(kmp.id, "khmer_angkor", "Incorrect package ID")
        // extracted ok, test kmp
        XCTAssert(kmp.sourceFolder == destinationFolderURL,
                  "The KMP's reported 'source folder' should match the specified destination folder")
      } else {
        XCTAssert(false, "KeymanPackage.extract failed")
      }
    } catch {
      XCTFail("KeymanPackage.extract failed with error \(error)")
    }
  }

  func testLexicalModelPackageExtraction() throws {
    let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
    let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp.zip")
    try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.LexicalModels.mtntKMP, to: mtntZip)

    let destinationFolderURL = cacheDirectory.appendingPathComponent("nrc.en.mtnt.model")

    // Requires that the source file is already .zip, not .kmp.  It's a ZipUtils limitation.
    do {
      if let kmp = try KeymanPackage.extract(fileUrl: mtntZip, destination: destinationFolderURL) {
        // Run assertions on the package's kmp.info.
        // Assumes the KMP used for testing here has the same kmp.info used for those tests.
        let kmp_json_testcase = KMPJSONTests()

        // As this test takes place after construction of the LexicalModelPackage,
        // the version will be set accordingly, unlike in the other JSON-related tests.
        kmp_json_testcase.kmp_info_nrc_en_mtnt_assertions(kmp.metadata, version: "0.1.4")

        XCTAssertNotNil(kmp as? LexicalModelKeymanPackage, "Lexical model KMP test extraction yielded a keyboard package!")
        XCTAssertTrue(!kmp.isKeyboard(), "Lexical model KMP test extraction yielded a keyboard package!")
        XCTAssertEqual(kmp.id, "nrc.en.mtnt")

        // extracted ok, test kmp
        XCTAssert(kmp.sourceFolder == destinationFolderURL,
                  "The KMP's reported 'source folder' should match the specified destination folder")
      } else {
        XCTAssert(false, "KeymanPackage.extract failed")
      }
    } catch {
      XCTFail("KeymanPackage.extract failed with error \(error)")
    }
  }

  func testPackageFindResourceMatch() throws {
    guard let kmp1 = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) as? KeyboardKeymanPackage else {
      XCTFail("Incorrect package type loaded for test")
      return
    }
    let kbd = kmp1.findResource(withID: TestUtils.Keyboards.khmer_angkor.fullID)
    XCTAssertNotNil(kbd)
    XCTAssertEqual(kbd?.packageID, "khmer_angkor", "Keyboard package ID not properly set")
    // This keyboard's not in the specified testing package.
    XCTAssertNil(kmp1.findResource(withID: TestUtils.Keyboards.khmer10.fullID))

    // Thanks to our package typing hierarchy, it's impossible to even TRY finding
    // a FullLexicalModelID within a KeyboardKeymanPackage!

    guard let kmp2 = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) as? LexicalModelKeymanPackage else {
      XCTFail("Incorrect package type loaded for test")
      return
    }
    let lm = kmp2.findResource(withID: TestUtils.LexicalModels.mtnt.fullID)
    XCTAssertNotNil(lm)
    XCTAssertEqual(lm?.packageID, "nrc.en.mtnt", "Lexical model ID not properly set")

    // Thanks to our package typing hierarchy, it's impossible to even TRY finding
    // a FullKeyboardID within a LexicalModelKeymanPackage!
  }

  func testInstalledPackageDeinit() throws {
    let kmp = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP)
    XCTAssertNotNil(kmp, "Failed to prepare KMP for installation")
    XCTAssertNotNil(kmp as? KeyboardKeymanPackage, "KMP resource type improperly recognized - expected a keyboard package!")

    try ResourceFileManager.shared.finalizePackageInstall(kmp, isCustom: true)

    let installedDir = Storage.active.resourceDir(for: TestUtils.Keyboards.khmer_angkor)!
    let installedURL = Storage.active.resourceURL(for: TestUtils.Keyboards.khmer_angkor)!

    // Intentionally scopes the `package` variable
    do {
      let package = try KeymanPackage.parse(installedDir)!
      XCTAssertTrue(FileManager.default.fileExists(atPath: installedURL.path))
      XCTAssertEqual(package.id, "khmer_angkor")
    }

    // Deinit should have triggered - is everything still in place?
    XCTAssertTrue(FileManager.default.fileExists(atPath: installedURL.path))
    XCTAssertTrue(FileManager.default.fileExists(atPath: installedDir.path))
  }

  func testTempPackageDeinit() throws {
    var tempDir: URL

    // Intentionally scopes the `package` variable.
    do {
      let package = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP)
      XCTAssertNotNil(package, "Failed to prepare KMP for installation")
      XCTAssertNotNil(package as? KeyboardKeymanPackage, "KMP resource type improperly recognized - expected a keyboard package!")

      tempDir = package.sourceFolder

      XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.path))
    }

    // Deinit should have triggered - were the files automatically cleaned up?
    XCTAssertFalse(FileManager.default.fileExists(atPath: tempDir.path))
  }

  // Analogous to QueryPackageVersionTests.testMockedBatchFetchParse, but with more analysis applied
  // and more integration.
  func testQueryDistributionStates() throws {
    let expectation = XCTestExpectation(description: "The query completes as expected.")

    // Test setup

    let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_case_1, error: nil)
    mockedURLSession!.queueMockResult(.data(mockedResult))

    let badKbdKey = KeymanPackage.Key(id: "foo", type: .keyboard)
    let badLexKey = KeymanPackage.Key(id: "bar", type: .lexicalModel)
    let packageKeys = [KeymanPackage.Key(forResource: TestUtils.Keyboards.khmer_angkor),
                       KeymanPackage.Key(forResource: TestUtils.Keyboards.sil_euro_latin),
                       KeymanPackage.Key(id: "foo", type: .keyboard),
                       KeymanPackage.Key(forResource: TestUtils.LexicalModels.mtnt),
                       KeymanPackage.Key(id: "bar", type: .lexicalModel)]

    KeymanPackage.queryDistributionStates(for: packageKeys, withSession: mockedURLSession!) { results, error in
      guard error == nil, let results = results else {
        XCTFail()
        expectation.fulfill()
        return
      }

      let khmer_angkor = KeymanPackage.Key(forResource: TestUtils.Keyboards.khmer_angkor)
      let sil_euro_latin = KeymanPackage.Key(forResource: TestUtils.Keyboards.sil_euro_latin)
      let mtnt = KeymanPackage.Key(forResource: TestUtils.LexicalModels.mtnt)
      XCTAssertEqual(results[khmer_angkor]?.latestVersion, "1.0.6")
      XCTAssertEqual(results[khmer_angkor]?.distributionMethod, .cloud)

      XCTAssertEqual(results[sil_euro_latin]?.latestVersion, "1.9.1")
      XCTAssertEqual(results[sil_euro_latin]?.distributionMethod, .cloud)

      XCTAssertEqual(results[mtnt]?.latestVersion, "0.1.4")
      XCTAssertEqual(results[mtnt]?.distributionMethod, .cloud)

      XCTAssertNotNil(results[badKbdKey])
      XCTAssertEqual(results[badKbdKey]?.distributionMethod, .custom)

      XCTAssertNotNil(results[badLexKey])
      XCTAssertEqual(results[badLexKey]?.distributionMethod, .custom)

      // Test that this metadata is stored and persisted (cached) in UserDefaults.
      let userDefaults = Storage.active.userDefaults

      let cache_khmer_angkor = userDefaults.cachedPackageQueryResult(forPackageKey: khmer_angkor)
      XCTAssertNotNil(cache_khmer_angkor)
      XCTAssertEqual(cache_khmer_angkor?.distributionMethod, results[khmer_angkor]?.distributionMethod)
      XCTAssertEqual(cache_khmer_angkor?.latestVersion, "1.0.6")
      XCTAssertEqual(cache_khmer_angkor?.timestampForLastQuery, results[khmer_angkor]?.timestampForLastQuery)

      let cache_mtnt = userDefaults.cachedPackageQueryResult(forPackageKey: mtnt)
      XCTAssertNotNil(cache_mtnt)
      XCTAssertEqual(cache_mtnt?.distributionMethod, results[mtnt]?.distributionMethod)
      XCTAssertEqual(cache_mtnt?.latestVersion, "0.1.4")
      XCTAssertEqual(cache_mtnt?.timestampForLastQuery, results[mtnt]?.timestampForLastQuery)

      let cache_foo = userDefaults.cachedPackageQueryResult(forPackageKey: badKbdKey)
      XCTAssertNotNil(cache_foo)
      XCTAssertEqual(cache_foo?.distributionMethod, results[badKbdKey]?.distributionMethod)
      XCTAssertNil(cache_foo?.latestVersion)

      expectation.fulfill()
    }

    wait(for: [expectation], timeout: 5)
  }

  // Analogous to QueryPackageVersionTests.testMockedBatchFetchParse, but with more analysis applied
  // and more integration.
  func testQueryCurrentVersions() throws {
    let mockedURLSession = TestUtils.Downloading.URLSessionMock()

    let expectation = XCTestExpectation(description: "The query completes as expected.")

    // Test setup

    let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_case_1, error: nil)
    mockedURLSession.queueMockResult(.data(mockedResult))

    let badKbdKey = KeymanPackage.Key(id: "foo", type: .keyboard)
    let badLexKey = KeymanPackage.Key(id: "bar", type: .lexicalModel)
    let packageKeys = [KeymanPackage.Key(forResource: TestUtils.Keyboards.khmer_angkor),
                       KeymanPackage.Key(forResource: TestUtils.Keyboards.sil_euro_latin),
                       KeymanPackage.Key(id: "foo", type: .keyboard),
                       KeymanPackage.Key(forResource: TestUtils.LexicalModels.mtnt),
                       KeymanPackage.Key(id: "bar", type: .lexicalModel)]

    KeymanPackage.queryCurrentVersions(for: packageKeys, withSession: mockedURLSession) { results, error in
      guard error == nil, let results = results else {
        XCTFail()
        expectation.fulfill()
        return
      }

      let khmer_angkor = KeymanPackage.Key(forResource: TestUtils.Keyboards.khmer_angkor)
      let sil_euro_latin = KeymanPackage.Key(forResource: TestUtils.Keyboards.sil_euro_latin)
      let mtnt = KeymanPackage.Key(forResource: TestUtils.LexicalModels.mtnt)
      XCTAssertEqual(results[khmer_angkor], Version("1.0.6"))
      XCTAssertEqual(results[sil_euro_latin], Version("1.9.1"))
      XCTAssertEqual(results[mtnt], Version("0.1.4"))
      XCTAssertNil(results[badKbdKey])
      XCTAssertNil(results[badLexKey])

      expectation.fulfill()
    }

    wait(for: [expectation], timeout: 5)

    // Post-execution cleanup
    let queueWasCleared = mockedURLSession.queueIsEmpty

    if !queueWasCleared {
      throw NSError(domain: "Keyman",
                    code: 4,
                    userInfo: [NSLocalizedDescriptionKey: "A test did not fully utilize its queued mock results!"])
    }
  }

  func testVersionState() throws {
    // Step 1:  Install khmer_angkor.  Fixture:  version 1.0.6.
    guard let installPackage = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) as? KeyboardKeymanPackage else {
      XCTFail("Could not load keyboard KMP for test")
      return
    }

    try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.khmer_angkor.fullID, from: installPackage)

    // Step 2:  retrieve the installed version of the package.  We're currently using the temp version.
    guard let package = ResourceFileManager.shared.getInstalledPackage(withKey: installPackage.key) else {
      XCTFail("Could not load installed form of keyboard KMP for test")
      return
    }

    // No query has yet occurred - version state unknown.
    XCTAssertEqual(package.versionState, .unknown)

    // Step 3:  Prepare mocking for two separate query rounds.
    let mockedVersionIsCurrent = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_km, error: nil)  // For Step 4.
    let mockedVersionIsOutdated = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_km_updated, error: nil) // For Step 5.
    mockedURLSession.queueMockResult(.data(mockedVersionIsCurrent))
    mockedURLSession.queueMockResult(.data(mockedVersionIsOutdated))

    // Step 4:  Mocked package-version query returns the same version as we installed.
    let expectationCurrent = XCTestExpectation(description: "Query matching 'current version' completed successfully.")

    KeymanPackage.queryCurrentVersions(for: [package.key], withSession: mockedURLSession) { _, _ in
      expectationCurrent.fulfill()
    }

    wait(for: [expectationCurrent], timeout: 5)
    XCTAssertEqual(package.versionState, .upToDate)

    // Step 5:  Mocked package-version query returns an updated version compared to what is installed.
    let expectationUpdate = XCTestExpectation(description: "Query matching 'update available' completed successfully.")

    KeymanPackage.queryCurrentVersions(for: [package.key], withSession: mockedURLSession) { _, _ in
      expectationUpdate.fulfill()
    }

    wait(for: [expectationUpdate], timeout: 5)
    XCTAssertEqual(package.versionState, .needsUpdate)
  }

  func testInstallState() throws {
    // Step 1:  Install khmer_angkor.  Fixture:  version 1.0.6.
    guard let installPackage = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.khmerAngkorKMP) as? KeyboardKeymanPackage else {
      XCTFail("Could not load keyboard KMP for test")
      return
    }

    XCTAssertEqual(installPackage.installState, .pending)

    try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.khmer_angkor.fullID, from: installPackage)

    // Step 2:  retrieve an instance for the installation.  We're currently using the temp version.
    guard let package = ResourceFileManager.shared.getInstalledPackage(withKey: installPackage.key) else {
      XCTFail("Could not load installed form of keyboard KMP for test")
      return
    }

    XCTAssertEqual(installPackage.installState, .pending) // Reflects that the instance is a temporary extraction.
    XCTAssertEqual(package.installState, .installed) // Reflects the actually-installed package
  }
}
