package providers

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/stretchr/testify/assert"

	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
)

func newCidaasProvider(serverURL *url.URL) *CIDAASProvider {
	providerData := &ProviderData{
		ProviderName: "cidaas",
		ClientID:     oidcClientID,
		ClientSecret: oidcSecret,
		LoginURL: &url.URL{
			Scheme: serverURL.Scheme,
			Host:   serverURL.Host,
			Path:   "/login/oauth/authorize"},
		RedeemURL: &url.URL{
			Scheme: serverURL.Scheme,
			Host:   serverURL.Host,
			Path:   "/login/oauth/access_token"},
		ProfileURL: &url.URL{
			Scheme: serverURL.Scheme,
			Host:   serverURL.Host,
			Path:   "/profile"},
		ValidateURL: &url.URL{
			Scheme: serverURL.Scheme,
			Host:   serverURL.Host,
			Path:   "/api"},
		Scope:       "openid profile offline_access roles groups",
		EmailClaim:  "email",
		GroupsClaim: "groups",
		Verifier: oidc.NewVerifier(
			oidcIssuer,
			mockJWKS{},
			&oidc.Config{ClientID: oidcClientID},
		),
	}
	cfg := options.Provider{
		Type: options.CidaasProvider,
	}

	p := NewCIDAASProvider(providerData, cfg)

	return p
}

func newCidaasServer(pathBodyMap map[string][]byte) (*url.URL, *httptest.Server) {
	s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		body, ok := pathBodyMap[r.URL.Path]
		if !ok {
			rw.WriteHeader(404)
			return
		}
		rw.Header().Add("content-type", "application/json")
		_, _ = rw.Write(body)
	}))
	u, _ := url.Parse(s.URL)
	return u, s
}

func newTestCidaasSetup(pathToBodyMap map[string][]byte) (*httptest.Server, *CIDAASProvider) {
	redeemURL, server := newCidaasServer(pathToBodyMap)
	provider := newCidaasProvider(redeemURL)
	return server, provider
}

func TestCidaasProvider_EnrichSession(t *testing.T) {
	testCases := map[string]struct {
		ExistingSession *sessions.SessionState
		EmailClaim      string
		GroupsClaim     string
		ProfileJSON     map[string]interface{}
		ExpectedError   error
		ExpectedSession *sessions.SessionState
	}{
		"Missing Email Only in Profile URL": {
			ExistingSession: &sessions.SessionState{
				User:         "missing.email",
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"email": "found@email.com",
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "missing.email",
				Email:        "found@email.com",
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Missing Email with Custom Claim": {
			ExistingSession: &sessions.SessionState{
				User:         "missing.email",
				Groups:       []string{"already", "populated"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "weird",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"weird": "weird@claim.com",
				"groups": []map[string]interface{}{
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "missing.email",
				Email:        "weird@claim.com",
				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Missing Email not in Profile URL": {
			ExistingSession: &sessions.SessionState{
				User:         "missing.email",
				Groups:       []string{"already", "populated"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"groups": []map[string]interface{}{
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: errors.New("neither the id_token nor the profileURL set an email"),
			ExpectedSession: &sessions.SessionState{
				User:         "missing.email",
				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Missing Groups": {
			ExistingSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       nil,
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"email": "new@thing.com",
				"groups": []map[string]interface{}{
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Empty Groups Claims": {
			ExistingSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       []string{},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"email": "new@thing.com",
				"groups": []map[string]interface{}{
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       []string{"CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Missing Groups with Custom Claim": {
			ExistingSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       nil,
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups2",
			ProfileJSON: map[string]interface{}{
				"email": "already@populated.com",
				"groups2": []map[string]interface{}{
					{
						"sub":     "aa5181ea-0841-4ea7-b67f-81882f153d40",
						"groupId": "CIDAAS_ADMINS",
						"path":    "/CIDAAS_ADMINS/",
						"roles":   []string{"ADMIN"},
					},
					{
						"sub":       "aa5181ea-0841-4ea7-b67f-81882f153d39",
						"groupId":   "customers",
						"groupType": "Customers",
						"path":      "/customers/",
						"roles": []string{
							"CUSTOMER_ACCOUNT_LOGIN",
							"GROUP_ADMIN",
						},
					},
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       []string{"CIDAAS_ADMINS:ADMIN", "customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Just format Groups": {
			ExistingSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       nil,
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups2",
			ProfileJSON: map[string]interface{}{
				"email": "already@populated.com",
				"groups2": []map[string]interface{}{
					{
						"sub":       "aa5181ea-0841-4ea7-b67f-81882f153d39",
						"groupId":   "customers",
						"groupType": "Customers",
						"path":      "/customers/",
						"roles": []string{
							"CUSTOMER_ACCOUNT_LOGIN",
							"GROUP_ADMIN",
						},
					},
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       []string{"customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Missing Groups String Profile URL Response": {
			ExistingSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       nil,
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"groups": []map[string]interface{}{
					{
						"sub":     "aa5181ea-0841-4ea7-b67f-81882f153d40",
						"groupId": "CIDAAS_ADMINS",
						"path":    "/CIDAAS_ADMINS/",
						"roles":   []string{"ADMIN"},
					},
					{
						"sub":       "aa5181ea-0841-4ea7-b67f-81882f153d39",
						"groupId":   "customers",
						"groupType": "Customers",
						"path":      "/customers/",
						"roles": []string{
							"CUSTOMER_ACCOUNT_LOGIN",
							"GROUP_ADMIN",
						},
					},
					{
						"groupId": "CIDAAS_USERS",
						"roles":   []string{"USER"},
					},
				},
				"roles": []string{"USER"},
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				Groups:       []string{"CIDAAS_ADMINS:ADMIN", "customers:CUSTOMER_ACCOUNT_LOGIN", "customers:GROUP_ADMIN", "CIDAAS_USERS:USER", "cidaas:USER"},
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
		"Missing Groups in both Claims and Profile URL": {
			ExistingSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			EmailClaim:  "email",
			GroupsClaim: "groups",
			ProfileJSON: map[string]interface{}{
				"email": "new@thing.com",
			},
			ExpectedError: nil,
			ExpectedSession: &sessions.SessionState{
				User:         "already",
				Email:        "already@populated.com",
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
		},
	}
	for testName, tc := range testCases {
		t.Run(testName, func(t *testing.T) {
			path := "/userinfo/"
			jsonResp, err := json.Marshal(tc.ProfileJSON)
			assert.NoError(t, err)

			server, provider := newTestCidaasSetup(map[string][]byte{path: jsonResp})
			provider.ProfileURL, err = url.Parse(fmt.Sprintf("%s%s", server.URL, path))
			assert.NoError(t, err)

			provider.EmailClaim = tc.EmailClaim
			provider.GroupsClaim = tc.GroupsClaim
			defer server.Close()

			err = provider.EnrichSession(context.Background(), tc.ExistingSession)
			if tc.ExpectedError != nil {
				assert.EqualError(t, err, tc.ExpectedError.Error())
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, *tc.ExpectedSession, *tc.ExistingSession)
		})
	}
}

func TestCidaasProvider_RefreshSession(t *testing.T) {
	testCases := map[string]struct {
		ExistingSession   *sessions.SessionState
		EmailClaim        string
		GroupsClaim       string
		ProfileJSON       map[string]interface{}
		RedeemJSON        redeemTokenResponse
		ExpectedRefreshed bool
		ExpectedError     error
		ExpectedEmail     string
		ExpectedUser      string
	}{
		"Refresh session successfully": {
			ExistingSession: &sessions.SessionState{
				User:         "session.is.not.locked",
				Email:        "found@email.com",
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			RedeemJSON: redeemTokenResponse{
				AccessToken:  accessToken,
				ExpiresIn:    10,
				TokenType:    "Bearer",
				RefreshToken: refreshToken,
			},
			ExpectedRefreshed: true,
			ExpectedError:     nil,
			ExpectedEmail:     defaultIDToken.Email,
			ExpectedUser:      defaultIDToken.Subject,
		},
		"Unable to refresh session": {
			ExistingSession: &sessions.SessionState{
				User:         "session.is.unable.to.refresh",
				Email:        "found@email.com",
				IDToken:      idToken,
				AccessToken:  accessToken,
				RefreshToken: refreshToken,
			},
			ExpectedRefreshed: false,
			ExpectedError:     fmt.Errorf("unable to redeem refresh token: failed to get token: oauth2: server response missing access_token"),
			ExpectedUser:      "session.is.unable.to.refresh",
			ExpectedEmail:     "found@email.com",
		},
	}
	for testName, tc := range testCases {
		t.Run(testName, func(t *testing.T) {
			idToken, _ := newSignedTestIDToken(defaultIDToken)
			tc.RedeemJSON.IDToken = idToken
			redeemPath := "/token/"
			redeemJSONResp, err := json.Marshal(tc.RedeemJSON)
			assert.NoError(t, err)

			serverURL, server := newCidaasServer(
				map[string][]byte{
					redeemPath: redeemJSONResp,
				})
			provider := newCidaasProvider(serverURL)

			// Disable session enrichment, because we want to focus on refreshing logic
			provider.ProfileURL, err = url.Parse("")
			assert.NoError(t, err)
			provider.RedeemURL, err = url.Parse(fmt.Sprintf("%s%s", server.URL, redeemPath))
			assert.NoError(t, err)

			provider.GroupsClaim = tc.GroupsClaim
			defer server.Close()

			var refreshed bool
			refreshed, err = provider.RefreshSession(context.Background(), tc.ExistingSession)

			if tc.ExpectedError != nil {
				assert.EqualError(t, err, tc.ExpectedError.Error())
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tc.ExpectedRefreshed, refreshed)
			assert.Equal(t, tc.ExpectedEmail, tc.ExistingSession.Email)
			assert.Equal(t, tc.ExpectedUser, tc.ExistingSession.User)
		})
	}
}
