//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import XCTest
@testable import NIO
@testable import NIOHTTP1

class HTTPHeadersTest : XCTestCase {
    func testCasePreservedButInsensitiveLookup() {
        let originalHeaders = [ ("User-Agent", "1"),
                                ("host", "2"),
                                ("X-SOMETHING", "3"),
                                ("SET-COOKIE", "foo=bar"),
                                ("Set-Cookie", "buz=cux")]

        let headers = HTTPHeaders(originalHeaders)

        // looking up headers value is case-insensitive
        XCTAssertEqual(["1"], headers["User-Agent"])
        XCTAssertEqual(["1"], headers["User-agent"])
        XCTAssertEqual(["2"], headers["Host"])
        XCTAssertEqual(["foo=bar", "buz=cux"], headers["set-cookie"])

        for (key,value) in headers {
            switch key {
            case "User-Agent":
                XCTAssertEqual("1", value)
            case "host":
                XCTAssertEqual("2", value)
            case "X-SOMETHING":
                XCTAssertEqual("3", value)
            case "SET-COOKIE":
                XCTAssertEqual("foo=bar", value)
            case "Set-Cookie":
                XCTAssertEqual("buz=cux", value)
            default:
                XCTFail("Unexpected key: \(key)")
            }
        }
    }

    func testDictionaryLiteralAlternative() {
        let headers: HTTPHeaders = [ "User-Agent": "1",
                                     "host": "2",
                                     "X-SOMETHING": "3",
                                     "SET-COOKIE": "foo=bar",
                                     "Set-Cookie": "buz=cux"]

        // looking up headers value is case-insensitive
        XCTAssertEqual(["1"], headers["User-Agent"])
        XCTAssertEqual(["1"], headers["User-agent"])
        XCTAssertEqual(["2"], headers["Host"])
        XCTAssertEqual(["foo=bar", "buz=cux"], headers["set-cookie"])

        for (key,value) in headers {
            switch key {
            case "User-Agent":
                XCTAssertEqual("1", value)
            case "host":
                XCTAssertEqual("2", value)
            case "X-SOMETHING":
                XCTAssertEqual("3", value)
            case "SET-COOKIE":
                XCTAssertEqual("foo=bar", value)
            case "Set-Cookie":
                XCTAssertEqual("buz=cux", value)
            default:
                XCTFail("Unexpected key: \(key)")
            }
        }
    }

    func testWriteHeadersSeparately() {
        let originalHeaders = [ ("User-Agent", "1"),
                                ("host", "2"),
                                ("X-SOMETHING", "3"),
                                ("X-Something", "4"),
                                ("SET-COOKIE", "foo=bar"),
                                ("Set-Cookie", "buz=cux")]

        let headers = HTTPHeaders(originalHeaders)
        let channel = EmbeddedChannel()
        var buffer = channel.allocator.buffer(capacity: 1024)
        buffer.write(headers: headers)

        let writtenBytes = buffer.getString(at: buffer.readerIndex, length: buffer.readableBytes)!
        XCTAssertTrue(writtenBytes.contains("User-Agent: 1\r\n"))
        XCTAssertTrue(writtenBytes.contains("host: 2\r\n"))
        XCTAssertTrue(writtenBytes.contains("X-SOMETHING: 3\r\n"))
        XCTAssertTrue(writtenBytes.contains("X-Something: 4\r\n"))
        XCTAssertTrue(writtenBytes.contains("SET-COOKIE: foo=bar\r\n"))
        XCTAssertTrue(writtenBytes.contains("Set-Cookie: buz=cux\r\n"))

        XCTAssertTrue(try channel.finish().isClean)
    }

    func testRevealHeadersSeparately() {
        let originalHeaders = [ ("User-Agent", "1"),
                                ("host", "2"),
                                ("X-SOMETHING", "3, 4"),
                                ("X-Something", "5")]

        let headers = HTTPHeaders(originalHeaders)
        XCTAssertEqual(headers[canonicalForm: "user-agent"], ["1"])
        XCTAssertEqual(headers[canonicalForm: "host"], ["2"])
        XCTAssertEqual(headers[canonicalForm: "x-something"], ["3", "4", "5"])
        XCTAssertEqual(headers[canonicalForm: "foo"], [])
    }

    func testSubscriptDoesntSplitHeaders() {
        let originalHeaders = [ ("User-Agent", "1"),
                                ("host", "2"),
                                ("X-SOMETHING", "3, 4"),
                                ("X-Something", "5")]

        let headers = HTTPHeaders(originalHeaders)
        XCTAssertEqual(headers["user-agent"], ["1"])
        XCTAssertEqual(headers["host"], ["2"])
        XCTAssertEqual(headers["x-something"], ["3, 4", "5"])
        XCTAssertEqual(headers["foo"], [])
    }

    func testCanonicalisationDoesntHappenForSetCookie() {
        let originalHeaders = [ ("User-Agent", "1"),
                                ("host", "2"),
                                ("Set-Cookie", "foo=bar; expires=Sun, 17-Mar-2013 13:49:50 GMT"),
                                ("Set-Cookie", "buz=cux; expires=Fri, 13 Oct 2017 21:21:41 GMT")]

        let headers = HTTPHeaders(originalHeaders)
        XCTAssertEqual(headers[canonicalForm: "user-agent"], ["1"])
        XCTAssertEqual(headers[canonicalForm: "host"], ["2"])
        XCTAssertEqual(headers[canonicalForm: "set-cookie"], ["foo=bar; expires=Sun, 17-Mar-2013 13:49:50 GMT",
                                                              "buz=cux; expires=Fri, 13 Oct 2017 21:21:41 GMT"])
    }

    func testTrimWhitespaceWorksOnEmptyString() {
        let expected = ""
        let actual = String("".trimASCIIWhitespace())
        XCTAssertEqual(expected, actual)
    }

    func testTrimWhitespaceWorksOnOnlyWhitespace() {
        let expected = ""
        for wsString in [" ", "\t", "\r", "\n", "\r\n", "\n\r", "  \r\n \n\r\t\r\t\n"] {
            let actual = String(wsString.trimASCIIWhitespace())
            XCTAssertEqual(expected, actual)
        }
    }

    func testTrimWorksWithCharactersInTheMiddleAndWhitespaceAround() {
        let expected = "x"
        let actual = String("         x\n\n\n".trimASCIIWhitespace())
        XCTAssertEqual(expected, actual)
    }

    func testContains() {
        let originalHeaders = [ ("X-Header", "1"),
                                ("X-SomeHeader", "3"),
                                ("X-Header", "2")]

        let headers = HTTPHeaders(originalHeaders)
        XCTAssertTrue(headers.contains(name: "x-header"))
        XCTAssertTrue(headers.contains(name: "X-Header"))
        XCTAssertFalse(headers.contains(name: "X-NonExistingHeader"))
    }
    
    func testKeepAliveStateStartsWithClose() {
        var headers = HTTPHeaders([("Connection", "close")])

        XCTAssertEqual("close", headers["connection"].first)
        XCTAssertFalse(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))

        headers.replaceOrAdd(name: "connection", value: "keep-alive")

        XCTAssertEqual("keep-alive", headers["connection"].first)
        XCTAssertTrue(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))

        headers.remove(name: "connection")
        XCTAssertTrue(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))
        XCTAssertFalse(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 0)))
    }

    func testKeepAliveStateStartsWithKeepAlive() {
        var headers = HTTPHeaders([("Connection", "keep-alive")])

        XCTAssertEqual("keep-alive", headers["connection"].first)
        XCTAssertTrue(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))

        headers.replaceOrAdd(name: "connection", value: "close")

        XCTAssertEqual("close", headers["connection"].first)
        XCTAssertFalse(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))

        headers.remove(name: "connection")
        XCTAssertTrue(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))
        XCTAssertFalse(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 0)))
    }

    func testKeepAliveStateHasKeepAlive() {
        let headers = HTTPHeaders([("Connection", "other, keEP-alive"),
                                   ("Content-Type", "text/html"),
                                   ("Connection", "server, x-options")])
        
        XCTAssertTrue(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))
    }

    func testKeepAliveStateHasClose() {
        let headers = HTTPHeaders([("Connection", "x-options,  other"),
                                   ("Content-Type", "text/html"),
                                   ("Connection", "server,     clOse")])
        
        XCTAssertFalse(headers.isKeepAlive(version: HTTPVersion(major: 1, minor: 1)))
    }
        
    func testRandomAccess() {
        let originalHeaders = [ ("X-first", "one"),
                                ("X-second", "two")]
        let headers = HTTPHeaders(originalHeaders)
        
        XCTAssertEqual(headers[headers.startIndex].name, originalHeaders.first?.0)
        XCTAssertEqual(headers[headers.startIndex].value, originalHeaders.first?.1)
        XCTAssertEqual(headers[headers.index(before: headers.endIndex)].name, originalHeaders.last?.0)
        XCTAssertEqual(headers[headers.index(before: headers.endIndex)].value, originalHeaders.last?.1)
        
        let beforeLast = headers[headers.index(before: headers.endIndex)]
        XCTAssertEqual(beforeLast.name, originalHeaders[originalHeaders.endIndex - 1].0)
        XCTAssertEqual(beforeLast.value, originalHeaders[originalHeaders.endIndex - 1].1)
        
        let afterFirst = headers[headers.index(after: headers.startIndex)]
        XCTAssertEqual(afterFirst.name, originalHeaders[originalHeaders.startIndex + 1].0)
        XCTAssertEqual(afterFirst.value, originalHeaders[originalHeaders.startIndex + 1].1)
    }

    func testCanBeSeededWithKeepAliveState() {
        // we may later on decide that this test doesn't make sense but for now we want to keep the seeding behaviour.
        let headersSeededWithClose = HTTPHeaders([], keepAliveState: .close)
        XCTAssertEqual(false, headersSeededWithClose.isKeepAlive(version: .init(major: 0, minor: 0)))

        let headersSeededWithKeepAlive = HTTPHeaders([], keepAliveState: .keepAlive)
        XCTAssertEqual(true, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 0, minor: 0)))
    }

    func testSeedDominatesActualValue() {
        // we may later on decide that this test doesn't make sense but for now we want to keep the seeding behaviour
        let headersSeededWithClose = HTTPHeaders([], keepAliveState: .close)
        XCTAssertEqual(false, headersSeededWithClose.isKeepAlive(version: .init(major: 1, minor: 1)))

        let headersSeededWithKeepAlive = HTTPHeaders([], keepAliveState: .keepAlive)
        XCTAssertEqual(true, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 1, minor: 0)))
    }

    func testSeedDominatesEvenAfterMutation() {
        // we may later on decide that this test doesn't make sense but for now we want to keep the seeding behaviour
        var headersSeededWithClose = HTTPHeaders([], keepAliveState: .close)
        headersSeededWithClose.add(name: "foo", value: "bar")
        headersSeededWithClose.add(name: "bar", value: "qux")
        headersSeededWithClose.remove(name: "bar")
        XCTAssertEqual(false, headersSeededWithClose.isKeepAlive(version: .init(major: 1, minor: 1)))

        var headersSeededWithKeepAlive = HTTPHeaders([], keepAliveState: .keepAlive)
        headersSeededWithKeepAlive.add(name: "foo", value: "bar")
        headersSeededWithKeepAlive.add(name: "bar", value: "qux")
        headersSeededWithKeepAlive.remove(name: "bar")
        XCTAssertEqual(true, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 1, minor: 0)))
    }

    func testSeedGetsUpdatedToDefaultOnConnectionHeaderModification() {
        var headersSeededWithClose = HTTPHeaders([], keepAliveState: .close)
        headersSeededWithClose.add(name: "connection", value: "bar")
        XCTAssertEqual(true, headersSeededWithClose.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, headersSeededWithClose.isKeepAlive(version: .init(major: 1, minor: 0)))

        var headersSeededWithKeepAlive = HTTPHeaders([], keepAliveState: .keepAlive)
        headersSeededWithKeepAlive.add(name: "connection", value: "bar")
        XCTAssertEqual(true, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 1, minor: 0)))
    }

    func testSeedGetsUpdatedToWhateverTheHeaderSaysIfPresent() {
        var headersSeededWithClose = HTTPHeaders([], keepAliveState: .close)
        headersSeededWithClose.add(name: "connection", value: "bar,keep-alive,true")
        XCTAssertEqual(true, headersSeededWithClose.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(true, headersSeededWithClose.isKeepAlive(version: .init(major: 1, minor: 0)))

        var headersSeededWithKeepAlive = HTTPHeaders([], keepAliveState: .keepAlive)
        headersSeededWithKeepAlive.add(name: "connection", value: "bar,close,true")
        XCTAssertEqual(false, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, headersSeededWithKeepAlive.isKeepAlive(version: .init(major: 1, minor: 0)))
    }

    func testWeDefaultToCloseIfDoesNotMakeSense() {
        var nonSenseInOneHeaderCK = HTTPHeaders([])
        nonSenseInOneHeaderCK.add(name: "connection", value: "close,keep-alive")
        XCTAssertEqual(false, nonSenseInOneHeaderCK.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, nonSenseInOneHeaderCK.isKeepAlive(version: .init(major: 1, minor: 0)))

        var nonSenseInMultipleHeadersCK = HTTPHeaders([])
        nonSenseInMultipleHeadersCK.add(name: "connection", value: "close")
        nonSenseInMultipleHeadersCK.add(name: "connection", value: "keep-alive")
        XCTAssertEqual(false, nonSenseInMultipleHeadersCK.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, nonSenseInMultipleHeadersCK.isKeepAlive(version: .init(major: 1, minor: 0)))

        var nonSenseInOneHeaderKC = HTTPHeaders([])
        nonSenseInOneHeaderKC.add(name: "connection", value: "keep-alive,close")
        XCTAssertEqual(false, nonSenseInOneHeaderKC.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, nonSenseInOneHeaderKC.isKeepAlive(version: .init(major: 1, minor: 0)))

        var nonSenseInMultipleHeadersKC = HTTPHeaders([])
        nonSenseInMultipleHeadersKC.add(name: "connection", value: "keep-alive")
        nonSenseInMultipleHeadersKC.add(name: "connection", value: "close")
        XCTAssertEqual(false, nonSenseInMultipleHeadersKC.isKeepAlive(version: .init(major: 1, minor: 1)))
        XCTAssertEqual(false, nonSenseInMultipleHeadersKC.isKeepAlive(version: .init(major: 1, minor: 0)))

    }
}
