// Copyright 2021 Google LLC
//
// 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 jwt_test

import (
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/tink-crypto/tink-go/v2/jwt"
)

const (
	invalidUTF8     = "\xF4\x7F\xBF\xBF"
	validExpiration = 1640043004
)

type testCase struct {
	tag   string
	opts  *jwt.RawJWTOptions
	json  string
	token *jwt.RawJWT
}

func refString(a string) *string {
	return &a
}

func refTime(ts int64) *time.Time {
	t := time.Unix(ts, 0)
	return &t
}

func TestCreatingRawJWTWithAllClaims(t *testing.T) {
	json := `{
				"sub": "tink-test-subject",
				"iss": "tink-test-issuer",
				"jti": "tink-jwt-id",
				"aud": ["aud-1", "aud-2"],
				"exp": 457888,
				"nbf": 450888,
				"iat": 400888,
				"cc-num": 1.67,
				"cc-bool": true,
				"cc-null": null,
				"cc-array": [1,2,3],
				"cc-string": "cc-val",
				"cc-object": {"nested-cc-num": 5.5}
			}`

	opts := &jwt.RawJWTOptions{
		TypeHeader: refString("typeHeader"),
		Subject:    refString("tink-test-subject"),
		Issuer:     refString("tink-test-issuer"),
		JWTID:      refString("tink-jwt-id"),
		Audiences:  []string{"aud-1", "aud-2"},
		ExpiresAt:  refTime(457888),
		NotBefore:  refTime(450888),
		IssuedAt:   refTime(400888),
		CustomClaims: map[string]interface{}{
			"cc-num":    1.67,
			"cc-bool":   true,
			"cc-null":   nil,
			"cc-string": "cc-val",
			"cc-array":  []interface{}{1.0, 2.0, 3.0},
			"cc-object": map[string]interface{}{"nested-cc-num": 5.5},
		},
	}
	fromJSON, err := jwt.NewRawJWTFromJSON(refString("typeHeader"), []byte(json))
	if err != nil {
		t.Fatalf("jwt.NewRawJWTFromJSON(%q): %v", json, err)
	}
	fromOpts, err := jwt.NewRawJWT(opts)
	if err != nil {
		t.Fatalf("jwt.NewRawJWT(%v): %v", opts, err)
	}
	for _, tc := range []testCase{
		{
			tag:   "jwt.NewRawJWTFromJSON",
			token: fromJSON,
		},
		{
			tag:   "NewRawJWT",
			token: fromOpts,
		},
	} {
		if !tc.token.HasTypeHeader() {
			t.Errorf("tc.token.HasTypeHeader() = false, want true")
		}
		if !tc.token.HasAudiences() {
			t.Errorf("tc.token.HasAudiences() = false, want true")
		}
		if !tc.token.HasSubject() {
			t.Errorf("tc.token.HasSubject() = false, want true")
		}
		if !tc.token.HasIssuer() {
			t.Errorf("tc.token.HasIssuer() = false, want true")
		}
		if !tc.token.HasJWTID() {
			t.Errorf("tc.token.HasJWTID() = false, want true")
		}
		if !tc.token.HasExpiration() {
			t.Errorf("tc.token.HasExpiration() = false, want true")
		}
		if !tc.token.HasNotBefore() {
			t.Errorf("tc.token.HasNotBefore() = false, want true")
		}
		if !tc.token.HasIssuedAt() {
			t.Errorf("tc.token.HasIssuedAt() = false, want true")
		}

		typeHeader, err := tc.token.TypeHeader()
		if err != nil {
			t.Errorf("tc.token.TypeHeader() err = %v, want nil", err)
		}
		if !cmp.Equal(typeHeader, *opts.TypeHeader) {
			t.Errorf("tc.token.TypeHeader() = %q, want %q", typeHeader, *opts.TypeHeader)
		}
		audiences, err := tc.token.Audiences()
		if err != nil {
			t.Errorf("tc.token.Audiences() err = %v, want nil", err)
		}
		if !cmp.Equal(audiences, opts.Audiences) {
			t.Errorf("tc.token.Audiences() = %q, want %q", audiences, opts.Audiences)
		}
		subject, err := tc.token.Subject()
		if err != nil {
			t.Errorf("tc.token.Subject() err = %v, want nil", err)
		}
		if !cmp.Equal(subject, *opts.Subject) {
			t.Errorf("tc.token.Subject() = %q, want %q", subject, *opts.Subject)
		}
		issuer, err := tc.token.Issuer()
		if err != nil {
			t.Errorf("tc.token.Issuer() err = %v, want nil", err)
		}
		if !cmp.Equal(issuer, *opts.Issuer) {
			t.Errorf("tc.token.Issuer() = %q, want %q", issuer, *opts.Issuer)
		}
		jwtID, err := tc.token.JWTID()
		if err != nil {
			t.Errorf("tc.token.JWTID() err = %v, want nil", err)
		}
		if !cmp.Equal(jwtID, *opts.JWTID) {
			t.Errorf("tc.token.JWTID() = %q, want %q", jwtID, *opts.JWTID)
		}
		expiresAt, err := tc.token.ExpiresAt()
		if err != nil {
			t.Errorf("tc.token.ExpiresAt() err = %v, want nil", err)
		}
		if !cmp.Equal(expiresAt, *opts.ExpiresAt) {
			t.Errorf("tc.token.ExpiresAt() = %q, want %q", expiresAt, *opts.ExpiresAt)
		}
		issuedAt, err := tc.token.IssuedAt()
		if err != nil {
			t.Errorf("tc.token.IssuedAt() err = %v, want nil", err)
		}
		if !cmp.Equal(issuedAt, *opts.IssuedAt) {
			t.Errorf("tc.token.IssuedAt() = %q, want %q", issuedAt, *opts.IssuedAt)
		}
		notBefore, err := tc.token.NotBefore()
		if err != nil {
			t.Errorf("tc.token.NotBefore() err = %v, want nil", err)
		}
		if !cmp.Equal(notBefore, *opts.NotBefore) {
			t.Errorf("tc.token.NotBefore() = %q, want %q", notBefore, *opts.NotBefore)
		}
		wantCustomClaims := []string{"cc-num", "cc-bool", "cc-null", "cc-string", "cc-array", "cc-object"}
		if !cmp.Equal(tc.token.CustomClaimNames(), wantCustomClaims, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
			t.Errorf("tc.token.CustomClaimNames() = %q, want %q", tc.token.CustomClaimNames(), wantCustomClaims)
		}
		if !tc.token.HasNumberClaim("cc-num") {
			t.Errorf("tc.token.HasNumberClaim('cc-num') = false, want true")
		}
		if !tc.token.HasBooleanClaim("cc-bool") {
			t.Errorf("tc.token.HasBooleanClaim('cc-bool') = false, want true")
		}
		if !tc.token.HasNullClaim("cc-null") {
			t.Errorf("tc.token.HasNullClaim('cc-null') = false, want true")
		}
		if !tc.token.HasStringClaim("cc-string") {
			t.Errorf("tc.token.HasStringClaim('cc-string') = false, want true")
		}
		if !tc.token.HasArrayClaim("cc-array") {
			t.Errorf("tc.token.HasArrayClaim('cc-array') = false, want true")
		}
		if !tc.token.HasObjectClaim("cc-object") {
			t.Errorf("tc.token.HasObjectClaim('cc-object') = false, want true")
		}

		number, err := tc.token.NumberClaim("cc-num")
		if err != nil {
			t.Errorf("tc.token.NumberClaim('cc-num') err = %v, want nil", err)
		}
		if !cmp.Equal(number, opts.CustomClaims["cc-num"]) {
			t.Errorf("tc.token.NumberClaim('cc-num') = %f, want %f", number, opts.CustomClaims["cc-num"])
		}
		boolean, err := tc.token.BooleanClaim("cc-bool")
		if err != nil {
			t.Errorf("tc.token.BooleanClaim('cc-bool') err = %v, want nil", err)
		}
		if !cmp.Equal(boolean, opts.CustomClaims["cc-bool"]) {
			t.Errorf("tc.token.BooleanClaim('cc-bool') = %v, want %v", boolean, opts.CustomClaims["cc-bool"])
		}
		str, err := tc.token.StringClaim("cc-string")
		if err != nil {
			t.Errorf("tc.token.StringClaim('cc-string') err = %v, want nil", err)
		}
		if !cmp.Equal(str, opts.CustomClaims["cc-string"]) {
			t.Errorf("tc.token.StringClaim('cc-string') = %q, want %q", str, opts.CustomClaims["cc-string"])
		}
		array, err := tc.token.ArrayClaim("cc-array")
		if err != nil {
			t.Errorf("tc.token.ArrayClaim('cc-array') err = %v, want nil", err)
		}
		if !cmp.Equal(array, opts.CustomClaims["cc-array"]) {
			t.Errorf("tc.token.ArrayClaim('cc-array') = %q, want %q", array, opts.CustomClaims["cc-array"])
		}
		object, err := tc.token.ObjectClaim("cc-object")
		if err != nil {
			t.Errorf("tc.token.ObjectClaim('cc-object') err = %v, want nil", err)
		}
		if !cmp.Equal(object, opts.CustomClaims["cc-object"]) {
			t.Errorf("tc.token.ObjectClaim('cc-object') = %q, want %q", object, opts.CustomClaims["cc-object"])
		}
	}
}

func TestGeneratingRawJWTWithoutClaims(t *testing.T) {
	jsonToken, err := jwt.NewRawJWTFromJSON(nil, []byte("{}"))
	if err != nil {
		t.Fatalf("jwt.NewRawJWTFromJSON({}): %v", err)
	}
	optsToken, err := jwt.NewRawJWT(&jwt.RawJWTOptions{WithoutExpiration: true})
	if err != nil {
		t.Fatalf("NewRawJWT with no claims: %v", err)
	}
	for _, tc := range []testCase{
		{
			tag:   "jwt.NewRawJWTFromJSON",
			token: jsonToken,
		},
		{
			tag:   "NewRawJWT",
			token: optsToken,
		},
	} {
		if tc.token.HasTypeHeader() {
			t.Errorf("tc.token.HasTypeHeader() = true, want false")
		}
		if tc.token.HasAudiences() {
			t.Errorf("tc.token.HasAudiences() = true, want false")
		}
		if tc.token.HasSubject() {
			t.Errorf("tc.token.HasSubject() = true, want false")
		}
		if tc.token.HasIssuer() {
			t.Errorf("tc.token.HasIssuer() = true, want false")
		}
		if tc.token.HasJWTID() {
			t.Errorf("tc.token.HasJWTID() = true, want false")
		}
		if tc.token.HasExpiration() {
			t.Errorf("tc.token.HasExpiration() = true, want false")
		}
		if tc.token.HasNotBefore() {
			t.Errorf("tc.token.HasNotBefore() = true, want false")
		}
		if tc.token.HasIssuedAt() {
			t.Errorf("tc.token.HasIssuedAt() = true, want false")
		}
		if _, err := tc.token.Audiences(); err == nil {
			t.Errorf("tc.token.Audiences() err = nil, want error")
		}
		if _, err := tc.token.Subject(); err == nil {
			t.Errorf("tc.token.Subject() err = nil, want error")
		}
		if _, err := tc.token.Issuer(); err == nil {
			t.Errorf("tc.token.Issuer() err = nil, want error")
		}
		if _, err := tc.token.JWTID(); err == nil {
			t.Errorf("tc.token.JWTID() err = nil, want error")
		}
		if _, err := tc.token.ExpiresAt(); err == nil {
			t.Errorf("tc.token.ExpiresAt() err = nil, want error")
		}
		if _, err := tc.token.IssuedAt(); err == nil {
			t.Errorf("tc.token.IssuedAt() err = nil, want error")
		}
		if _, err := tc.token.NotBefore(); err == nil {
			t.Errorf("tc.token.NotBefore() err = nil, want error")
		}
		if !cmp.Equal(tc.token.CustomClaimNames(), []string{}) {
			t.Errorf("tc.token.CustomClaimNames() = %q want %q", tc.token.CustomClaimNames(), []string{})
		}
	}
}

func TestNewRawJWTLargeValidTimestamps(t *testing.T) {
	opts := &jwt.RawJWTOptions{
		TypeHeader: refString("typeHeader"),
		ExpiresAt:  refTime(253402300799),
		NotBefore:  refTime(253402300700),
		IssuedAt:   refTime(253402300000),
	}
	token, err := jwt.NewRawJWT(opts)
	if err != nil {
		t.Fatalf("generating RawJWT with valid timestamps (%q, %q, %q): %v", opts.ExpiresAt, opts.NotBefore, opts.IssuedAt, err)
	}
	expiresAt, err := token.ExpiresAt()
	if err != nil {
		t.Errorf("tc.token.ExpiresAt() err = %v, want nil", err)
	}
	if !cmp.Equal(expiresAt, *opts.ExpiresAt) {
		t.Errorf("tc.token.ExpiresAt() = %q want %q", expiresAt, *opts.ExpiresAt)
	}

	notBefore, err := token.NotBefore()
	if err != nil {
		t.Errorf("tc.token.NotBefore() err = %v, want nil", err)
	}
	if !cmp.Equal(notBefore, *opts.NotBefore) {
		t.Errorf("tc.token.NotBefore() = %q want %q", notBefore, *opts.NotBefore)
	}

	issuedAt, err := token.IssuedAt()
	if err != nil {
		t.Errorf("tc.token.IssuedAt() err = %v, want nil", err)
	}
	if !cmp.Equal(issuedAt, *opts.IssuedAt) {
		t.Errorf("tc.token.IssuedAt() = %q want %q", issuedAt, *opts.IssuedAt)
	}
}

func TestNewRawJWTSingleStringAudience(t *testing.T) {
	opts := &jwt.RawJWTOptions{
		WithoutExpiration: true,
		Audience:          refString("tink-aud"),
	}
	rawJWT, err := jwt.NewRawJWT(opts)
	if err != nil {
		t.Fatalf("generating RawJWT with a single audience: %v", err)
	}
	aud, err := rawJWT.Audiences()
	if err != nil {
		t.Errorf("getting audience from token: %v", err)
	}
	want := []string{*opts.Audience}
	if !cmp.Equal(aud, want) {
		t.Errorf("rawJWT.Audiences() = %q, want %q", aud, want)
	}
}

func TestSingleStringAudienceFromJSON(t *testing.T) {
	rawJWT, err := jwt.NewRawJWTFromJSON(nil, []byte(`{"aud": "tink-aud"}`))
	if err != nil {
		t.Fatalf("parsing valid RawJWT: %v", err)
	}
	aud, err := rawJWT.Audiences()
	if err != nil {
		t.Errorf("getting audience from token: %v", err)
	}
	want := []string{"tink-aud"}
	if !cmp.Equal(aud, want) {
		t.Errorf("rawJWT.Audiences() = %q, want %q", aud, want)
	}
}

func TestNewRawJWTValidationFailures(t *testing.T) {
	testCases := []testCase{
		{
			tag: "empty jwt.RawJWTOptions options fails",
		},
		{
			tag: "no ExpiresAt specified and WithoutExpiration = false fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
			},
		},
		{
			tag: "ExpiresAt and WithoutExpiration = true fails",
			opts: &jwt.RawJWTOptions{
				Audiences:         []string{"tink-foo"},
				ExpiresAt:         refTime(validExpiration),
				WithoutExpiration: true,
			},
		},
		{
			tag: "specifying Audenience and Audiences fails",
			opts: &jwt.RawJWTOptions{
				Audiences:         []string{"tink-foo"},
				Audience:          refString("tink-bar"),
				WithoutExpiration: true,
			},
		},
		{
			tag: "empty audiences array fails",
			opts: &jwt.RawJWTOptions{
				ExpiresAt: refTime(validExpiration),
				Audiences: []string{},
			},
		},
		{
			tag: "audiences with invalid UTF-8 string fails",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audiences:         []string{"valid", invalidUTF8},
			},
		},
		{
			tag: "custom claims containing registered subject claims fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				CustomClaims: map[string]interface{}{
					"sub": "overwrite",
				},
			},
		},
		{
			tag: "custom claims containing registered issuer claims fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				CustomClaims: map[string]interface{}{
					"iss": "overwrite",
				},
			},
		},
		{
			tag: "custom claims containing registered jwt id claims fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				CustomClaims: map[string]interface{}{
					"jti": "overwrite",
				},
			},
		},
		{
			tag: "custom claims containing registered expiration claims fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				CustomClaims: map[string]interface{}{
					"exp": "overwrite",
				},
			},
		},
		{
			tag: "custom claims containing registered audience claims fails",
			opts: &jwt.RawJWTOptions{
				Audiences:         []string{"tink-foo"},
				WithoutExpiration: true,
				CustomClaims: map[string]interface{}{
					"aud": []interface{}{"overwrite"},
				},
			},
		},
		{
			tag: "custom claims with non standard JSON types fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				CustomClaims: map[string]interface{}{
					"complex": time.Time{},
				},
			},
		},
		{
			tag: "non UTF-8 string on isser claim fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				Issuer:    refString(invalidUTF8),
			},
		},
		{
			tag: "non UTF-8 string on subject claim fails",
			opts: &jwt.RawJWTOptions{
				Audiences:         []string{"tink-foo"},
				WithoutExpiration: true,
				Subject:           refString(invalidUTF8),
			},
		},
		{
			tag: "non UTF-8 string on JWT ID claim fails",
			opts: &jwt.RawJWTOptions{
				Audiences:         []string{"tink-foo"},
				WithoutExpiration: true,
				JWTID:             refString(invalidUTF8),
			},
		},
		{
			tag: "non UTF-8 string on custom claim fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				Issuer:    refString("ise-testing"),
				ExpiresAt: refTime(validExpiration),
				CustomClaims: map[string]interface{}{
					"esoteric": invalidUTF8,
				},
			},
		},
		{
			tag: "issued at timestamp greater than valid JWT max time fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				IssuedAt:  refTime(253402300800),
			},
		},
		{
			tag: "expires at timestamp greater than valid JWT max time fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(253402300800),
			},
		},
		{
			tag: "not before timestamp smaller than valid JWT min time fails",
			opts: &jwt.RawJWTOptions{
				Audiences: []string{"tink-foo"},
				ExpiresAt: refTime(validExpiration),
				NotBefore: refTime(-5),
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.tag, func(t *testing.T) {
			_, err := jwt.NewRawJWT(tc.opts)
			if err == nil {
				t.Errorf("expected error instead got nil")
			}
		})
	}
}

func TestJSONPayload(t *testing.T) {
	for _, tc := range []testCase{
		{
			tag: "subject",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Subject:           refString("tink-subject"),
			},
			json: `{"sub":"tink-subject"}`,
		},
		{
			tag: "audience list",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audiences:         []string{"one"},
			},
			json: `{"aud":["one"]}`,
		},
		{
			tag: "audience string",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("one"),
			},
			json: `{"aud":"one"}`,
		},
		{
			tag: "issuer",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Issuer:            refString("tink-test"),
			},
			json: `{"iss":"tink-test"}`,
		},
		{
			tag: "jwt id",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				JWTID:             refString("tink-id"),
			},
			json: `{"jti":"tink-id"}`,
		},
		{
			tag: "issued at",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				IssuedAt:          refTime(78324),
			},
			json: `{"iat":78324}`,
		},
		{
			tag: "not before",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				NotBefore:         refTime(78324),
			},
			json: `{"nbf":78324}`,
		},
		{
			tag: "expiration",
			opts: &jwt.RawJWTOptions{
				ExpiresAt: refTime(78324),
			},
			json: `{"exp":78324}`,
		},
		{
			tag: "integer",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				CustomClaims: map[string]interface{}{
					"num": 1,
				},
			},
			json: `{"num":1}`,
		},
		{
			tag: "custom-claim",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				CustomClaims: map[string]interface{}{
					"cust": []interface{}{map[string]interface{}{"key": "val"}},
				},
			},
			json: `{"cust":[{"key":"val"}]}`,
		},
		{
			tag: "no claims",
			opts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			json: `{}`,
		},
	} {
		t.Run(tc.tag, func(t *testing.T) {
			token, err := jwt.NewRawJWT(tc.opts)
			if err != nil {
				t.Errorf("generating valid RawJWT: %v", err)
			}
			j, err := token.JSONPayload()
			if err != nil {
				t.Errorf("calling JSONPayload() on rawJWT: %v", err)
			}
			if !cmp.Equal(string(j), tc.json) {
				t.Fatalf("JSONPayload output got %v, expected %v", string(j), tc.json)
			}
		})
	}
}

func TestFromJSONValidationFailures(t *testing.T) {
	testCases := []testCase{
		{
			tag:  "json with empty audience",
			json: `{"sub": "tink", "aud": []}`,
		},
		{
			tag:  "json with audience of wrong type",
			json: `{"aud": 5}`,
		},
		{
			tag:  "json with audiences of wrong type",
			json: `{"aud": ["one", null]}`,
		},
		{
			tag:  "json with registered claim with wrong type",
			json: `{"sub": 1}`,
		},
		{
			tag:  "json with non UTF-8 string on subject claim fails",
			json: `{"sub": "\xF4\x7F\xBF\xBF"}`,
		},
		{
			tag:  "json with non UTF-8 string on issuer claim fails",
			json: `{"iss": "\xF4\x7F\xBF\xBF"}`,
		},
		{
			tag:  "json with non UTF-8 string on jwt id claim fails",
			json: `{"jti": "\xF4\x7F\xBF\xBF"}`,
		},
		{
			tag:  "json with `not before` timestamp claim greater than valid JWT max time fails",
			json: `{"nbf": 253402301799}`,
		},
		{
			tag:  "json with `issued at` timestamp claim greater than valid JWT max time fails",
			json: `{"iat": 253402301799}`,
		},
		{
			tag:  "json with `expiration` timestamp claim greater than valid JWT max time fails",
			json: `{"exp": 253402301799}`,
		},
		{
			tag:  "json with `not before` timestamp claim smaller than valid JWT min time fails",
			json: `{"nbf": -4}`,
		},
		{
			tag:  "json with `issued at` timestamp claim smaller than valid JWT min time fails",
			json: `{"iat": -4}`,
		},
		{
			tag:  "json with `expiration` timestamp claim smaller than valid JWT min time fails",
			json: `{"exp": -4}`,
		},
		{
			tag:  "json with `not before` claim of non numeric type fails",
			json: `{"nbf": "invalid"}`,
		},
		{
			tag:  "json with `issued at` claim of non numeric type fails",
			json: `{"iat": "invalid"}`,
		},
		{
			tag:  "json with `expiration` claim of non numeric type fails",
			json: `{"exp": "invalid"}`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.tag, func(t *testing.T) {
			if _, err := jwt.NewRawJWTFromJSON(nil, []byte(tc.json)); err == nil {
				t.Errorf("expected error instead got nil")
			}
		})
	}
}

func TestHasCustomClaimsOfKind(t *testing.T) {
	opts := &jwt.RawJWTOptions{
		TypeHeader:        refString("typeHeader"),
		WithoutExpiration: true,
		CustomClaims: map[string]interface{}{
			"cc-num":    1.67,
			"cc-bool":   false,
			"cc-nil":    nil,
			"cc-list":   []interface{}{1.0, 2.0, 3.0},
			"cc-string": "cc-val",
			"cc-object": map[string]interface{}{
				"nested-cc-num": 5.5,
			},
		},
	}
	token, err := jwt.NewRawJWT(opts)
	if err != nil {
		t.Fatalf("generating valid RawJWT: %v", err)
	}
	if token.HasBooleanClaim("cc-num") {
		t.Errorf("custom number claim 'cc-num' should return false when queried for another type")
	}
	if token.HasNullClaim("cc-bool") {
		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
	}
	if token.HasNumberClaim("cc-bool") {
		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
	}
	if token.HasStringClaim("cc-bool") {
		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
	}
	if token.HasArrayClaim("cc-bool") {
		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
	}
	if token.HasObjectClaim("cc-bool") {
		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
	}
}

func TestGettingRegisteredClaimsThroughCustomFails(t *testing.T) {
	opts := &jwt.RawJWTOptions{
		TypeHeader: refString("typeHeader"),
		Subject:    refString("tink-test-subject"),
		Issuer:     refString("tink-test-issuer"),
		JWTID:      refString("tink-jwt-id-1"),
		Audiences:  []string{"aud-1", "aud-2"},
		ExpiresAt:  refTime(validExpiration),
		IssuedAt:   refTime(validExpiration - 100),
		NotBefore:  refTime(validExpiration - 50),
	}
	token, err := jwt.NewRawJWT(opts)
	if err != nil {
		t.Fatalf("generating valid RawJWT: %v", err)
	}
	if !cmp.Equal(token.CustomClaimNames(), []string{}) {
		t.Errorf("tc.token.CustomClaimNames() = %q want %q", token.CustomClaimNames(), []string{})
	}
	for _, c := range []string{"sub", "iss", "aud", "nbf", "exp", "iat", "jti"} {
		if token.HasNullClaim(c) {
			t.Errorf("registered '%q' claim should return false when calling HasNullClaim", c)
		}
		if token.HasBooleanClaim(c) {
			t.Errorf("registered '%q' claim should return false when calling HasBooleanClaim", c)
		}
		if _, err := token.BooleanClaim(c); err == nil {
			t.Errorf("expected error when calling token.BoolClaim(%q) instead got nil", c)
		}
		if token.HasNumberClaim(c) {
			t.Errorf("registered '%q' claim should return false when calling HasNumberClaim", c)
		}
		if _, err := token.NumberClaim(c); err == nil {
			t.Errorf("expected error when calling token.NumberClaim(%q) instead got nil", c)
		}
		if token.HasStringClaim(c) {
			t.Errorf("registered '%q' claim should return false when calling HasStringClaim", c)
		}
		if _, err := token.StringClaim(c); err == nil {
			t.Errorf("expected error when calling token.StringClaim(%q) instead got nil", c)
		}
		if token.HasArrayClaim(c) {
			t.Errorf("registered '%q' claim should return false when calling HasArrayClaim", c)
		}
		if _, err := token.ArrayClaim(c); err == nil {
			t.Errorf("expected error when calling token.ListClaim(%q) instead got nil", c)
		}
		if token.HasObjectClaim(c) {
			t.Errorf("registered '%q' claim should return false when calling HasObjectClaim", c)
		}
		if _, err := token.ObjectClaim(c); err == nil {
			t.Errorf("expected error when calling token.JSONClaim(%q) instead got nil", c)
		}
	}
}
