package openid_test

import (
	"context"
	"fmt"
	"strconv"
	"testing"
	"time"

	"github.com/lestrrat-go/jwx/internal/json"
	"github.com/lestrrat-go/jwx/internal/jwxtest"

	"github.com/lestrrat-go/jwx/jwa"
	"github.com/lestrrat-go/jwx/jwt"
	"github.com/lestrrat-go/jwx/jwt/internal/types"
	"github.com/lestrrat-go/jwx/jwt/openid"
	"github.com/stretchr/testify/assert"
)

const aLongLongTimeAgo = 233431200
const aLongLongTimeAgoString = "233431200"
const (
	tokenTime = 233431200
)

var expectedTokenTime = time.Unix(tokenTime, 0).UTC()

func testStockAddressClaim(t *testing.T, x *openid.AddressClaim) {
	t.Helper()
	if !assert.NotNil(t, x) {
		return
	}

	tests := []struct {
		Accessor func() string
		KeyName  string
		Value    string
	}{
		{
			Accessor: x.Formatted,
			KeyName:  openid.AddressFormattedKey,
			Value:    "〒105-0011 東京都港区芝公園４丁目２−８",
		},
		{
			Accessor: x.Country,
			KeyName:  openid.AddressCountryKey,
			Value:    "日本",
		},
		{
			Accessor: x.Region,
			KeyName:  openid.AddressRegionKey,
			Value:    "東京都",
		},
		{
			Accessor: x.Locality,
			KeyName:  openid.AddressLocalityKey,
			Value:    "港区",
		},
		{
			Accessor: x.StreetAddress,
			KeyName:  openid.AddressStreetAddressKey,
			Value:    "芝公園４丁目２−８",
		},
		{
			Accessor: x.PostalCode,
			KeyName:  openid.AddressPostalCodeKey,
			Value:    "105-0011",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.KeyName, func(t *testing.T) {
			t.Run("Accessor", func(t *testing.T) {
				if !assert.Equal(t, tc.Value, tc.Accessor(), "values should match") {
					return
				}
			})
			t.Run("Get", func(t *testing.T) {
				v, ok := x.Get(tc.KeyName)
				if !assert.True(t, ok, `x.Get should succeed`) {
					return
				}
				if !assert.Equal(t, tc.Value, v, `values should match`) {
					return
				}
			})
		})
	}
}

func TestAdressClaim(t *testing.T) {
	const src = `{
    "formatted": "〒105-0011 東京都港区芝公園４丁目２−８",
		"street_address": "芝公園４丁目２−８",
		"locality": "港区",
		"region": "東京都",
		"postal_code": "105-0011",
		"country": "日本"
	}`

	var address openid.AddressClaim
	if !assert.NoError(t, json.Unmarshal([]byte(src), &address), "json.Unmarshal should succeed") {
		return
	}

	var roundtrip openid.AddressClaim
	buf, err := json.Marshal(address)
	if !assert.NoError(t, err, `json.Marshal(address) should succeed`) {
		return
	}

	if !assert.NoError(t, json.Unmarshal(buf, &roundtrip), "json.Unmarshal should succeed") {
		return
	}

	for _, x := range []*openid.AddressClaim{&address, &roundtrip} {
		testStockAddressClaim(t, x)
	}
}

func TestOpenIDClaims(t *testing.T) {
	getVerify := func(token openid.Token, key string, expected interface{}) bool {
		v, ok := token.Get(key)
		if !assert.True(t, ok, `token.Get %#v should succeed`, key) {
			return false
		}
		return assert.Equal(t, v, expected)
	}

	var base = []struct {
		Value    interface{}
		Expected func(interface{}) interface{}
		Check    func(openid.Token)
		Key      string
	}{
		{
			Key:   openid.AudienceKey,
			Value: []string{"developers", "secops", "tac"},
			Check: func(token openid.Token) {
				assert.Equal(t, token.Audience(), []string{"developers", "secops", "tac"})
			},
		},
		{
			Key:   openid.ExpirationKey,
			Value: tokenTime,
			Expected: func(v interface{}) interface{} {
				var n types.NumericDate
				if err := n.Accept(v); err != nil {
					panic(err)
				}
				return n.Get()
			},
			Check: func(token openid.Token) {
				assert.Equal(t, token.Expiration(), expectedTokenTime)
			},
		},
		{
			Key:   openid.IssuedAtKey,
			Value: tokenTime,
			Expected: func(v interface{}) interface{} {
				var n types.NumericDate
				if err := n.Accept(v); err != nil {
					panic(err)
				}
				return n.Get()
			},
			Check: func(token openid.Token) {
				assert.Equal(t, token.Expiration(), expectedTokenTime)
			},
		},
		{
			Key:   openid.IssuerKey,
			Value: "http://www.example.com",
			Check: func(token openid.Token) {
				assert.Equal(t, token.Issuer(), "http://www.example.com")
			},
		},
		{
			Key:   openid.JwtIDKey,
			Value: "e9bc097a-ce51-4036-9562-d2ade882db0d",
			Check: func(token openid.Token) {
				assert.Equal(t, token.JwtID(), "e9bc097a-ce51-4036-9562-d2ade882db0d")
			},
		},
		{
			Key:   openid.NotBeforeKey,
			Value: tokenTime,
			Expected: func(v interface{}) interface{} {
				var n types.NumericDate
				if err := n.Accept(v); err != nil {
					panic(err)
				}
				return n.Get()
			},
			Check: func(token openid.Token) {
				assert.Equal(t, token.NotBefore(), expectedTokenTime)
			},
		},
		{
			Key:   openid.SubjectKey,
			Value: "unit test",
			Check: func(token openid.Token) {
				assert.Equal(t, token.Subject(), "unit test")
			},
		},
		{
			Value: "jwx",
			Key:   openid.NameKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Name(), "jwx")
			},
		},
		{
			Value: "jay",
			Key:   openid.GivenNameKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.GivenName(), "jay")
			},
		},
		{
			Value: "weee",
			Key:   openid.MiddleNameKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.MiddleName(), "weee")
			},
		},
		{
			Value: "xi",
			Key:   openid.FamilyNameKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.FamilyName(), "xi")
			},
		},
		{
			Value: "jayweexi",
			Key:   openid.NicknameKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Nickname(), "jayweexi")
			},
		},
		{
			Value: "jwx",
			Key:   openid.PreferredUsernameKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.PreferredUsername(), "jwx")
			},
		},
		{
			Value: "https://github.com/lestrrat-go/jwx",
			Key:   openid.ProfileKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Profile(), "https://github.com/lestrrat-go/jwx")
			},
		},
		{
			Value: "https://avatars1.githubusercontent.com/u/36653903?s=400&amp;v=4",
			Key:   openid.PictureKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Picture(), "https://avatars1.githubusercontent.com/u/36653903?s=400&amp;v=4")
			},
		},
		{
			Value: "https://github.com/lestrrat-go/jwx",
			Key:   openid.WebsiteKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Website(), "https://github.com/lestrrat-go/jwx")
			},
		},
		{
			Value: "lestrrat+github@gmail.com",
			Key:   openid.EmailKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Email(), "lestrrat+github@gmail.com")
			},
		},
		{
			Value: true,
			Key:   openid.EmailVerifiedKey,
			Check: func(token openid.Token) {
				assert.True(t, token.EmailVerified())
			},
		},
		{
			Value: "n/a",
			Key:   openid.GenderKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Gender(), "n/a")
			},
		},
		{
			Value: "2015-11-04",
			Key:   openid.BirthdateKey,
			Expected: func(v interface{}) interface{} {
				var b openid.BirthdateClaim
				if err := b.Accept(v); err != nil {
					panic(err)
				}
				return &b
			},
			Check: func(token openid.Token) {
				var b openid.BirthdateClaim
				b.Accept("2015-11-04")
				assert.Equal(t, token.Birthdate(), &b)
			},
		},
		{
			Value: "Asia/Tokyo",
			Key:   openid.ZoneinfoKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Zoneinfo(), "Asia/Tokyo")
			},
		},
		{
			Value: "ja_JP",
			Key:   openid.LocaleKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.Locale(), "ja_JP")
			},
		},
		{
			Value: "819012345678",
			Key:   openid.PhoneNumberKey,
			Check: func(token openid.Token) {
				assert.Equal(t, token.PhoneNumber(), "819012345678")
			},
		},
		{
			Value: true,
			Key:   openid.PhoneNumberVerifiedKey,
			Check: func(token openid.Token) {
				assert.True(t, token.PhoneNumberVerified())
			},
		},
		{
			Value: map[string]interface{}{
				"formatted":      "〒105-0011 東京都港区芝公園４丁目２−８",
				"street_address": "芝公園４丁目２−８",
				"locality":       "港区",
				"region":         "東京都",
				"country":        "日本",
				"postal_code":    "105-0011",
			},
			Key: openid.AddressKey,
			Expected: func(v interface{}) interface{} {
				address := openid.NewAddress()
				m, ok := v.(map[string]interface{})
				if !ok {
					panic(fmt.Sprintf("expected map[string]interface{}, got %T", v))
				}
				for name, val := range m {
					if !assert.NoError(t, address.Set(name, val), `address.Set should succeed`) {
						return nil
					}
				}
				return address
			},
			Check: func(token openid.Token) {
				testStockAddressClaim(t, token.Address())
			},
		},
		{
			Value: aLongLongTimeAgoString,
			Key:   openid.UpdatedAtKey,
			Expected: func(v interface{}) interface{} {
				var n types.NumericDate
				if err := n.Accept(v); err != nil {
					panic(err)
				}
				return n.Get()
			},
			Check: func(token openid.Token) {
				assert.Equal(t, time.Unix(aLongLongTimeAgo, 0).UTC(), token.UpdatedAt())
			},
		},
		{
			Value: `dummy`,
			Key:   `dummy`,
			Check: func(token openid.Token) {
				v, ok := token.Get(`dummy`)
				if !assert.True(t, ok, `token.Get should return valid value`) {
					return
				}
				if !assert.Equal(t, `dummy`, v, `values should match`) {
					return
				}
			},
		},
	}

	var data = map[string]interface{}{}
	var expected = map[string]interface{}{}
	for _, value := range base {
		data[value.Key] = value.Value
		if expf := value.Expected; expf != nil {
			expected[value.Key] = expf(value.Value)
		} else {
			expected[value.Key] = value.Value
		}
	}

	type openidTokTestCase struct {
		Token openid.Token
		Name  string
	}
	var tokens []openidTokTestCase

	{ // one with Set()
		token := openid.New()
		for name, value := range data {
			if !assert.NoError(t, token.Set(name, value), `token.Set should succeed`) {
				return
			}
		}
		tokens = append(tokens, openidTokTestCase{Name: `token constructed by calling Set()`, Token: token})
	}

	{ // two with json.Marshal / json.Unmarshal
		src, err := json.MarshalIndent(data, "", "  ")
		if !assert.NoError(t, err, `failed to marshal base map`) {
			return
		}

		t.Logf("Using source JSON: %s", src)

		token := openid.New()
		if !assert.NoError(t, json.Unmarshal(src, &token), `json.Unmarshal should succeed`) {
			return
		}
		tokens = append(tokens, openidTokTestCase{Name: `token constructed by Marshal(map)+Unmashal`, Token: token})

		// One more... Marshal the token, _and_ re-unmarshal
		buf, err := json.Marshal(token)
		if !assert.NoError(t, err, `json.Marshal should succeed`) {
			return
		}

		token2 := openid.New()
		if !assert.NoError(t, json.Unmarshal(buf, &token2), `json.Unmarshal should succeed`) {
			return
		}
		tokens = append(tokens, openidTokTestCase{Name: `token constructed by Marshal(openid.Token)+Unmashal`, Token: token2})

		// Sign it, and use jwt.Parse

		var token3 openid.Token
		{
			alg := jwa.RS256
			key, err := jwxtest.GenerateRsaKey()
			if !assert.NoError(t, err, `rsa.GeneraKey should succeed`) {
				return
			}
			signed, err := jwt.Sign(token, alg, key)
			if !assert.NoError(t, err, `jwt.Sign should succeed`) {
				return
			}

			tokenTmp, err := jwt.Parse(signed, jwt.WithToken(openid.New()), jwt.WithVerify(alg, &key.PublicKey))
			if !assert.NoError(t, err, `parsing the token via jwt.Parse should succeed`) {
				return
			}

			// Check if token is an OpenID token
			if _, ok := tokenTmp.(openid.Token); !assert.True(t, ok, `token should be a openid.Token (%T)`, tokenTmp) {
				return
			}
			token3 = tokenTmp.(openid.Token)
		}

		tokens = append(tokens, openidTokTestCase{Name: `token constructed by jwt.Parse`, Token: token3})
	}

	for _, token := range tokens {
		token := token
		t.Run(token.Name, func(t *testing.T) {
			for _, value := range base {
				value := value
				t.Run(value.Key, func(_ *testing.T) {
					value.Check(token.Token)
				})
				t.Run(value.Key+" via Get()", func(_ *testing.T) {
					expected := value.Value
					if expf := value.Expected; expf != nil {
						expected = expf(value.Value)
					}
					getVerify(token.Token, value.Key, expected)
				})
			}
		})
	}

	t.Run("Iterator", func(t *testing.T) {
		v := tokens[0].Token
		t.Run("Iterate", func(t *testing.T) {
			seen := make(map[string]interface{})
			for iter := v.Iterate(context.TODO()); iter.Next(context.TODO()); {
				pair := iter.Pair()
				seen[pair.Key.(string)] = pair.Value

				getV, ok := v.Get(pair.Key.(string))
				if !assert.True(t, ok, `v.Get should succeed for key %#v`, pair.Key) {
					return
				}
				if !assert.Equal(t, pair.Value, getV, `pair.Value should match value from v.Get()`) {
					return
				}
			}
			if !assert.Equal(t, expected, seen, `values should match`) {
				return
			}
		})
		t.Run("Walk", func(t *testing.T) {
			seen := make(map[string]interface{})
			v.Walk(context.TODO(), openid.VisitorFunc(func(key string, value interface{}) error {
				seen[key] = value
				return nil
			}))
			if !assert.Equal(t, expected, seen, `values should match`) {
				return
			}
		})
		t.Run("AsMap", func(t *testing.T) {
			seen, err := v.AsMap(context.TODO())
			if !assert.NoError(t, err, `v.AsMap should succeed`) {
				return
			}
			if !assert.Equal(t, expected, seen, `values should match`) {
				return
			}
		})
		t.Run("Clone", func(t *testing.T) {
			cloned, err := v.Clone()
			if !assert.NoError(t, err, `v.Clone should succeed`) {
				return
			}

			if !assert.True(t, jwt.Equal(v, cloned), `values should match`) {
				return
			}
		})
	})
}

func TestBirthdateClaim(t *testing.T) {
	t.Parallel()
	t.Run("regular date", func(t *testing.T) {
		t.Parallel()
		testcases := []struct {
			Source string
			Year   int
			Month  int
			Day    int
			Error  bool
		}{
			{
				Source: `"2015-11-04"`,
				Year:   2015,
				Month:  11,
				Day:    4,
			},
			{
				Source: `"0009-09-09"`,
				Year:   9,
				Month:  9,
				Day:    9,
			},
			{
				Source: `{}`,
				Error:  true,
			},
			{
				Source: `"202X-01-01"`,
				Error:  true,
			},
			{
				Source: `"0000-01-01"`,
				Error:  true,
			},
			{
				Source: `"0001-00-01"`,
				Error:  true,
			},
			{
				Source: `"0001-01-00"`,
				Error:  true,
			},
		}

		for _, tc := range testcases {
			tc := tc
			t.Run(tc.Source, func(t *testing.T) {
				var b openid.BirthdateClaim
				if tc.Error {
					assert.Error(t, json.Unmarshal([]byte(tc.Source), &b), `json.Unmarshal should fail`)
					return
				}

				if !assert.NoError(t, json.Unmarshal([]byte(tc.Source), &b), `json.Unmarshal should succeed`) {
					return
				}

				if !assert.Equal(t, b.Year(), tc.Year, "year should match") {
					return
				}
				if !assert.Equal(t, b.Month(), tc.Month, "month should match") {
					return
				}
				if !assert.Equal(t, b.Day(), tc.Day, "day should match") {
					return
				}
				serialized, err := json.Marshal(b)
				if !assert.NoError(t, err, `json.Marshal should succeed`) {
					return
				}
				if !assert.Equal(t, string(serialized), tc.Source, `serialized format should be the same`) {
					return
				}
				stringified := b.String()
				expectedString, _ := strconv.Unquote(tc.Source)
				if !assert.Equal(t, stringified, expectedString, `stringified format should be the same`) {
					return
				}
			})
		}
	})
	t.Run("empty date", func(t *testing.T) {
		t.Parallel()
		var b openid.BirthdateClaim
		if !assert.Equal(t, b.Year(), 0, "year should match") {
			return
		}
		if !assert.Equal(t, b.Month(), 0, "month should match") {
			return
		}
		if !assert.Equal(t, b.Day(), 0, "day should match") {
			return
		}
	})
	t.Run("invalid accept", func(t *testing.T) {
		t.Parallel()
		var b openid.BirthdateClaim
		if !assert.Error(t, b.Accept(nil)) {
			return
		}
	})
}

func TestKeys(t *testing.T) {
	at := assert.New(t)
	at.Equal(`address`, openid.AddressKey)
	at.Equal(`aud`, openid.AudienceKey)
	at.Equal(`birthdate`, openid.BirthdateKey)
	at.Equal(`email`, openid.EmailKey)
	at.Equal(`email_verified`, openid.EmailVerifiedKey)
	at.Equal(`exp`, openid.ExpirationKey)
	at.Equal(`family_name`, openid.FamilyNameKey)
	at.Equal(`gender`, openid.GenderKey)
	at.Equal(`given_name`, openid.GivenNameKey)
	at.Equal(`iat`, openid.IssuedAtKey)
	at.Equal(`iss`, openid.IssuerKey)
	at.Equal(`jti`, openid.JwtIDKey)
	at.Equal(`locale`, openid.LocaleKey)
	at.Equal(`middle_name`, openid.MiddleNameKey)
	at.Equal(`name`, openid.NameKey)
	at.Equal(`nickname`, openid.NicknameKey)
	at.Equal(`nbf`, openid.NotBeforeKey)
	at.Equal(`phone_number`, openid.PhoneNumberKey)
	at.Equal(`phone_number_verified`, openid.PhoneNumberVerifiedKey)
	at.Equal(`picture`, openid.PictureKey)
	at.Equal(`preferred_username`, openid.PreferredUsernameKey)
	at.Equal(`profile`, openid.ProfileKey)
	at.Equal(`sub`, openid.SubjectKey)
	at.Equal(`updated_at`, openid.UpdatedAtKey)
	at.Equal(`website`, openid.WebsiteKey)
	at.Equal(`zoneinfo`, openid.ZoneinfoKey)
}
