package awsauth

import (
	"net/http"
	"net/url"
	"strings"
	"testing"

	. "github.com/smartystreets/goconvey/convey"
)

func TestVersion4RequestPreparer(t *testing.T) {
	Convey("Given a plain request with no custom headers", t, func() {
		request := test_plainRequestV4(false)

		expectedUnsigned := test_unsignedRequestV4(true, false)
		expectedUnsigned.Header.Set("X-Amz-Date", timestampV4())

		Convey("The necessary, default headers should be appended", func() {
			prepareRequestV4(request)
			So(request, ShouldResemble, expectedUnsigned)
		})

		Convey("Forward-slash should be appended to URI if not present", func() {
			prepareRequestV4(request)
			So(request.URL.Path, ShouldEqual, "/")
		})

		Convey("And a set of credentials", func() {
			var keys Credentials
			keys = *testCredV4

			Convey("It should be signed with an Authorization header", func() {
				actualSigned := Sign4(request, keys)
				actual := actualSigned.Header.Get("Authorization")

				So(actual, ShouldNotBeBlank)
				So(actual, ShouldContainSubstring, "Credential="+testCredV4.AccessKeyID)
				So(actual, ShouldContainSubstring, "SignedHeaders=")
				So(actual, ShouldContainSubstring, "Signature=")
				So(actual, ShouldContainSubstring, "AWS4")
			})
		})
	})

	Convey("Given a request with custom, necessary headers", t, func() {
		Convey("The custom, necessary headers must not be changed", func() {
			request := test_unsignedRequestV4(true, false)
			prepareRequestV4(request)
			So(request, ShouldResemble, test_unsignedRequestV4(true, false))
		})
	})
}

func TestVersion4STSRequestPreparer(t *testing.T) {
	Convey("Given a plain request with no custom headers", t, func() {
		request := test_plainRequestV4(false)

		Convey("And a set of credentials with an STS token", func() {
			var keys Credentials
			keys = *testCredV4WithSTS

			Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
				actualSigned := Sign4(request, keys)
				actual := actualSigned.Header.Get("X-Amz-Security-Token")

				So(actual, ShouldNotBeBlank)
				So(actual, ShouldEqual, testCredV4WithSTS.SecurityToken)

			})
		})
	})

}

func TestVersion4SigningTasks(t *testing.T) {
	// http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html

	Convey("Given a bogus request and credentials from AWS documentation with an additional meta tag", t, func() {
		request := test_unsignedRequestV4(true, true)
		meta := new(metadata)

		Convey("(Task 1) The canonical request should be built correctly", func() {
			hashedCanonReq := hashedCanonicalRequestV4(request, meta)

			So(hashedCanonReq, ShouldEqual, expectingV4["CanonicalHash"])
		})

		Convey("(Task 2) The string to sign should be built correctly", func() {
			hashedCanonReq := hashedCanonicalRequestV4(request, meta)
			stringToSign := stringToSignV4(request, hashedCanonReq, meta)

			So(stringToSign, ShouldEqual, expectingV4["StringToSign"])
		})

		Convey("(Task 3) The version 4 signed signature should be correct", func() {
			hashedCanonReq := hashedCanonicalRequestV4(request, meta)
			stringToSign := stringToSignV4(request, hashedCanonReq, meta)
			signature := signatureV4(test_signingKeyV4(), stringToSign)

			So(signature, ShouldEqual, expectingV4["SignatureV4"])
		})
	})
}

func TestSignature4Helpers(t *testing.T) {

	keys := *testCredV4

	Convey("The signing key should be properly generated", t, func() {
		expected := []byte{152, 241, 216, 137, 254, 196, 244, 66, 26, 220, 82, 43, 171, 12, 225, 248, 46, 105, 41, 194, 98, 237, 21, 229, 169, 76, 144, 239, 209, 227, 176, 231}
		actual := test_signingKeyV4()

		So(actual, ShouldResemble, expected)
	})

	Convey("Authorization headers should be built properly", t, func() {
		meta := &metadata{
			algorithm:       "AWS4-HMAC-SHA256",
			credentialScope: "20110909/us-east-1/iam/aws4_request",
			signedHeaders:   "content-type;host;x-amz-date",
		}
		expected := expectingV4["AuthHeader"] + expectingV4["SignatureV4"]
		actual := buildAuthHeaderV4(expectingV4["SignatureV4"], meta, keys)

		So(actual, ShouldEqual, expected)
	})

	Convey("Timestamps should be in the correct format, in UTC time", t, func() {
		actual := timestampV4()

		So(len(actual), ShouldEqual, 16)
		So(actual, ShouldNotContainSubstring, ":")
		So(actual, ShouldNotContainSubstring, "-")
		So(actual, ShouldNotContainSubstring, " ")
		So(actual, ShouldEndWith, "Z")
		So(actual, ShouldContainSubstring, "T")
	})

	Convey("Given an Version 4 AWS-formatted timestamp", t, func() {
		ts := "20110909T233600Z"

		Convey("The date string should be extracted properly", func() {
			So(tsDateV4(ts), ShouldEqual, "20110909")
		})
	})

	Convey("Given any request with a body", t, func() {
		request := test_plainRequestV4(false)

		Convey("Its body should be read and replaced without differences", func() {
			expected := []byte(requestValuesV4.Encode())

			actual1 := readAndReplaceBody(request)
			So(actual1, ShouldResemble, expected)

			actual2 := readAndReplaceBody(request)
			So(actual2, ShouldResemble, expected)
		})
	})
}

func test_plainRequestV4(trailingSlash bool) *http.Request {
	url := "http://iam.amazonaws.com"
	body := strings.NewReader(requestValuesV4.Encode())

	if trailingSlash {
		url += "/"
	}

	request, err := http.NewRequest("POST", url, body)

	if err != nil {
		panic(err)
	}

	return request
}

func test_unsignedRequestV4(trailingSlash, tag bool) *http.Request {
	request := test_plainRequestV4(trailingSlash)
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
	request.Header.Set("X-Amz-Date", "20110909T233600Z")
	if tag {
		request.Header.Set("X-Amz-Meta-Foo", "Bar!")
	}
	return request
}

func test_signingKeyV4() []byte {
	return signingKeyV4(testCredV4.SecretAccessKey, "20110909", "us-east-1", "iam")
}

var (
	testCredV4 = &Credentials{
		AccessKeyID:     "AKIDEXAMPLE",
		SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
	}

	testCredV4WithSTS = &Credentials{
		AccessKeyID:     "AKIDEXAMPLE",
		SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
		SecurityToken:   "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
	}

	expectingV4 = map[string]string{
		"CanonicalHash": "41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92",
		"StringToSign":  "AWS4-HMAC-SHA256\n20110909T233600Z\n20110909/us-east-1/iam/aws4_request\n41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92",
		"SignatureV4":   "08292a4b86aae1a6f80f1988182a33cbf73ccc70c5da505303e355a67cc64cb4",
		"AuthHeader":    "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=",
	}

	requestValuesV4 = &url.Values{
		"Action":  []string{"ListUsers"},
		"Version": []string{"2010-05-08"},
	}
)
