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

import XCTest
@testable import KeymanEngine

class ResourceDownloadManagerTests: XCTestCase {
  var mockedURLSession: TestUtils.Downloading.URLSessionMock?
  var downloadManager: ResourceDownloadManager?

  override func setUp() {
    mockedURLSession = TestUtils.Downloading.URLSessionMock()
    downloadManager = ResourceDownloadManager(session: mockedURLSession!, autoExecute: false)
  }

  override func tearDownWithError() throws {
    // Resets resource directories for a clean slate.
    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 testDefaultKeyboardDownloadURL() {
    let tier = Version.currentTagged.tier ?? .stable
    let root = "/go/package/download"

    let basic_khmer_angkor_components = URLComponents(string: downloadManager!.defaultDownloadURL(forPackage: TestUtils.Packages.Keys.khmer_angkor).absoluteString)!

    XCTAssertEqual(basic_khmer_angkor_components.path, "\(root)/khmer_angkor")
    XCTAssertTrue(basic_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "platform" && item.value == "ios"
    })
    XCTAssertTrue(basic_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "tier" && item.value == tier.rawValue
    })
    XCTAssertTrue(basic_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "type" && item.value == "keyboard"
    })

    let adv_khmer_angkor_url = downloadManager!.defaultDownloadURL(forPackage: TestUtils.Packages.Keys.khmer_angkor, andResource: TestUtils.Keyboards.khmer_angkor.fullID, withVersion: Version("1.0.6"), asUpdate: true)
    let adv_khmer_angkor_components = URLComponents(string: adv_khmer_angkor_url.absoluteString)!
    XCTAssertEqual(adv_khmer_angkor_components.path, "\(root)/khmer_angkor")
    XCTAssertTrue(adv_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "platform" && item.value == "ios"
    })
    XCTAssertTrue(adv_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "tier" && item.value == tier.rawValue
    })
    XCTAssertTrue(adv_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "type" && item.value == "keyboard"
    })
    XCTAssertTrue(adv_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "update" && item.value == "1"
    })
    XCTAssertTrue(adv_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "version" && item.value == "1.0.6"
    })
    XCTAssertTrue(adv_khmer_angkor_components.queryItems!.contains { item in
      return item.name == "bcp47" && item.value == "km"
    })
  }

  func testDefaultLexicalModelDownloadURL() {
    let tier = Version.currentTagged.tier ?? .stable
    let root = "/go/package/download"

    let basic_mtnt_components = URLComponents(string: downloadManager!.defaultDownloadURL(forPackage: TestUtils.Packages.Keys.nrc_en_mtnt).absoluteString)!

    XCTAssertEqual(basic_mtnt_components.path, "\(root)/nrc.en.mtnt")
    XCTAssertTrue(basic_mtnt_components.queryItems!.contains { item in
      return item.name == "platform" && item.value == "ios"
    })
    XCTAssertTrue(basic_mtnt_components.queryItems!.contains { item in
      return item.name == "tier" && item.value == tier.rawValue
    })
    XCTAssertTrue(basic_mtnt_components.queryItems!.contains { item in
      return item.name == "type" && item.value == "model"
    })

    let adv_mtnt_url = downloadManager!.defaultDownloadURL(forPackage: TestUtils.Packages.Keys.nrc_en_mtnt, andResource: TestUtils.LexicalModels.mtnt.fullID, withVersion: Version(TestUtils.LexicalModels.mtnt.version), asUpdate: true)
    let adv_mtnt_components = URLComponents(string: adv_mtnt_url.absoluteString)!
    XCTAssertEqual(adv_mtnt_components.path, "\(root)/nrc.en.mtnt")
    XCTAssertTrue(adv_mtnt_components.queryItems!.contains { item in
      return item.name == "platform" && item.value == "ios"
    })
    XCTAssertTrue(adv_mtnt_components.queryItems!.contains { item in
      return item.name == "tier" && item.value == tier.rawValue
    })
    XCTAssertTrue(adv_mtnt_components.queryItems!.contains { item in
      return item.name == "type" && item.value == "model"
    })
    XCTAssertTrue(adv_mtnt_components.queryItems!.contains { item in
      return item.name == "update" && item.value == "1"
    })
    XCTAssertTrue(adv_mtnt_components.queryItems!.contains { item in
      return item.name == "version" && item.value == TestUtils.LexicalModels.mtnt.version
    })
    XCTAssertTrue(adv_mtnt_components.queryItems!.contains { item in
      return item.name == "bcp47" && item.value == "en"
    })
  }

  func testDownloadPackageForKeyboard() throws {
    let expectation = XCTestExpectation(description: "Mocked \"download\" should complete successfully.")
    let packageKey = TestUtils.Keyboards.khmer_angkor.packageKey

    let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
    mockedURLSession?.queueMockResult(.download(mockedResult))

    downloadManager?.downloadPackage(withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, withNotifications: false) { (package: KeyboardKeymanPackage?, error: Error?) in

      let tempDownloadKMP = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey)
      XCTAssertFalse(FileManager.default.fileExists(atPath: tempDownloadKMP.path))
      
      // Assertions:  a copy of the KMP file exists in the documents directory, facilitating user sharing.
      let cachedDocumentsKMP = ResourceFileManager.shared.cachedPackagePath(forKey: packageKey)

      XCTAssertTrue(FileManager.default.fileExists(atPath: cachedDocumentsKMP.path))

      XCTAssertNil(error)
      XCTAssertNotNil(package)

      // Basic verification that it really is the khmer_angkor keyboard package.
      if let package = package {
        XCTAssertEqual(package.id, TestUtils.Keyboards.khmer_angkor.id)
        XCTAssertNotNil(package.findResource(withID: TestUtils.Keyboards.khmer_angkor.fullID))
      }

      expectation.fulfill()
    }

    let downloadQueue = downloadManager!.downloader
    downloadQueue.step()

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

  func testDownloadPackageForLexicalModel() throws {
    let expectation = XCTestExpectation(description: "Mocked \"download\" should complete successfully.")
    let packageKey = TestUtils.LexicalModels.mtnt.packageKey

    let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.LexicalModels.mtntKMP, error: nil)
    mockedURLSession?.queueMockResult(.download(mockedResult))

    downloadManager?.downloadPackage(withKey: packageKey, from: TestUtils.LexicalModels.mtntKMP, withNotifications: false) { (package: LexicalModelKeymanPackage?, error: Error?) in
      let tempDownloadKMP = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey)
      XCTAssertFalse(FileManager.default.fileExists(atPath: tempDownloadKMP.path))

      // Assertion:  a copy of the KMP file exists in the documents directory, facilitating user sharing.
      let cachedDocumentsKMP = ResourceFileManager.shared.cachedPackagePath(forKey: packageKey)

      XCTAssertTrue(FileManager.default.fileExists(atPath: cachedDocumentsKMP.path))

      XCTAssertNil(error)
      XCTAssertNotNil(package)

      // Basic verification that it really is the khmer_angkor keyboard package.
      if let package = package {
        XCTAssertEqual(package.id, TestUtils.LexicalModels.mtnt.id)
        XCTAssertNotNil(package.findResource(withID: TestUtils.LexicalModels.mtnt.fullID))
      }

      expectation.fulfill()
    }

    let downloadQueue = downloadManager!.downloader
    downloadQueue.step()

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

  func testDownloadPackageFailure() throws {
    let expectation = XCTestExpectation(description: "Mocked \"download\" should complete, though with an error.")
    let packageKey = TestUtils.Keyboards.khmer_angkor.packageKey

    let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: TestUtils.mockedError)
    mockedURLSession?.queueMockResult(.download(mockedResult))

    downloadManager?.downloadPackage(withKey: packageKey, from: TestUtils.Keyboards.khmerAngkorKMP, withNotifications: false) { package, error in

      let tempDownloadKMP = ResourceFileManager.shared.packageDownloadTempPath(forKey: packageKey)
      XCTAssertFalse(FileManager.default.fileExists(atPath: tempDownloadKMP.path))

      // Assertion:  a copy of the KMP file exists in the documents directory, facilitating user sharing.
      let cachedDocumentsKMP = ResourceFileManager.shared.cachedPackagePath(forKey: packageKey)

      // On download failure, we shouldn't be producing a file here.
      XCTAssertFalse(FileManager.default.fileExists(atPath: cachedDocumentsKMP.path))

      XCTAssertNotNil(error)
      XCTAssertNil(package)

      expectation.fulfill()
    }

    let downloadQueue = downloadManager!.downloader
    downloadQueue.step()

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

  func testLegacyDownloadKeyboard() throws {
    let expectation = XCTestExpectation(description: "Mocked package \"download\" should complete successfully.")

    // For this test, we actually want the downloader to run automatically.  It's... a bit tricky to
    // run this integration test otherwise.
    downloadManager = ResourceDownloadManager(session: mockedURLSession!, autoExecute: true)

    let mockedDownload = TestUtils.Downloading.MockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)

    mockedURLSession?.queueMockResult(.download(mockedDownload))

    let khmer_angkor_id = TestUtils.Keyboards.khmer_angkor.fullID

    downloadManager?.downloadKeyboard(withID: khmer_angkor_id.id, languageID: khmer_angkor_id.languageID, isUpdate: false, fetchRepositoryIfNeeded: false) { package, error in
      if let error = error {
        XCTFail("Mocked query and download should both succeed; reported error: \(error.localizedDescription)")
      } else if let package = package {
        // Double-check it!
        XCTAssertEqual(package.id, "khmer_angkor")
        XCTAssertNotNil(package.findResource(withID: khmer_angkor_id))
      }

      expectation.fulfill()
    }

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

  func testLegacyDownloadLexicalModel() throws {
    let expectation = XCTestExpectation(description: "Mocked package \"download\" should complete successfully.")

    // For this test, we actually want the downloader to run automatically.  It's... a bit tricky to
    // run this integration test otherwise.
    downloadManager = ResourceDownloadManager(session: mockedURLSession!, autoExecute: true)
    
    let mockedDownload = TestUtils.Downloading.MockResult(location: TestUtils.LexicalModels.mtntKMP, error: nil)

    mockedURLSession?.queueMockResult(.download(mockedDownload))

    let mtnt_id = TestUtils.LexicalModels.mtnt.fullID

    downloadManager?.downloadLexicalModel(withID: mtnt_id.id, languageID: mtnt_id.languageID, isUpdate: false, fetchRepositoryIfNeeded: false) { package, error in
      if let error = error {
        XCTFail("Mocked query and download should both succeed; reported error: \(error.localizedDescription)")
      } else if let package = package {
        // Double-check it!
        XCTAssertEqual(package.id, "nrc.en.mtnt")
        XCTAssertNotNil(package.findResource(withID: mtnt_id))
      }

      expectation.fulfill()
    }

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

  // This annotation prevents local deprecation warnings for what it tests.
  @available(*, deprecated, message: "This method tests the deprecated ResourceDownloadManager.stateForKeyboard.")
  func testStateForKeyboard() {
    let baseInstallation = XCTestExpectation(description: "Mocked \"download\" should complete successfully.")
    let khmer_angkor_id = TestUtils.Keyboards.khmer_angkor.fullID
    let packageKey = TestUtils.Keyboards.khmer_angkor.packageKey

    let mockedResult = TestUtils.Downloading.MockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
    mockedURLSession?.queueMockResult(.download(mockedResult))

    XCTAssertEqual(downloadManager!.stateForKeyboard(withID: khmer_angkor_id.id), .needsDownload)

    downloadManager!.downloadPackage(withKey: packageKey,
                                     from: TestUtils.Keyboards.khmerAngkorKMP,
                                     withNotifications: false) { (package: KeyboardKeymanPackage?, error) in
      if let _ = error {
        XCTFail()
        baseInstallation.fulfill()
        return
      } else if let package = package {
        do {
          try ResourceFileManager.shared.install(resourceWithID: khmer_angkor_id, from: package)
          baseInstallation.fulfill()
        } catch {
          XCTFail()
          baseInstallation.fulfill()
        }
      }
    }

    XCTAssertEqual(downloadManager!.stateForKeyboard(withID: khmer_angkor_id.id), .downloading)

    downloadManager!.downloader.step()

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

    XCTAssertEqual(downloadManager!.stateForKeyboard(withID: khmer_angkor_id.id), .upToDate)

    // Now, to test the update-check part of the function...
    // This fixture was hand-altered.  At the time of this test's creation, this version did not exist.
    let mockedQuery = TestUtils.Downloading.MockResult(location: TestUtils.Queries.package_version_km_updated, error: nil)
    mockedURLSession?.queueMockResult(.data(mockedQuery))

    let versionQuery = XCTestExpectation()

    // Now to do a package-version check.
    KeymanPackage.queryCurrentVersions(for: [KeymanPackage.Key(id: khmer_angkor_id.id, type: .keyboard)], withSession: downloadManager!.session) { result, _ in
      XCTAssertEqual(self.downloadManager!.stateForKeyboard(withID: khmer_angkor_id.id), .needsUpdate)
      versionQuery.fulfill()
    }

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

  public func testDefaultLexicalModelForLanguage() {
    let mockedQueryWithModel = TestUtils.Downloading.MockResult(location: TestUtils.Queries.model_case_en, error: nil)
    let mockedModelDownload = TestUtils.Downloading.MockResult(location: TestUtils.LexicalModels.mtntKMP, error: nil)
    let mockedQueryWithoutModel = TestUtils.Downloading.MockResult(location: TestUtils.Queries.model_case_km, error: nil)
    mockedURLSession?.queueMockResult(.data(mockedQueryWithModel))
    mockedURLSession?.queueMockResult(.download(mockedModelDownload))
    mockedURLSession?.queueMockResult(.data(mockedQueryWithoutModel))

    let foundExpectation = XCTestExpectation()
    let noneExpectation = XCTestExpectation()

    downloadManager?.downloader.autoExecute = true
    downloadManager?.downloadLexicalModelsForLanguageIfExists(languageID: "en") { package, error in
      if error != nil {
        XCTFail()
      }

      XCTAssertNotNil(package)

      foundExpectation.fulfill()
    }

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

    downloadManager?.downloadLexicalModelsForLanguageIfExists(languageID: "km") { package, error in
      if error != nil {
        XCTFail()
      }

      XCTAssertNil(package)

      noneExpectation.fulfill()
    }

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