package middleware

import (
	"context"
	"net/http"
	"net/http/httptest"
	"time"

	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
	"github.com/golang-jwt/jwt/v5"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/opencloud-eu/opencloud/pkg/log"
	"github.com/opencloud-eu/opencloud/pkg/oidc"
	oidcmocks "github.com/opencloud-eu/opencloud/pkg/oidc/mocks"
	"github.com/opencloud-eu/opencloud/services/proxy/pkg/router"
	"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
	"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend/mocks"
	"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
	"github.com/stretchr/testify/mock"
	"go-micro.dev/v4/store"
	"google.golang.org/grpc"
)

var _ = Describe("authentication helpers", func() {
	DescribeTable("isPublicPath should recognize public paths",
		func(input string, expected bool) {
			isPublic := isPublicPath(input)
			Expect(isPublic).To(Equal(expected))
		},
		Entry("public files path", "/remote.php/dav/public-files/", true),
		Entry("public files path without remote.php", "/remote.php/dav/public-files/", true),
		Entry("token info path", "/ocs/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected", true),
		Entry("token info path", "/ocs/v2.php/apps/files_sharing/api/v1/tokeninfo/unprotected", true),
		Entry("capabilities", "/ocs/v1.php/cloud/capabilities", true),
	)
})

var _ = Describe("Authenticating requests", Label("Authentication"), func() {
	var (
		authenticators []Authenticator
	)
	oc := oidcmocks.OIDCClient{}
	oc.On("VerifyAccessToken", mock.Anything, mock.Anything).Return(
		oidc.RegClaimsWithSID{
			SessionID: "a-session-id",
			RegisteredClaims: jwt.RegisteredClaims{
				ExpiresAt: jwt.NewNumericDate(time.Unix(1147483647, 0)),
			},
		}, jwt.MapClaims{
			"sid": "a-session-id",
			"exp": 1147483647,
		},
		nil,
	)

	ub := mocks.UserBackend{}
	ub.On("Authenticate", mock.Anything, "testuser", "testpassword").Return(
		&userv1beta1.User{
			Id: &userv1beta1.UserId{
				Idp:      "IdpId",
				OpaqueId: "OpaqueId",
			},
			Username: "testuser",
			Mail:     "testuser@example.com",
		},
		"",
		nil,
	)
	ub.On("Authenticate", mock.Anything, mock.Anything, mock.Anything).Return(nil, "", backend.ErrAccountNotFound)

	BeforeEach(func() {
		pool.RemoveSelector("GatewaySelector" + "eu.opencloud.api.gateway")

		logger := log.NewLogger()
		authenticators = []Authenticator{
			BasicAuthenticator{
				Logger:       logger,
				UserProvider: &ub,
			},
			&OIDCAuthenticator{
				OIDCIss:       "http://idp.example.com",
				Logger:        logger,
				oidcClient:    &oc,
				userInfoCache: store.NewMemoryStore(),
				skipUserInfo:  true,
			},
			PublicShareAuthenticator{
				Logger: logger,
				RevaGatewaySelector: pool.GetSelector[gateway.GatewayAPIClient](
					"GatewaySelector",
					"eu.opencloud.api.gateway",
					func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
						return mockGatewayClient{
							AuthenticateFunc: func(authType, clientID, clientSecret string) (string, rpcv1beta1.Code) {
								if authType != "publicshares" {
									return "", rpcv1beta1.Code_CODE_NOT_FOUND
								}

								if clientID == "sharetoken" && (clientSecret == "password|examples3cr3t" || clientSecret == "signature|examplesignature|exampleexpiration") {
									return "exampletoken", rpcv1beta1.Code_CODE_OK
								}

								if clientID == "sharetoken" && clientSecret == "password|" {
									return "otherexampletoken", rpcv1beta1.Code_CODE_OK
								}

								return "", rpcv1beta1.Code_CODE_NOT_FOUND
							},
						}
					},
				),
			},
		}
	})

	When("the public request must contains correct data", func() {
		It("ensures the context oidc data when the Bearer authentication is successful", func() {
			req := httptest.NewRequest("PROPFIND", "http://example.com/remote.php/dav/public-files/", http.NoBody)
			req = req.WithContext(router.SetRoutingInfo(context.Background(), router.RoutingInfo{}))
			req.Header.Set(_headerAuthorization, "Bearer jwt.token.sig")

			handler := Authentication(authenticators,
				EnableBasicAuth(true),
			)
			testHandler := handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				Expect(oidc.FromContext(r.Context())).To(Equal(map[string]any{
					"sid": "a-session-id",
					"exp": int64(1147483647),
				}))
			}))
			rr := httptest.NewRecorder()
			testHandler.ServeHTTP(rr, req)
			Expect(rr).To(HaveHTTPStatus(http.StatusOK))
		})
		It("ensures the context oidc data when user the Basic authentication is successful", func() {
			req := httptest.NewRequest("PROPFIND", "http://example.com/remote.php/dav/public-files/", http.NoBody)
			req = req.WithContext(router.SetRoutingInfo(context.Background(), router.RoutingInfo{}))
			req.SetBasicAuth("testuser", "testpassword")

			handler := Authentication(authenticators,
				EnableBasicAuth(true),
			)
			testHandler := handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				Expect(oidc.FromContext(r.Context())).To(Equal(map[string]any{
					"email":              "testuser@example.com",
					"openclouduuid":      "OpaqueId",
					"iss":                "IdpId",
					"preferred_username": "testuser",
				}))
			}))
			rr := httptest.NewRecorder()
			testHandler.ServeHTTP(rr, req)
			Expect(rr).To(HaveHTTPStatus(http.StatusOK))
		})
		It("ensures the x-access-token header when public-token URL parameter is set", func() {
			req := httptest.NewRequest("PROPFIND", "http://example.com/dav/public-files/?public-token=sharetoken", http.NoBody)
			req = req.WithContext(router.SetRoutingInfo(context.Background(), router.RoutingInfo{}))

			handler := Authentication(authenticators,
				EnableBasicAuth(true),
			)
			testHandler := handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				Expect(r.Header.Get(headerRevaAccessToken)).To(Equal("otherexampletoken"))
			}))
			rr := httptest.NewRecorder()
			testHandler.ServeHTTP(rr, req)
			Expect(rr).To(HaveHTTPStatus(http.StatusOK))
		})
		It("ensures the x-access-token header when public-token URL parameter and BasicAuth are set", func() {
			req := httptest.NewRequest("PROPFIND", "http://example.com/dav/public-files/?public-token=sharetoken", http.NoBody)
			req.SetBasicAuth("public", "examples3cr3t")
			req = req.WithContext(router.SetRoutingInfo(context.Background(), router.RoutingInfo{}))

			handler := Authentication(authenticators,
				EnableBasicAuth(true),
			)
			testHandler := handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				Expect(r.Header.Get(headerRevaAccessToken)).To(Equal("exampletoken"))
			}))
			rr := httptest.NewRecorder()
			testHandler.ServeHTTP(rr, req)
			Expect(rr).To(HaveHTTPStatus(http.StatusOK))
		})
		It("ensures the x-access-token header when public-token BasicAuth is set", func() {
			req := httptest.NewRequest("GET", "http://example.com/archiver", http.NoBody)
			req.Header.Set("public-token", "sharetoken")
			req = req.WithContext(router.SetRoutingInfo(context.Background(), router.RoutingInfo{}))

			handler := Authentication(authenticators,
				EnableBasicAuth(true),
			)
			testHandler := handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				Expect(r.Header.Get(headerRevaAccessToken)).To(Equal("otherexampletoken"))
			}))
			rr := httptest.NewRecorder()
			testHandler.ServeHTTP(rr, req)
			Expect(rr).To(HaveHTTPStatus(http.StatusOK))
		})
	})
})