// Copyright Istio Authors
//
// 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.

package util

import (
	"fmt"
	"reflect"
	"testing"
	"time"
)

var (
	// thirdPartyJwt is generated in a testing K8s cluster, using the "istio-token" projected volume.
	// Token is issued at 2020-04-04 22:13:54 Pacific Daylight time.
	// Expiration time is 2020-04-05 10:13:54 Pacific Daylight time.
	thirdPartyJwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9." +
		"eyJhdWQiOlsieW9uZ2dhbmdsLWlzdGlvLTQuc3ZjLmlkLmdvb2ciXSwiZXhwIjoxNTg2MTA2ODM0LCJpYXQiOjE1" +
		"ODYwNjM2MzQsImlzcyI6Imh0dHBzOi8vY29udGFpbmVyLmdvb2dsZWFwaXMuY29tL3YxL3Byb2plY3RzL3lvbmdn" +
		"YW5nbC1pc3Rpby00L2xvY2F0aW9ucy91cy1jZW50cmFsMS1hL2NsdXN0ZXJzL2NsdXN0ZXItMyIsImt1YmVybmV0" +
		"ZXMuaW8iOnsibmFtZXNwYWNlIjoiZm9vIiwicG9kIjp7Im5hbWUiOiJodHRwYmluLTY0Nzc2YmY3OGQtanFsNWIi" +
		"LCJ1aWQiOiI5YWQ3NTcxYi03NjBhLTExZWEtODllNy00MjAxMGE4MDAxYzEifSwic2VydmljZWFjY291bnQiOnsi" +
		"bmFtZSI6Imh0dHBiaW4iLCJ1aWQiOiI5OWY2NWY1MC03NjBhLTExZWEtODllNy00MjAxMGE4MDAxYzEifX0sIm5i" +
		"ZiI6MTU4NjA2MzYzNCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmZvbzpodHRwYmluIn0.XWSCdarBR0cx" +
		"MlVV5X9pvgI9m0lyO-17B45aBKJBIQjvjluKXqxnCuIeD3X2ItLkCUzmKUa3ftTjUUov1MJ89MdBngNfUP7IfwnD" +
		"2dBl7Jtju0-Ks7aTFOkgtoMYqNnQ1VSDTAOfNpdZVUnsR_oY8obXSQR_H4uMcaNOGED2RX5HLBWFlvymtn4JXuyg" +
		"_rpOrJ8dv-snrmO3LT9y-zaUnZqSceDC8skzStrJIRvsRkO8GEcoQd5VwDn-UVgOcqWb-S-vgSjdtwBsnGPXsh_I" +
		"NZCq3ftr0Qu8-IxsIjpMjhLmAGTH1bR324aqLTYhAXp6fk06Pe3T9stCY5acSeadKA"
	// firstPartyJwt is generated in a testing K8s cluster. It is the default service account JWT.
	// No expiration time.
	firstPartyJwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9." +
		"eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9u" +
		"YW1lc3BhY2UiOiJmb28iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiaHR0cGJp" +
		"bi10b2tlbi14cWRncCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUi" +
		"OiJodHRwYmluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiOTlm" +
		"NjVmNTAtNzYwYS0xMWVhLTg5ZTctNDIwMTBhODAwMWMxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmZv" +
		"bzpodHRwYmluIn0.4kIl9TRjXEw6DfhtR-LdpxsAlYjJgC6Ly1DY_rqYY4h0haxcXB3kYZ3b2He-3fqOBryz524W" +
		"KkZscZgvs5L-sApmvlqdUG61TMAl7josB0x4IMHm1NS995LNEaXiI4driffwfopvqc_z3lVKfbF9j-mBgnCepxz3" +
		"UyWo5irFa3qcwbOUB9kuuUNGBdtbFBN5yIYLpfa9E-MtTX_zJ9fQ9j2pi8Z4ljii0tEmPmRxokHkmG_xNJjUkxKU" +
		"WZf4bLDdCEjVFyshNae-FdxiUVyeyYorTYzwZZYQch9MJeedg4keKKUOvCCJUlKixd2qAe-H7r15RPmo4AU5O5YL" +
		"65xiNg"

	// oneAudString includes one `aud` claim "abc" of type string.
	oneAudString = "header.eyJhdWQiOiJhYmMiLCJleHAiOjQ3MzI5OTQ4MDEsImlhdCI6MTU3OTM5NDgwMSwiaXNzIjoidGVzdC1pc3N1ZXItMUBpc3Rpby5pbyIsInN1YiI6InN1Yi0xIn0.signature" // nolint: lll

	// twoAudList includes two `aud` claims ["abc", "xyz"] of type []string.
	twoAudList = "header.eyJhdWQiOlsiYWJjIiwieHl6Il0sImV4cCI6NDczMjk5NDgwMSwiaWF0IjoxNTc5Mzk0ODAxLCJpc3MiOiJ0ZXN0LWlzc3Vlci0xQGlzdGlvLmlvIiwic3ViIjoic3ViLTEifQ.signature" // nolint: lll

	// A JWT encoded payload that has the padding stripped and contains an underscore character from the base64url alphabet
	base64UrlEncodedPaddingStrippedPart = "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjoiSm9lIEFyZG_DsWV6In0"
)

func TestGetExp(t *testing.T) {
	testCases := map[string]struct {
		jwt         string
		expectedExp time.Time
		expectedErr error
	}{
		"jwt with expiration time": {
			jwt:         thirdPartyJwt,
			expectedExp: time.Date(2020, time.April, 5, 10, 13, 54, 0, time.FixedZone("PDT", -int((7*time.Hour).Seconds()))),
			expectedErr: nil,
		},
		"jwt with no expiration time": {
			jwt:         firstPartyJwt,
			expectedExp: time.Time{},
			expectedErr: nil,
		},
		"invalid jwt": {
			jwt:         "invalid-section1.invalid-section2.invalid-section3",
			expectedExp: time.Time{},
			expectedErr: fmt.Errorf("failed to decode the JWT claims"),
		},
	}

	for id, tc := range testCases {
		t.Run(id, func(t *testing.T) {
			exp, err := GetExp(tc.jwt)
			if err != nil && tc.expectedErr == nil || err == nil && tc.expectedErr != nil {
				t.Errorf("%s: Got error \"%v\", expected error \"%v\"", id, err, tc.expectedErr)
			} else if err != nil && tc.expectedErr != nil && err.Error() != tc.expectedErr.Error() {
				t.Errorf("%s: Got error \"%v\", expected error \"%v\"", id, err, tc.expectedErr)
			} else if err == nil && exp.Sub(tc.expectedExp) != time.Duration(0) {
				t.Errorf("%s: Got expiration time: %s, expected expiration time: %s",
					id, exp.String(), tc.expectedExp.String())
			}
		})
	}
}

func TestGetAud(t *testing.T) {
	testCases := map[string]struct {
		jwt string
		aud []string
	}{
		"no audience": {
			jwt: firstPartyJwt,
		},
		"one audience string": {
			jwt: oneAudString,
			aud: []string{"abc"},
		},
		"one audience list": {
			jwt: thirdPartyJwt,
			aud: []string{"yonggangl-istio-4.svc.id.goog"},
		},
		"two audiences list": {
			jwt: twoAudList,
			aud: []string{"abc", "xyz"},
		},
	}

	for id, tc := range testCases {
		t.Run(id, func(t *testing.T) {
			if got, _ := GetAud(tc.jwt); !reflect.DeepEqual(tc.aud, got) {
				t.Errorf("want audience %v but got %v", tc.aud, got)
			}
		})
	}
}

func Test3p(t *testing.T) {
	for _, s := range []string{thirdPartyJwt, "InvalidToken"} {
		if IsK8SUnbound(s) {
			t.Error("Expecting bound token, detected unbound ", s)
		}
	}
	for _, s := range []string{firstPartyJwt, ".bnVsbM."} {
		if !IsK8SUnbound(s) {
			t.Error("Expecting unbound, detected bound ", s)
		}
	}
}

func TestBase64UrlPartDecoding(t *testing.T) {
	payloadBytes, err := DecodeJwtPart(base64UrlEncodedPaddingStrippedPart)
	if err != nil {
		t.Error("Expected DecodeJwtPart success, got failure", err)
	}
	if payloadBytes == nil {
		t.Error("Expected DecodeJwtPart to return non-nil, got nil")
	}

	expectedAud := "Joe Ardoñez"
	if got, _ := GetAud("header." + base64UrlEncodedPaddingStrippedPart + ".signature"); len(got) != 1 || expectedAud != got[0] {
		t.Errorf("want audience %v but got %v", expectedAud, got)
	}
}
