//go:build integration

package admin_test

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/text/language"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/zitadel/zitadel/pkg/grpc/admin"
	"github.com/zitadel/zitadel/pkg/grpc/management"
	"github.com/zitadel/zitadel/pkg/grpc/text"
	"github.com/zitadel/zitadel/pkg/grpc/user"
)

func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
	defer cancel()

	var (
		defaultAndAllowedLanguage = language.German
		supportedLanguagesStr     = []string{language.German.String(), language.English.String(), language.Japanese.String()}
		disallowedLanguage        = language.Spanish
		unsupportedLanguage       = language.Afrikaans
	)

	domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
	t.Run("assumed defaults are correct", func(tt *testing.T) {
		tt.Run("languages are not restricted by default", func(ttt *testing.T) {
			restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{})
			require.NoError(ttt, err)
			require.Len(ttt, restrictions.AllowedLanguages, 0)
		})
		tt.Run("default language is English by default", func(ttt *testing.T) {
			defaultLang, err := Tester.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{})
			require.NoError(ttt, err)
			require.Equal(ttt, language.Make(defaultLang.Language), language.English)
		})
		tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) {
			awaitDiscoveryEndpoint(ttt, domain, supportedLanguagesStr, nil)
		})
	})
	t.Run("restricting the default language fails", func(tt *testing.T) {
		_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}})
		expectStatus, ok := status.FromError(err)
		require.True(tt, ok)
		require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
	})
	t.Run("not defining any restrictions throws an error", func(tt *testing.T) {
		_, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{})
		expectStatus, ok := status.FromError(err)
		require.True(tt, ok)
		require.Equal(tt, codes.InvalidArgument, expectStatus.Code())
	})
	t.Run("setting the default language works", func(tt *testing.T) {
		setAndAwaitDefaultLanguage(iamOwnerCtx, tt, defaultAndAllowedLanguage)
	})
	t.Run("restricting allowed languages works", func(tt *testing.T) {
		setAndAwaitAllowedLanguages(iamOwnerCtx, tt, []string{defaultAndAllowedLanguage.String()})
	})
	t.Run("GetAllowedLanguage returns only the allowed languages", func(tt *testing.T) {
		expectContains, expectNotContains := []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}
		adminResp, err := Tester.Client.Admin.GetAllowedLanguages(iamOwnerCtx, &admin.GetAllowedLanguagesRequest{})
		require.NoError(t, err)
		langs := adminResp.GetLanguages()
		assert.Condition(t, contains(langs, expectContains))
		assert.Condition(t, not(contains(langs, expectNotContains)))
	})
	t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) {
		_, err := Tester.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()})
		expectStatus, ok := status.FromError(err)
		require.True(tt, ok)
		require.Equal(tt, codes.FailedPrecondition, expectStatus.Code())
	})
	t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) {
		supported, err := Tester.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{})
		require.NoError(tt, err)
		require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr))
	})
	t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) {
		awaitDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()})
	})
	t.Run("the login ui is rendered in the default language", func(tt *testing.T) {
		awaitLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz")
	})
	t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) {
		tt.Run("change user profile", func(ttt *testing.T) {
			resp, err := Tester.Client.Mgmt.ListUsers(iamOwnerCtx, &management.ListUsersRequest{Queries: []*user.SearchQuery{{Query: &user.SearchQuery_UserNameQuery{UserNameQuery: &user.UserNameQuery{
				UserName: "zitadel-admin@zitadel.localhost"}},
			}}})
			require.NoError(ttt, err)
			require.Len(ttt, resp.GetResult(), 1)
			humanAdmin := resp.GetResult()[0]
			profile := humanAdmin.GetHuman().GetProfile()
			require.NotEqual(ttt, unsupportedLanguage.String(), profile.GetPreferredLanguage())
			_, updateErr := Tester.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{
				PreferredLanguage: unsupportedLanguage.String(),
				UserId:            humanAdmin.GetId(),
				FirstName:         profile.GetFirstName(),
				LastName:          profile.GetLastName(),
				NickName:          profile.GetNickName(),
				DisplayName:       profile.GetDisplayName(),
				Gender:            profile.GetGender(),
			})
			require.NoError(ttt, updateErr)
		})
	})
	t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) {
		_, err := Tester.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{
			Language: disallowedLanguage.String(),
			EmailVerificationText: &text.EmailVerificationScreenText{
				Description: "hodor",
			},
		})
		assert.NoError(tt, err)
		_, err = Tester.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{
			Language: disallowedLanguage.String(),
			EmailVerificationText: &text.EmailVerificationScreenText{
				Description: "hodor",
			},
		})
		assert.NoError(tt, err)
		_, err = Tester.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{
			Language: disallowedLanguage.String(),
			Text:     "hodor",
		})
		assert.NoError(tt, err)
		_, err = Tester.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{
			Language: disallowedLanguage.String(),
			Text:     "hodor",
		})
		assert.NoError(tt, err)
	})
	t.Run("allowing all languages works", func(tt *testing.T) {
		tt.Run("restricting allowed languages works", func(ttt *testing.T) {
			setAndAwaitAllowedLanguages(iamOwnerCtx, ttt, make([]string, 0))
		})
	})

	t.Run("allowing the language makes it usable again", func(tt *testing.T) {
		tt.Run("the previously disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) {
			awaitDiscoveryEndpoint(ttt, domain, []string{disallowedLanguage.String()}, nil)
		})
		tt.Run("the login ui is rendered in the previously disallowed language", func(ttt *testing.T) {
			awaitLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones")
		})
	})
}

func setAndAwaitAllowedLanguages(ctx context.Context, t *testing.T, selectLanguages []string) {
	_, err := Tester.Client.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}})
	require.NoError(t, err)
	awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
	defer awaitCancel()
	await(t, awaitCtx, func(tt *assert.CollectT) {
		restrictions, getErr := Tester.Client.Admin.GetRestrictions(awaitCtx, &admin.GetRestrictionsRequest{})
		expectLanguages := selectLanguages
		if len(selectLanguages) == 0 {
			expectLanguages = nil
		}
		assert.NoError(tt, getErr)
		assert.Equal(tt, expectLanguages, restrictions.GetAllowedLanguages())
	})
}

func setAndAwaitDefaultLanguage(ctx context.Context, t *testing.T, lang language.Tag) {
	_, err := Tester.Client.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()})
	require.NoError(t, err)
	awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second)
	defer awaitCancel()
	await(t, awaitCtx, func(tt *assert.CollectT) {
		defaultLang, getErr := Tester.Client.Admin.GetDefaultLanguage(awaitCtx, &admin.GetDefaultLanguageRequest{})
		assert.NoError(tt, getErr)
		assert.Equal(tt, lang.String(), defaultLang.GetLanguage())
	})
}

func awaitDiscoveryEndpoint(t *testing.T, domain string, containsUILocales, notContainsUILocales []string) {
	awaitCtx, awaitCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer awaitCancel()
	await(t, awaitCtx, func(tt *assert.CollectT) {
		req, err := http.NewRequestWithContext(awaitCtx, http.MethodGet, "http://"+domain+":8080/.well-known/openid-configuration", nil)
		require.NoError(tt, err)
		resp, err := http.DefaultClient.Do(req)
		require.NoError(tt, err)
		require.Equal(tt, http.StatusOK, resp.StatusCode)
		body, err := io.ReadAll(resp.Body)
		defer func() {
			require.NoError(tt, resp.Body.Close())
		}()
		require.NoError(tt, err)
		doc := struct {
			UILocalesSupported []string `json:"ui_locales_supported"`
		}{}
		require.NoError(tt, json.Unmarshal(body, &doc))
		if containsUILocales != nil {
			assert.Condition(tt, contains(doc.UILocalesSupported, containsUILocales))
		}
		if notContainsUILocales != nil {
			assert.Condition(tt, not(contains(doc.UILocalesSupported, notContainsUILocales)))
		}
	})
}

func awaitLoginUILanguage(t *testing.T, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) {
	awaitCtx, awaitCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer awaitCancel()
	await(t, awaitCtx, func(tt *assert.CollectT) {
		req, err := http.NewRequestWithContext(awaitCtx, http.MethodGet, "http://"+domain+":8080/ui/login/register", nil)
		req.Header.Set("Accept-Language", acceptLanguage.String())
		require.NoError(t, err)
		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode)
		body, err := io.ReadAll(resp.Body)
		defer func() {
			require.NoError(t, resp.Body.Close())
		}()
		require.NoError(t, err)
		assert.Containsf(t, string(body), containsText, "login ui language is in "+expectLang.String())
	})
}

// We would love to use assert.Contains here, but it doesn't work with slices of strings
func contains(container []string, subset []string) assert.Comparison {
	return func() bool {
		if subset == nil {
			return true
		}
		for _, str := range subset {
			var found bool
			for _, containerStr := range container {
				if str == containerStr {
					found = true
					break
				}
			}
			if !found {
				return false
			}
		}
		return true
	}
}

func not(cmp assert.Comparison) assert.Comparison {
	return func() bool {
		return !cmp()
	}
}