//
// Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file 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
import AWSCore

/// A holder for test case data from the SigV4 test suite. The
/// important fields are the name, expectedPresignedURL, and
/// originalRequest, which we'll use to exercise our presigned URL
/// code. The remaining properties are useful for debugging if the
/// values do not match as expected.
struct SigV4TestCase {
    let testCaseName: String

    // Given these setups (plus the other configs in SigV4TestCredentials)
    let credentialsProvider: AWSCredentialsProvider
    let shouldSignBody: Bool
    let shouldSignSecurityToken: Bool
    let serviceName: String

    // When we presign this original request
    let originalRequest: String

    // Then we get this presigned URL
    let expectedPresignedURL: String

    // Useful debugging properties
    let canonicalRequest: String
    let stringToSign: String
    let signature: String

    internal init(testCaseName: String, credentialsProvider: AWSCredentialsProvider, shouldSignBody: Bool = false, shouldSignSecurityToken: Bool = false, serviceName: String = "service", originalRequest: String, expectedPresignedURL: String, canonicalRequest: String, stringToSign: String, signature: String) {
        self.testCaseName = testCaseName
        self.credentialsProvider = credentialsProvider
        self.shouldSignBody = shouldSignBody
        self.shouldSignSecurityToken = shouldSignSecurityToken
        self.serviceName = serviceName
        self.originalRequest = originalRequest
        self.expectedPresignedURL = expectedPresignedURL
        self.canonicalRequest = canonicalRequest
        self.stringToSign = stringToSign
        self.signature = signature
    }
}

extension SigV4TestCase {
    func makeURLRequest(fromRequestString requestString: String) throws -> URLRequest {
        var lines = requestString.split(separator: "\n", maxSplits: Int.max, omittingEmptySubsequences: false)

        let startLine = lines.remove(at: 0)
        let startLineComponents = startLine.components(separatedBy: .whitespaces)

        let httpMethod = startLineComponents[0]
        let target = startLineComponents[1]
        let httpVersion = startLineComponents[2]
        guard httpVersion.lowercased().hasPrefix("http") else {
            throw "Not an HTTP request: \(originalRequest)"
        }

        // Remaining lines are headers, up to the first empty line, then body
        var headers = [String: String]()
        var hostFromHeader: String?
        var portFromHeader: String?
        var currentHeaderName: String?
        while !lines.isEmpty {
            let line = lines.remove(at: 0)

            if line == "" {
                break
            }

            // Folded headers are obsoleted, but we still need to be able to canonicalize them
            if line.range(of: "^\\s+", options: .regularExpression) != nil, let currentHeaderName = currentHeaderName {
                if let currentHeaderValue = headers[currentHeaderName] {
                    headers[currentHeaderName] = currentHeaderValue + String(line)
                } else {
                    headers[currentHeaderName] = String(line)
                }
                continue
            }

            // Get key/value pair, respecting potential colons in the value (e.g., "host:www.example.com:80")
            let headerComponents = line
                .split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
                .map { String($0) }

            let key = headerComponents[0]
            let value = headerComponents.last
            headers[key] = value
            currentHeaderName = key

            if key.lowercased() == "host" {
                hostFromHeader = value
                // If the incoming host header has a port, store it
                if headerComponents.count == 3 {
                    portFromHeader = headerComponents[2]
                }
            }
        }

        guard let host = hostFromHeader else {
            throw "No host in request: \(originalRequest)"
        }

        var portString = ""
        if let port = portFromHeader {
            portString = ":\(port)"
        }
        let urlString = "https://\(host)\(portString)\(target)"

        guard let url = URL(string: urlString) else {
            throw "Could not construct URL from \(urlString)"
        }

        var request = URLRequest(url: url)
        request.allHTTPHeaderFields = headers
        request.httpMethod = httpMethod

        if !lines.isEmpty {
            let body = lines.joined(separator: "\n")
            let bodyData = body.data(using: .utf8)
            request.httpBody = bodyData
        }

        return request
    }
}

extension SigV4TestCase: CustomStringConvertible {
    var description: String {
        let description =
        """
        ######################
        testCaseName: \(testCaseName)
        shouldSignBody: \(shouldSignBody)
        shouldSignSecurityToken: \(shouldSignSecurityToken)
        access_key: \(credentialsProvider)
        expectedPresignedURL: \(expectedPresignedURL)

        originalRequest:
        \(originalRequest)

        canonicalRequest:
        \(canonicalRequest)

        stringToSign:
        \(stringToSign)

        signature: \(signature)
        
        (end \(testCaseName))
        ######################
        """

        return description
    }
}
