package e2e

import (
	"bytes"
	"context"
	"net/http"
	"strings"
	"testing"
	"time"

	g "github.com/onsi/ginkgo/v2"
	"github.com/stretchr/testify/require"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"

	oauthapi "github.com/openshift/api/oauth/v1"
	userapi "github.com/openshift/api/user/v1"
	configclient "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
	oauthclient "github.com/openshift/client-go/oauth/clientset/versioned/typed/oauth/v1"
	userclient "github.com/openshift/client-go/user/clientset/versioned/typed/user/v1"

	test "github.com/openshift/cluster-authentication-operator/test/library"
)

const (
	defaultAccessTokenMaxAgeSeconds = 86400
)

var _ = g.Describe("[sig-auth] authentication operator", func() {
	g.It("[Tokens] TestTokenInactivityTimeout [Timeout:1h]", func() {
		testTokenInactivityTimeout(g.GinkgoTB())
	})
})

func testTokenInactivityTimeout(t testing.TB) {
	clients := test.NewTestClients(t)
	kubeConfig := test.NewClientConfigForTest(t)

	transport := test.NewInsecureHTTPTransport()

	testTokenValidity := func(t testing.TB, req *http.Request, statusCode int, bearerToken, errMsg string) {
		req.Header.Set("Authorization", "Bearer "+bearerToken)
		resp, err := transport.RoundTrip(req)
		require.NoError(t, err)
		defer resp.Body.Close()
		if resp.StatusCode != statusCode {
			t.Fatalf("%s. Received status %q", errMsg, resp.Status)
		}
	}

	// This checks that
	// 1. Token with timeout does not work after it times out
	// 2. Token without timeout works at anytime
	testTokenTimeouts := func(t testing.TB, tokenWithTimeout, tokenWithoutTimeout string, timeout time.Duration) {
		req, err := http.NewRequest(http.MethodGet, kubeConfig.Host+"/apis/user.openshift.io/v1/users/~", &bytes.Buffer{})
		require.NoError(t, err)

		time.Sleep(timeout + 5*time.Second)

		testTokenValidity(t, req, http.StatusUnauthorized, tokenWithTimeout, "accessing token after it timed out should not work")
		testTokenValidity(t, req, http.StatusOK, tokenWithoutTimeout, "token with out timeout should work")
	}

	// This checks that
	// 1. Token with timeout works immediately after they are created
	// 2. Token with timeout works anytime before it times out
	// 3. Token with timeout does not work after it times out
	// 4. Token without timeout works at anytime
	testInactivityTimeoutScenarios := func(t testing.TB, tokenWithTimeout, tokenWithoutTimeout string, timeout time.Duration) {
		req, err := http.NewRequest(http.MethodGet, kubeConfig.Host+"/apis/user.openshift.io/v1/users/~", &bytes.Buffer{})
		require.NoError(t, err)

		testTokenValidity(t, req, http.StatusOK, tokenWithTimeout, "accessing token before it timed out should work")
		testTokenValidity(t, req, http.StatusOK, tokenWithoutTimeout, "token with out timeout should work")

		time.Sleep(20 * time.Second)

		testTokenValidity(t, req, http.StatusOK, tokenWithTimeout, "accessing token before it timed out should work")
		testTokenValidity(t, req, http.StatusOK, tokenWithoutTimeout, "token with out timeout should work")

		time.Sleep(timeout + 5*time.Second)

		testTokenValidity(t, req, http.StatusUnauthorized, tokenWithTimeout, "accessing token after it timed out should not work")
		testTokenValidity(t, req, http.StatusOK, tokenWithoutTimeout, "token with out timeout should work")
	}

	configInactivityTimeout := int32(300) // minimum acceptable token timeout value is 300 seconds
	oauthClientTimeout := int32(300)      // minimum acceptable token timeout value is 300 seconds

	// No OAuthClient timeout and no OAuth config timeout.
	checkTokenAccess(t, clients.UserClient, clients.OAuthClient.OauthV1(), configInactivityTimeout, nil, testTokenTimeouts)

	// With only OAuth config timeout.
	updateOAuthConfigInactivityTimeout(t, clients.ConfigClient.ConfigV1(), &metav1.Duration{Duration: time.Duration(configInactivityTimeout) * time.Second})

	err := test.WaitForClusterOperatorProgressing(t, clients.ConfigClient.ConfigV1(), "authentication")
	require.NoError(t, err, "authentication operator never became progressing")
	err = test.WaitForClusterOperatorAvailableNotProgressingNotDegraded(t, clients.ConfigClient.ConfigV1(), "authentication")
	require.NoError(t, err)
	client := kubernetes.NewForConfigOrDie(kubeConfig)
	waitOAuthServerReplicasReady(t, client)

	checkTokenAccess(t, clients.UserClient, clients.OAuthClient.OauthV1(), configInactivityTimeout, nil, testInactivityTimeoutScenarios)

	// With both OAuth config timeout and OAuthClient timeout.
	checkTokenAccess(t, clients.UserClient, clients.OAuthClient.OauthV1(), configInactivityTimeout, &oauthClientTimeout, testInactivityTimeoutScenarios)

	// No OAuthClient timeout and no OAuth config timeout.
	updateOAuthConfigInactivityTimeout(t, clients.ConfigClient.ConfigV1(), nil)

	err = test.WaitForClusterOperatorProgressing(t, clients.ConfigClient.ConfigV1(), "authentication")
	require.NoError(t, err, "authentication operator never became progressing")
	err = test.WaitForClusterOperatorAvailableNotProgressingNotDegraded(t, clients.ConfigClient.ConfigV1(), "authentication")
	require.NoError(t, err)
	client = kubernetes.NewForConfigOrDie(kubeConfig)
	waitOAuthServerReplicasReady(t, client)

	checkTokenAccess(t, clients.UserClient, clients.OAuthClient.OauthV1(), configInactivityTimeout, nil, testTokenTimeouts)
}

func checkTokenAccess(t testing.TB,
	userClient userclient.UserV1Interface,
	oauthClient oauthclient.OauthV1Interface,
	configInactivityTimeout int32, oauthClientTimeout *int32,
	testAccess func(testing.TB, string, string, time.Duration)) {
	// Create the user, identity, oauthclient and oauthaccesstoken objects needed for authentication using Bearer tokens.
	subTestNameHierarchy := strings.Split(t.Name(), "/")
	prefix := test.SanitizeResourceName(subTestNameHierarchy[len(subTestNameHierarchy)-1]) + "-"

	userName := prefix + "testuser"
	idpName := "htpasswd"
	oauthClientName := prefix + "oauthclient"
	redirectURIs := []string{"https://localhost"}
	identityName := idpName + ":" + userName

	uid, cleanup := createUser(t, userClient, userName, identityName)
	defer cleanup()

	cleanup = createIdentity(t, userClient, userName, identityName, idpName, uid)
	defer cleanup()

	cleanup = createOAuthClient(t, oauthClient, oauthClientName, redirectURIs, oauthClientTimeout)
	defer cleanup()

	expectedInactivityTimeout := configInactivityTimeout
	if oauthClientTimeout != nil {
		expectedInactivityTimeout = *oauthClientTimeout
	}

	tokenWithTimeoutClear, tokenWithTimeoutHash := test.GenerateOAuthTokenPair()
	tokenWithTimeout := &oauthapi.OAuthAccessToken{
		ObjectMeta: metav1.ObjectMeta{
			Name: tokenWithTimeoutHash,
		},
		ClientName:               oauthClientName,
		ExpiresIn:                defaultAccessTokenMaxAgeSeconds,
		Scopes:                   []string{"user:full"},
		RedirectURI:              redirectURIs[0],
		UserName:                 userName,
		UserUID:                  string(uid),
		AuthorizeToken:           "mJOQ7Es5l9V7WYDl0bvl3E_hRjnJ21ZZxXH6YZj3yeS",
		InactivityTimeoutSeconds: int32(expectedInactivityTimeout),
	}

	tokenWithoutTimeoutClear, tokenWithoutTimeoutHash := test.GenerateOAuthTokenPair()
	tokenWithoutTimeout := &oauthapi.OAuthAccessToken{
		ObjectMeta: metav1.ObjectMeta{
			Name: tokenWithoutTimeoutHash,
		},
		ClientName:     oauthClientName,
		ExpiresIn:      defaultAccessTokenMaxAgeSeconds,
		Scopes:         []string{"user:full"},
		RedirectURI:    redirectURIs[0],
		UserName:       userName,
		UserUID:        string(uid),
		AuthorizeToken: "mJOQ7Es5l9V7WYDl0bvl3E_hRjnJ21ZZxXH6YZj3yeT",
	}

	// create tokens with and without timeouts
	for _, accessToken := range []*oauthapi.OAuthAccessToken{tokenWithTimeout, tokenWithoutTimeout} {
		_, err := oauthClient.OAuthAccessTokens().Create(context.TODO(), accessToken, metav1.CreateOptions{})
		require.NoError(t, err)
		defer func(name string) {
			if err := oauthClient.OAuthAccessTokens().Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
				t.Logf("%v", err)
			}
		}(accessToken.Name)
	}
	testAccess(t, tokenWithTimeoutClear, tokenWithoutTimeoutClear, time.Duration(expectedInactivityTimeout)*time.Second)
}

func updateOAuthConfigInactivityTimeout(t testing.TB, client configclient.ConfigV1Interface, duration *metav1.Duration) {
	oauthConfig, err := client.OAuths().Get(context.TODO(), "cluster", metav1.GetOptions{})
	require.NoError(t, err)

	oauthConfig.Spec.TokenConfig.AccessTokenInactivityTimeout = duration

	err = wait.PollImmediate(300*time.Millisecond, 2*time.Second, func() (bool, error) {
		_, err := client.OAuths().Update(context.TODO(), oauthConfig, metav1.UpdateOptions{})
		if err != nil {
			t.Logf("failed to update oauth cluster config: %v", err)
			return false, nil
		}
		return true, nil
	})
	require.NoError(t, err)
}

func createUser(t testing.TB, userClient userclient.UserV1Interface, userName, identity string) (types.UID, func()) {
	user := &userapi.User{
		ObjectMeta: metav1.ObjectMeta{
			Name: userName,
		},
		Identities: []string{identity},
	}

	err := wait.PollImmediate(300*time.Millisecond, 2*time.Second, func() (bool, error) {
		var err error
		user, err = userClient.Users().Create(context.TODO(), user, metav1.CreateOptions{})
		if err != nil {
			t.Logf("failed to create user: %v", err)
			return false, nil
		}
		return true, nil
	})
	require.NoError(t, err)
	return user.UID, func() {
		if err := userClient.Users().Delete(context.TODO(), userName, metav1.DeleteOptions{}); err != nil {
			t.Logf("%v", err)
		}
	}
}

func createIdentity(t testing.TB, userClient userclient.UserV1Interface, userName, identityName, idpName string, uid types.UID) func() {
	identity := &userapi.Identity{
		ObjectMeta: metav1.ObjectMeta{
			Name: identityName,
		},
		ProviderName:     idpName,
		ProviderUserName: userName,
		User: corev1.ObjectReference{
			Name: userName,
			UID:  uid,
		},
	}

	err := wait.PollImmediate(2*time.Second, 300*time.Second, func() (bool, error) {
		_, err := userClient.Identities().Create(context.TODO(), identity, metav1.CreateOptions{})
		if err != nil {
			t.Logf("failed to create user identity: %v", err)
			return false, nil
		}
		return true, nil
	})
	require.NoError(t, err)
	return func() {
		if err := userClient.Identities().Delete(context.TODO(), identity.Name, metav1.DeleteOptions{}); err != nil {
			t.Logf("%v", err)
		}
	}
}

func createOAuthClient(t testing.TB, oauthClient oauthclient.OauthV1Interface, oauthClientName string, redirectURIs []string, timeout *int32) func() {
	oauthClientObj := &oauthapi.OAuthClient{
		ObjectMeta: metav1.ObjectMeta{
			Name: oauthClientName,
		},
		Secret:                              "the-secret-for-oauth-client",
		RedirectURIs:                        redirectURIs,
		GrantMethod:                         "auto",
		AccessTokenInactivityTimeoutSeconds: timeout,
	}

	err := wait.PollImmediate(300*time.Millisecond, 2*time.Second, func() (bool, error) {
		_, err := oauthClient.OAuthClients().Create(context.TODO(), oauthClientObj, metav1.CreateOptions{})
		if err != nil {
			t.Logf("failed to create oauth client: %v", err)
			return false, nil
		}
		return true, nil
	})
	require.NoError(t, err)
	return func() {
		if err := oauthClient.OAuthClients().Delete(context.TODO(), oauthClientObj.Name, metav1.DeleteOptions{}); err != nil {
			t.Logf("%v", err)
		}
	}
}

func waitOAuthServerReplicasReady(t testing.TB, kubeClient kubernetes.Interface) {
	t.Logf("waiting all oauth-apiserver replicas to be updated and ready")

	err := wait.PollImmediate(time.Second, 5*time.Minute, func() (bool, error) {
		deployment, err := kubeClient.AppsV1().Deployments("openshift-oauth-apiserver").Get(context.Background(), "apiserver", metav1.GetOptions{})
		if err != nil {
			t.Logf("failed to retrieve oauth-apiserver's deployment: %v", err)
			return false, nil
		}

		var requestedReplicas int32 = 1
		if specReplicas := deployment.Spec.Replicas; specReplicas != nil {
			requestedReplicas = *specReplicas
		}
		return requestedReplicas == deployment.Status.ReadyReplicas && deployment.Status.UpdatedReplicas == requestedReplicas, nil
	})
	require.NoError(t, err)

}
