//
//  RequestTests.swift
//
//  Created by Steve Madsen on 5/8/20.
//  Copyright © 2021 Light Year Software, LLC.
//

import XCTest
import Nimble
@testable import Request

class RequestTests: XCTestCase {
    let session = TestSession()
    private var request = TestRequest()

    override func setUpWithError() throws {
        try super.setUpWithError()
        RequestTaskManager.shared.session = session
    }

    func testSuccess() {
        session.allow(.get("https://api/test"), return: 200, body: "1")
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = request.start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished:           expectation.fulfill()
                case .failure(let error): fail("\(error)")
                }
            }, receiveValue: {
                expect($0) == 1
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testServerFailure() {
        session.allow(.get("https://api/test"), return: 500, body: "1")
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = request.start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: fail("should not succeed")
                case .failure(RequestError.serverFailure(let status)):
                    expect(status) == 500
                    expectation.fulfill()
                case .failure: fail("failed with the wrong error type")
                }
            }, receiveValue: { _ in
                fail("should not receive a value")
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testCustomErrorHandler() {
        session.allow(.get("https://api/test"), return: 500, body: "{}")
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = CustomErrorRequest().start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: fail("should not succeed")
                case .failure(let error as NSError):
                    expect(error.domain) == "TestDomain"
                    expect(error.code) == 500
                    expect(error.userInfo["body"] as? Data) == "{}".data(using: .utf8)
                    expectation.fulfill()
                }
            }, receiveValue: { _ in
                fail("should not receive a value")
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testInvalidResponseType() {
        session.allow(.get("https://api/test"), return: 200, headers: ["Content-Type": "text/html"], body: "a")
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = request.start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: fail("should not succeed")
                case .failure(RequestError.wrongContentType):
                    expectation.fulfill()
                case .failure: fail("failed with the wrong error type")
                }
            }, receiveValue: { _ in
                fail("should not receive a value")
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testEmptyResponse() {
        session.allow(.get("https://api/test"), return: 204, headers: ["Content-Length": "0"], body: "")
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = EmptyResponseRequest().start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: expectation.fulfill()
                case .failure:  fail("should not fail")
                }
            }, receiveValue: {
                expect($0) == 1
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testParsingFailure() {
        session.allow(.get("https://api/test"), return: 200, body: "a")
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = request.start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: fail("should not succeed")
                case .failure(RequestError.parseError):
                    expectation.fulfill()
                case .failure: fail("failed with the wrong error type")
                }
            }, receiveValue: { _ in
                fail("should not receive a value")
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testNetworkFailure() {
        session.allow(.get("https://api/test"), return: NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost, userInfo: nil))
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = request.start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: fail("should not succeed")
                case .failure(let error as NSError):
                    expect(error.domain) == NSURLErrorDomain
                    expect(error.code) == NSURLErrorCannotFindHost
                    expectation.fulfill()
                }
            }, receiveValue: { _ in
                fail("should not receive a value")
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }

    func testNonTransientNetworkFailureIsLogged() {
        session.allow(.get("https://api/test"), return: NSError(domain: NSURLErrorDomain, code: NSURLErrorServerCertificateUntrusted, userInfo: nil))
        let expectation = XCTestExpectation(description: "GET /test")
        let publisher = request.start()
            .sink(receiveCompletion: { result in
                switch result {
                case .finished: fail("should not succeed")
                case .failure(_):
                    expect(self.request.errors).toNot(beEmpty())
                    expectation.fulfill()
                }
            }, receiveValue: { _ in
                fail("should not receive a value")
            })
        expect(publisher).toNot(beNil())
        wait(for: [expectation], timeout: 1)
    }
}

private class TestRequest: Request {
    let baseURL = URL(string: "https://api")!
    let path = "/test"

    var errors = [String]()

    func parseResponse(context: Void?, data: Data) throws -> Int {
        if let string = String(data: data, encoding: .utf8), let value = Int(string) {
            return value
        } else {
            throw RequestError.parseError
        }
    }

    func logError(_ message: String, data: [String: Any]) {
        errors.append(message)
    }
}

private class EmptyResponseRequest: Request {
    let baseURL = URL(string: "https://api")!
    let path = "/test"

    func parseResponse(context: Void?, data: Data) throws -> Int {
        1
    }
}

private class CustomErrorRequest: Request {
    let baseURL = URL(string: "https://api")!
    let path = "/test"

    func parseResponse(context: Void?, data: Data) throws {
    }

    func parseError(status: Int, data: Data?) -> Error {
        NSError(domain: "TestDomain", code: status, userInfo: ["body": data!])
    }
}