// Copyright 2022 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 Foundation
@testable import FirebaseStorage
import GTMSessionFetcherCore
import XCTest

class StorageListTests: StorageTestHelpers {
  var fetcherService: GTMSessionFetcherService?
  var dispatchQueue: DispatchQueue?

  override func setUp() {
    super.setUp()
    fetcherService = GTMSessionFetcherService()
    fetcherService?.authorizer = StorageTokenAuthorizer(
      googleAppID: "dummyAppID",
      fetcherService: fetcherService!,
      authProvider: nil,
      appCheck: nil
    )
    dispatchQueue = DispatchQueue(label: "Test dispatch queue")
  }

  override func tearDown() {
    fetcherService = nil
    super.tearDown()
  }

  func testValidatesInput() {
    let expectation = self.expectation(description: #function)
    expectation.expectedFulfillmentCount = 4

    let errorBlock = { (result: StorageListResult?, error: Error?) in
      XCTAssertNil(result)
      XCTAssertNotNil(error)
      let nsError = error! as NSError
      XCTAssertEqual(nsError.domain, "FIRStorageErrorDomain")
      XCTAssertEqual(nsError.code, StorageErrorCode.invalidArgument.rawValue)
      expectation.fulfill()
    }
    let ref = storage().reference(withPath: "object")
    ref.list(maxResults: 0, completion: errorBlock)
    ref.list(maxResults: 1001, completion: errorBlock)
    ref.list(maxResults: 0, pageToken: "foo", completion: errorBlock)
    ref.list(maxResults: 1001, pageToken: "foo", completion: errorBlock)

    waitForExpectation(test: self)
  }

  func testListAllCallbackOnlyCalledOnce() {
    let expectation = self.expectation(description: #function)
    expectation.expectedFulfillmentCount = 1

    let errorBlock = { (result: StorageListResult?, error: Error?) in
      XCTAssertNil(result)
      XCTAssertNotNil(error)
      let nsError = error! as NSError
      XCTAssertEqual(nsError.domain, "FIRStorageErrorDomain")
      XCTAssertEqual(nsError.code, StorageErrorCode.unknown.rawValue)
      expectation.fulfill()
    }
    let ref = storage().reference(withPath: "object")
    ref.listAll(completion: errorBlock)

    waitForExpectation(test: self)
  }

  func testDefaultList() {
    let expectation = self.expectation(description: #function)

    fetcherService?.testBlock = { (fetcher: GTMSessionFetcher,
                                   response: GTMSessionFetcherTestResponse) in
        let url = fetcher.request!.url!
        XCTAssertEqual(url.scheme, "https")
        XCTAssertEqual(url.host, "firebasestorage.googleapis.com")
        XCTAssertEqual(url.port, 443)
        XCTAssertEqual(url.path, "/v0/b/bucket/o")
        let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)!.queryItems!
        XCTAssertEqual(queryItems.count, 2)
        for item in queryItems {
          switch item.name {
          case "prefix": XCTAssertEqual(item.value, "object/")
          case "delimiter": XCTAssertEqual(item.value, "/")
          default: XCTFail("Unexpected URLComponent Query Item")
          }
        }
        XCTAssertEqual(fetcher.request?.httpMethod, "GET")
        let httpResponse = HTTPURLResponse(url: (fetcher.request?.url)!,
                                           statusCode: 200,
                                           httpVersion: "HTTP/1.1",
                                           headerFields: nil)
        response(httpResponse, nil, nil)
    }

    let path = objectPath()
    let ref = StorageReference(storage: storage(), path: path)
    let task = StorageListTask(
      reference: ref,
      fetcherService: fetcherService!.self,
      queue: dispatchQueue!.self,
      pageSize: nil,
      previousPageToken: nil
    ) { result, error in
      expectation.fulfill()
    }
    task.enqueue()
    waitForExpectation(test: self)
  }

  func testDefaultListWithEmulator() {
    let expectation = self.expectation(description: #function)
    let storage = self.storage()
    storage.useEmulator(withHost: "localhost", port: 8080)
    fetcherService?.allowLocalhostRequest = true

    fetcherService?.testBlock = { (fetcher: GTMSessionFetcher,
                                   response: GTMSessionFetcherTestResponse) in
        let url = fetcher.request!.url!
        XCTAssertEqual(url.scheme, "http")
        XCTAssertEqual(url.host, "localhost")
        XCTAssertEqual(url.port, 8080)
        XCTAssertEqual(url.path, "/v0/b/bucket/o")
        let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)!.queryItems!
        XCTAssertEqual(queryItems.count, 2)
        for item in queryItems {
          switch item.name {
          case "prefix": XCTAssertEqual(item.value, "object/")
          case "delimiter": XCTAssertEqual(item.value, "/")
          default: XCTFail("Unexpected URLComponent Query Item")
          }
        }
        XCTAssertEqual(fetcher.request?.httpMethod, "GET")
        let httpResponse = HTTPURLResponse(url: (fetcher.request?.url)!,
                                           statusCode: 200,
                                           httpVersion: "HTTP/1.1",
                                           headerFields: nil)
        response(httpResponse, "{}".data(using: .utf8), nil)
    }

    let path = objectPath()
    let ref = StorageReference(storage: storage, path: path)
    let task = StorageListTask(
      reference: ref,
      fetcherService: fetcherService!.self,
      queue: dispatchQueue!.self,
      pageSize: nil,
      previousPageToken: nil
    ) { result, error in
      XCTAssertNil(error)
      expectation.fulfill()
    }

    task.enqueue()

    waitForExpectation(test: self)
  }

  func testListWithPageSizeAndPageToken() {
    let expectation = self.expectation(description: #function)

    fetcherService?.testBlock = { (fetcher: GTMSessionFetcher,
                                   response: GTMSessionFetcherTestResponse) in
        let url = fetcher.request!.url!
        XCTAssertEqual(url.scheme, "https")
        XCTAssertEqual(url.host, "firebasestorage.googleapis.com")
        XCTAssertEqual(url.port, 443)
        XCTAssertEqual(url.path, "/v0/b/bucket/o")
        let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)!.queryItems!
        XCTAssertEqual(queryItems.count, 4)
        for item in queryItems {
          switch item.name {
          case "prefix": XCTAssertEqual(item.value, "object/")
          case "delimiter": XCTAssertEqual(item.value, "/")
          case "pageToken": XCTAssertEqual(item.value, "foo")
          case "maxResults": XCTAssertEqual(item.value, "42")
          default: XCTFail("Unexpected URLComponent Query Item")
          }
        }
        XCTAssertEqual(fetcher.request?.httpMethod, "GET")
        let httpResponse = HTTPURLResponse(url: (fetcher.request?.url)!,
                                           statusCode: 200,
                                           httpVersion: "HTTP/1.1",
                                           headerFields: nil)
        response(httpResponse, nil, nil)
    }

    let path = objectPath()
    let ref = StorageReference(storage: storage(), path: path)
    let task = StorageListTask(
      reference: ref,
      fetcherService: fetcherService!.self,
      queue: dispatchQueue!.self,
      pageSize: 42,
      previousPageToken: "foo"
    ) { result, error in
      expectation.fulfill()
    }
    task.enqueue()
    waitForExpectation(test: self)
  }

  func testPercentEncodesPlusToken() {
    let expectation = self.expectation(description: #function)

    fetcherService?.testBlock = { (fetcher: GTMSessionFetcher,
                                   response: GTMSessionFetcherTestResponse) in
        let url = fetcher.request!.url!
        XCTAssertEqual(url.scheme, "https")
        XCTAssertEqual(url.host, "firebasestorage.googleapis.com")
        XCTAssertEqual(url.port, 443)
        XCTAssertEqual(url.path, "/v0/b/bucket/o")
        let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)!.queryItems!
        XCTAssertEqual(queryItems.count, 2)
        for item in queryItems {
          switch item.name {
          case "prefix": XCTAssertEqual(item.value, "+foo/")
          case "delimiter": XCTAssertEqual(item.value, "/")
          default: XCTFail("Unexpected URLComponent Query Item")
          }
        }
        XCTAssertEqual(fetcher.request?.httpMethod, "GET")
        let httpResponse = HTTPURLResponse(url: (fetcher.request?.url)!,
                                           statusCode: 200,
                                           httpVersion: "HTTP/1.1",
                                           headerFields: nil)
        response(httpResponse, nil, nil)
    }

    let storage = storage()
    let ref = storage.reference(withPath: "+foo")
    let task = StorageListTask(
      reference: ref,
      fetcherService: fetcherService!.self,
      queue: dispatchQueue!.self,
      pageSize: nil,
      previousPageToken: nil
    ) { result, error in
      expectation.fulfill()
    }
    task.enqueue()
    waitForExpectation(test: self)
  }

  func testListWithResponse() throws {
    let expectation = self.expectation(description: #function)

    let jsonString = "{\n" +
      "  \"prefixes\": [\n" +
      "    \"object/prefixWithoutSlash\",\n" +
      "    \"object/prefixWithSlash/\"\n" +
      "  ],\n" +
      "  \"items\": [\n" +
      "    {\n" +
      "      \"name\": \"object/data1.dat\",\n" +
      "      \"bucket\": \"bucket.appspot.com\"\n" +
      "    },\n" +
      "    {\n" +
      "      \"name\": \"object/data2.dat\",\n" +
      "      \"bucket\": \"bucket.appspot.com\"\n" +
      "    },\n" +
      "  ],\n" +
      "  \"nextPageToken\": \"foo\"" +
      "}"

    let responseData = try XCTUnwrap(jsonString.data(using: .utf8))

    fetcherService?.testBlock = { (fetcher: GTMSessionFetcher,
                                   response: GTMSessionFetcherTestResponse) in
        let httpResponse = HTTPURLResponse(url: (fetcher.request?.url)!,
                                           statusCode: 200,
                                           httpVersion: "HTTP/1.1",
                                           headerFields: nil)
        response(httpResponse, responseData, nil)
    }

    let storage = storage()
    let ref = storage.reference(withPath: "object")
    let task = StorageListTask(
      reference: ref,
      fetcherService: fetcherService!.self,
      queue: dispatchQueue!.self,
      pageSize: nil,
      previousPageToken: nil
    ) { result, error in
      XCTAssertNotNil(result)
      XCTAssertNil(error)

      XCTAssertEqual(result?.items, [ref.child("data1.dat"), ref.child("data2.dat")])
      XCTAssertEqual(
        result?.prefixes,
        [ref.child("prefixWithoutSlash"), ref.child("prefixWithSlash")]
      )
      XCTAssertEqual(result?.pageToken, "foo")

      expectation.fulfill()
    }
    task.enqueue()
    waitForExpectation(test: self)
  }

  func testListWithErrorResponse() throws {
    let expectation = self.expectation(description: #function)

    let error = NSError(domain: "com.google.firebase.storage", code: 404)

    fetcherService?.testBlock = { (fetcher: GTMSessionFetcher,
                                   response: GTMSessionFetcherTestResponse) in
        let httpResponse = HTTPURLResponse(url: (fetcher.request?.url)!,
                                           statusCode: 403,
                                           httpVersion: "HTTP/1.1",
                                           headerFields: nil)
        response(httpResponse, nil, error)
    }

    let storage = storage()
    let ref = storage.reference(withPath: "object")
    let task = StorageListTask(
      reference: ref,
      fetcherService: fetcherService!.self,
      queue: dispatchQueue!.self,
      pageSize: nil,
      previousPageToken: nil
    ) { result, error in
      XCTAssertNotNil(error)
      XCTAssertNil(result)

      XCTAssertEqual(error!.domain, "FIRStorageErrorDomain")
      XCTAssertEqual(error!.code, StorageErrorCode.objectNotFound.rawValue)

      expectation.fulfill()
    }
    task.enqueue()
    waitForExpectation(test: self)
  }
}
