// Copyright 2022 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/tink-crypto/tink-go/v2/jwt"
)

type validationTestCase struct {
	tag           string
	tokenOpts     *jwt.RawJWTOptions
	validatorOpts *jwt.ValidatorOpts
}

func TestNewValidatorFailure(t *testing.T) {
	for _, tc := range []validationTestCase{
		{
			tag: "combining ExpectedTypeHeader and IgnoreTypeHeader",
			validatorOpts: &jwt.ValidatorOpts{
				ExpectedTypeHeader: refString("should fail"),
				IgnoreTypeHeader:   true,
			},
		},
		{
			tag: "combining ExpectedIssuer and IgnoreIssuer",
			validatorOpts: &jwt.ValidatorOpts{
				ExpectedIssuer: refString("should fail"),
				IgnoreIssuer:   true,
			},
		},
		{
			tag: "combining ExpectedAudience and IgnoreAudiences",
			validatorOpts: &jwt.ValidatorOpts{
				ExpectedAudience: refString("should fail"),
				IgnoreAudiences:  true,
			},
		},
		{
			tag: "combining ExpectedAudiences and IgnoreAudiences",
			validatorOpts: &jwt.ValidatorOpts{
				ExpectedAudiences: refString("should fail"),
				IgnoreAudiences:   true,
			},
		},
		{
			tag: "both ExpectedAudience and ExpectedAudiences are set",
			validatorOpts: &jwt.ValidatorOpts{
				ExpectedAudience:  refString("aud"),
				ExpectedAudiences: refString("aud"),
			},
		},
		{
			tag: "invalid clock skew",
			validatorOpts: &jwt.ValidatorOpts{
				ClockSkew: time.Minute * 11,
			},
		},
		{
			tag: "validator opts can't be nil",
		},
	} {
		t.Run(tc.tag, func(t *testing.T) {
			if _, err := jwt.NewValidator(tc.validatorOpts); err == nil {
				t.Errorf("NewValidator(%v) err = nil, want error", tc.validatorOpts)
			}
		})
	}
}

func TestValidationFailures(t *testing.T) {
	for _, tc := range []validationTestCase{
		{
			tag: "expired token",
			tokenOpts: &jwt.RawJWTOptions{
				ExpiresAt: refTime(100),
			},
			validatorOpts: &jwt.ValidatorOpts{
				FixedNow: time.Unix(500, 0),
			},
		},
		{
			tag: "no expiration and AllowMissingExpiration = false",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{},
		},
		{
			tag: "token expiry equals current time",
			tokenOpts: &jwt.RawJWTOptions{
				ExpiresAt: refTime(123),
			},
			validatorOpts: &jwt.ValidatorOpts{
				FixedNow: time.Unix(123, 0),
			},
		},
		{
			tag: "not before in the future",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				NotBefore:         refTime(1500),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(1000, 0),
			},
		},
		{
			tag: "issued in the future with ExpectIssuedInThePast = true",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				IssuedAt:          refTime(5000),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(1000, 0),
				ExpectIssuedInThePast:  true,
			},
		},
		{
			tag: "without issued at with ExpectIssuedInThePast = true",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectIssuedInThePast:  true,
			},
		},
		{
			tag: "no type header and RequiresTypeHeader = true",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedTypeHeader:     refString("typeHeader"),
			},
		},
		{
			tag: "invalid type header",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				TypeHeader:        refString("typeHeader"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedTypeHeader:     refString("different"),
			},
		},
		{
			tag: "type header in token but no type header in validator",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				TypeHeader:        refString("typeHeader"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
			},
		},
		{
			tag: "issuer required but not specified",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedIssuer:         refString("tink-issuer"),
			},
		},
		{
			tag: "invalid issuer",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Issuer:            refString("tink-issuer"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedIssuer:         refString("different"),
			},
		},
		{
			tag: "issuer in token but not in validator",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Issuer:            refString("issuer"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
			},
		},
		{
			tag: "audience required but no specified",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedAudience:       refString("tink-audience"),
			},
		},
		{
			tag: "audience required but no specified, deprecated",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedAudiences:      refString("tink-audience"),
			},
		},
		{
			tag: "invalid audience",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("tink-audience"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedAudience:       refString("audience"),
			},
		},
		{
			tag: "invalid audience, deprecated",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("tink-audience"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedAudiences:      refString("audience"),
			},
		},
		{
			tag: "audience in token but not in validator",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("audience"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
			},
		},
	} {

		t.Run(tc.tag, func(t *testing.T) {
			token, err := jwt.NewRawJWT(tc.tokenOpts)
			if err != nil {
				t.Fatalf("jwt.NewRawJWT(%v) err = %v, want nil", tc.tokenOpts, err)
			}
			validator, err := jwt.NewValidator(tc.validatorOpts)
			if err != nil {
				t.Fatalf("jwt.NewValidator(%v) err = %v, want nil", tc.validatorOpts, err)
			}
			if err := validator.Validate(token); err == nil {
				t.Errorf("validator.Validate(%v) err = nil, want error", token)
			}
		})
	}
}

func TestExpiredTokenValidationReturnsExpiredErr(t *testing.T) {
	tokenOpts := &jwt.RawJWTOptions{
		ExpiresAt: refTime(100),
	}
	expiredToken, err := jwt.NewRawJWT(tokenOpts)
	if err != nil {
		t.Fatalf("jwt.NewRawJWT(tokenOpts) err = %v, want nil", err)
	}
	validatorOpts := &jwt.ValidatorOpts{
		FixedNow: time.Unix(500, 0),
	}
	validator, err := jwt.NewValidator(validatorOpts)
	if err != nil {
		t.Fatalf("jwt.NewValidator(validatorOpts) err = %v, want nil", err)
	}

	validationErr := validator.Validate(expiredToken)
	if validationErr == nil {
		t.Errorf("validator.Validate(expiredToken) err = nil, want error")
	}
	if !jwt.IsExpirationErr(validationErr) {
		t.Errorf("jwt.IsExpirationErr(validationErr) = false, want true")
	}
}

func TestExpirationGetsValidatedFirst(t *testing.T) {
	tokenOpts := &jwt.RawJWTOptions{
		ExpiresAt: refTime(100),
		Audience:  refString("invalidAudience"),
	}
	expiredTokenWithInvalidAudience, err := jwt.NewRawJWT(tokenOpts)
	if err != nil {
		t.Fatalf("jwt.NewRawJWT(tokenOpts) err = %v, want nil", err)
	}
	validatorOpts := &jwt.ValidatorOpts{
		ExpectedAudiences: refString("audience"),
		FixedNow:          time.Unix(500, 0),
	}
	validator, err := jwt.NewValidator(validatorOpts)
	if err != nil {
		t.Fatalf("jwt.NewValidator(validatorOpts) err = %v, want nil", err)
	}

	validationErr := validator.Validate(expiredTokenWithInvalidAudience)
	if validationErr == nil {
		t.Errorf("validator.Validate(expiredTokenWithInvalidAudience) err = nil, want error")
	}
	if !jwt.IsExpirationErr(validationErr) {
		t.Errorf("jwt.IsExpirationErr(validationErr) = false, want true")
	}
}

func TestValidationSuccess(t *testing.T) {
	for _, tc := range []validationTestCase{
		{
			tag: "unexpired token",
			tokenOpts: &jwt.RawJWTOptions{
				ExpiresAt: refTime(1000),
			},
			validatorOpts: &jwt.ValidatorOpts{
				FixedNow: time.Unix(100, 0),
			},
		},
		{
			tag: "expired with clock slew",
			tokenOpts: &jwt.RawJWTOptions{
				ExpiresAt: refTime(400),
			},
			validatorOpts: &jwt.ValidatorOpts{
				FixedNow:  time.Unix(500, 0),
				ClockSkew: time.Second * 200,
			},
		},
		{
			tag: "not before in the past",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				NotBefore:         refTime(500),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(1000, 0),
			},
		},
		{
			tag: "not before equals now",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				NotBefore:         refTime(500),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(500, 0),
			},
		},
		{
			tag: "not before in near future with clock skew",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				NotBefore:         refTime(600),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(500, 0),
				ClockSkew:              time.Second * 200,
			},
		},
		{
			tag: "issued in the past",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				IssuedAt:          refTime(500),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(1000, 0),
			},
		},
		{
			tag: "issued in the future",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				IssuedAt:          refTime(5000),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				FixedNow:               time.Unix(1000, 0),
			},
		},
		{
			tag: "without issued at",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
			},
		},
		{
			tag: "issued in the past with ExpectIssuedInThePast",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				IssuedAt:          refTime(500),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectIssuedInThePast:  true,
				FixedNow:               time.Unix(1000, 0),
			},
		},
		{
			tag: "issued in the past with ExpectIssuedInThePast and clock skew",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				IssuedAt:          refTime(1100),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectIssuedInThePast:  true,
				FixedNow:               time.Unix(1000, 0),
				ClockSkew:              time.Second * 200,
			},
		},
		{

			tag: "expected type header",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				TypeHeader:        refString("typeHeader"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedTypeHeader:     refString("typeHeader"),
			},
		},
		{
			tag: "ignore type header",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				TypeHeader:        refString("typeHeader"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				IgnoreTypeHeader:       true,
			},
		},
		{
			tag: "expected issuer",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Issuer:            refString("issuer"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedIssuer:         refString("issuer"),
			},
		},
		{
			tag: "ignore issuer",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Issuer:            refString("issuer"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				IgnoreIssuer:           true,
			},
		},
		{
			tag: "expected audience",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("audience"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedAudience:       refString("audience"),
			},
		},
		{
			tag: "deprecated expected audiences",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("audience"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				ExpectedAudiences:      refString("audience"),
			},
		},
		{
			tag: "ignore audience",
			tokenOpts: &jwt.RawJWTOptions{
				WithoutExpiration: true,
				Audience:          refString("audience"),
			},
			validatorOpts: &jwt.ValidatorOpts{
				AllowMissingExpiration: true,
				IgnoreAudiences:        true,
			},
		},
	} {
		t.Run(tc.tag, func(t *testing.T) {
			token, err := jwt.NewRawJWT(tc.tokenOpts)
			if err != nil {
				t.Fatalf("NewRawJWT(%v) err = %v, want nil", tc.tokenOpts, err)
			}
			validator, err := jwt.NewValidator(tc.validatorOpts)
			if err != nil {
				t.Fatalf("NewValidator(%v) err = %v, want nil", tc.validatorOpts, err)
			}
			if err := validator.Validate(token); err != nil {
				t.Errorf("validator.Validate(%v) err = %v, want nil", token, err)
			}
		})
	}
}
