// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0

package middleware

import (
	"context"
	"errors"
	"net/http"
	"testing"

	"github.com/go-openapi/testify/v2/require"

	"github.com/go-openapi/runtime"
)

const testAuthenticator = "auth1"

type principalType struct {
	Name string
}

type countAuthenticator struct {
	count     int
	applies   bool
	principal any
	err       error
}

func (c *countAuthenticator) Authenticate(_ any) (bool, any, error) {
	c.count++
	return c.applies, c.principal, c.err
}

func newCountAuthenticator(applies bool, principal any, err error) *countAuthenticator {
	return &countAuthenticator{applies: applies, principal: principal, err: err}
}

var (
	successAuth = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		return true, valTheUser, nil
	})
	failAuth = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		return true, nil, errors.New("unauthenticated")
	})
	noApplyAuth = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		return false, nil, nil
	})
	successAuthWithPointer = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		return true, &principalType{Name: valTheUser}, nil
	})
	failAuthWithPointer = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		var typedPrincipal *principalType
		return true, typedPrincipal, errors.New("unauthenticated")
	})
	failAuthWithNilPointer = runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		var typedPrincipal *principalType
		return true, typedPrincipal, nil
	})
)

func TestAuthenticateSingle(t *testing.T) {
	t.Parallel()

	t.Run("with string", func(t *testing.T) {
		t.Parallel()

		ra := RouteAuthenticator{
			Authenticator: map[string]runtime.Authenticator{
				testAuthenticator: successAuth,
			},
			Schemes: []string{testAuthenticator},
			Scopes:  map[string][]string{testAuthenticator: nil},
		}
		ras := RouteAuthenticators([]RouteAuthenticator{ra})
		require.FalseT(t, ras.AllowsAnonymous())

		req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
		route := &MatchedRoute{}
		ok, prin, err := ras.Authenticate(req, route)
		require.NoError(t, err)
		require.TrueT(t, ok)
		require.Equal(t, valTheUser, prin)

		require.Equal(t, ra, *route.Authenticator)
	})

	t.Run("with pointer", func(t *testing.T) {
		t.Parallel()

		ra := RouteAuthenticator{
			Authenticator: map[string]runtime.Authenticator{
				testAuthenticator: successAuthWithPointer,
			},
			Schemes: []string{testAuthenticator},
			Scopes:  map[string][]string{testAuthenticator: nil},
		}
		ras := RouteAuthenticators([]RouteAuthenticator{ra})
		require.FalseT(t, ras.AllowsAnonymous())

		req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
		route := &MatchedRoute{}
		ok, prin, err := ras.Authenticate(req, route)
		require.NoError(t, err)
		require.TrueT(t, ok)
		typed, ok := prin.(*principalType)
		require.TrueT(t, ok)
		require.EqualT(t, valTheUser, typed.Name)
		require.Equal(t, ra, *route.Authenticator)
	})
}

func TestAuthenticateWrong(t *testing.T) {
	t.Run("with principal as a pointer to a concrete type", func(t *testing.T) {
		t.Run("should authenticate", func(t *testing.T) {
			ra := RouteAuthenticator{
				Authenticator: map[string]runtime.Authenticator{
					testAuthenticator: successAuthWithPointer,
				},
				Schemes: []string{testAuthenticator},
				Scopes:  map[string][]string{testAuthenticator: nil},
			}
			ras := RouteAuthenticators([]RouteAuthenticator{ra})

			require.False(t, ras.AllowsAnonymous())

			req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
			route := &MatchedRoute{}
			ok, prin, err := ras.Authenticate(req, route)
			require.NoError(t, err)
			require.True(t, ok)
			require.EqualValues(t, &principalType{Name: valTheUser}, prin)
		})
		t.Run("should not authenticate", func(t *testing.T) {
			ra := RouteAuthenticator{
				Authenticator: map[string]runtime.Authenticator{
					testAuthenticator: failAuthWithPointer,
				},
				Schemes: []string{testAuthenticator},
				Scopes:  map[string][]string{testAuthenticator: nil},
			}
			ras := RouteAuthenticators([]RouteAuthenticator{ra})

			require.False(t, ras.AllowsAnonymous())

			req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
			route := &MatchedRoute{}
			ok, prin, err := ras.Authenticate(req, route)
			require.Error(t, err)
			require.True(t, ok)
			require.Nil(t, prin)
		})
		t.Run("should yield nil principal", func(t *testing.T) {
			ra := RouteAuthenticator{
				Authenticator: map[string]runtime.Authenticator{
					testAuthenticator: failAuthWithNilPointer,
				},
				Schemes: []string{testAuthenticator},
				Scopes:  map[string][]string{testAuthenticator: nil},
			}
			ras := RouteAuthenticators([]RouteAuthenticator{ra})

			require.False(t, ras.AllowsAnonymous())

			req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
			route := &MatchedRoute{}
			ok, prin, err := ras.Authenticate(req, route)
			require.NoError(t, err)
			require.FalseT(t, ok, "should not be authenticated: the principal is nil")
			require.Nil(t, prin)
		})
	})
}

func TestAuthenticateLogicalOr(t *testing.T) {
	ra1 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuthenticator: noApplyAuth,
		},
		Schemes: []string{testAuthenticator},
		Scopes:  map[string][]string{testAuthenticator: nil},
	}
	ra2 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuth2: successAuth,
		},
		Schemes: []string{testAuth2},
		Scopes:  map[string][]string{testAuth2: nil},
	}
	// right side matches
	ras := RouteAuthenticators([]RouteAuthenticator{ra1, ra2})
	require.FalseT(t, ras.AllowsAnonymous())

	req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route := &MatchedRoute{}
	ok, prin, err := ras.Authenticate(req, route)
	require.NoError(t, err)
	require.TrueT(t, ok)
	require.Equal(t, valTheUser, prin)

	require.Equal(t, ra2, *route.Authenticator)

	// left side matches
	ras = RouteAuthenticators([]RouteAuthenticator{ra2, ra1})
	require.FalseT(t, ras.AllowsAnonymous())

	req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route = &MatchedRoute{}
	ok, prin, err = ras.Authenticate(req, route)
	require.NoError(t, err)

	require.TrueT(t, ok)
	require.Equal(t, valTheUser, prin)
	require.Equal(t, ra2, *route.Authenticator)
}

func TestAuthenticateLogicalAnd(t *testing.T) {
	ra1 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuthenticator: noApplyAuth,
		},
		Schemes: []string{testAuthenticator},
		Scopes:  map[string][]string{testAuthenticator: nil},
	}
	authorizer := newCountAuthenticator(true, valTheUser, nil)
	ra2 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuth2: authorizer,
			testAuth3: authorizer,
		},
		Schemes: []string{testAuth2, testAuth3},
		Scopes:  map[string][]string{testAuth2: nil},
	}
	ras := RouteAuthenticators([]RouteAuthenticator{ra1, ra2})
	require.FalseT(t, ras.AllowsAnonymous())

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	require.NoError(t, err)
	route := &MatchedRoute{}
	ok, prin, err := ras.Authenticate(req, route)
	require.NoError(t, err)
	require.TrueT(t, ok)
	require.Equal(t, valTheUser, prin)

	require.Equal(t, ra2, *route.Authenticator)
	require.EqualT(t, 2, authorizer.count)

	var count int
	successA := runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		count++
		return true, valTheUser, nil
	})
	failA := runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		count++
		return true, nil, errors.New("unauthenticated")
	})

	ra3 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuth2: successA,
			testAuth3: failA,
			testAuth4: successA,
		},
		Schemes: []string{testAuth2, testAuth3, testAuth4},
		Scopes:  map[string][]string{testAuth2: nil},
	}
	ras = RouteAuthenticators([]RouteAuthenticator{ra1, ra3})

	require.FalseT(t, ras.AllowsAnonymous())

	req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route = &MatchedRoute{}
	ok, prin, err = ras.Authenticate(req, route)
	require.Error(t, err)
	require.TrueT(t, ok)
	require.Nil(t, prin)

	require.Equal(t, ra3, *route.Authenticator)
	require.EqualT(t, 2, count)

	ra4 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuth2: successA,
			testAuth3: successA,
			testAuth4: failA,
		},
		Schemes: []string{testAuth2, testAuth3, testAuth4},
		Scopes:  map[string][]string{testAuth2: nil},
	}
	ras = RouteAuthenticators([]RouteAuthenticator{ra1, ra4})

	require.FalseT(t, ras.AllowsAnonymous())

	req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route = &MatchedRoute{}
	ok, prin, err = ras.Authenticate(req, route)
	require.Error(t, err)
	require.TrueT(t, ok)
	require.Nil(t, prin)

	require.Equal(t, ra4, *route.Authenticator)
	require.EqualT(t, 5, count)
}

// TestAuthenticateTypedNilPrincipal is a regression test for
// https://github.com/go-openapi/runtime/issues/147.
//
// When an authenticator is written with a concrete principal type (e.g. *myPrincipal),
// returning (true, nil, nil) packs a typed-nil into the any return. A naive
// `usr == nil` check would miss it and treat the typed-nil as a successful auth.
// The Authenticate path must detect that case so the unauthenticated branch
// (a 401 in Context.Authorize) is taken.
func TestAuthenticateTypedNilPrincipal(t *testing.T) {
	type myPrincipal struct{}

	typedNilAuth := runtime.AuthenticatorFunc(func(_ any) (bool, any, error) {
		var p *myPrincipal // typed nil
		return true, p, nil
	})

	ra := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuthenticator: typedNilAuth,
		},
		Schemes: []string{testAuthenticator},
		Scopes:  map[string][]string{testAuthenticator: nil},
	}
	ras := RouteAuthenticators([]RouteAuthenticator{ra})

	req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route := &MatchedRoute{}
	ok, prin, err := ras.Authenticate(req, route)
	require.NoError(t, err)
	// applies=true was returned but principal is (typed) nil: treated as
	// not-authenticated, so the aggregator must report no successful auth.
	require.FalseT(t, ok)
	require.Nil(t, prin)
}

func TestAuthenticateOptional(t *testing.T) {
	ra1 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuthenticator: noApplyAuth,
		},
		Schemes: []string{testAuthenticator},
		Scopes:  map[string][]string{testAuthenticator: nil},
	}
	ra2 := RouteAuthenticator{
		allowAnonymous: true,
		Schemes:        []string{""},
		Scopes:         map[string][]string{"": {}},
	}

	ra3 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuth2: noApplyAuth,
		},
		Schemes: []string{testAuth2},
		Scopes:  map[string][]string{testAuth2: nil},
	}

	ras := RouteAuthenticators([]RouteAuthenticator{ra1, ra2, ra3})
	require.TrueT(t, ras.AllowsAnonymous())

	req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route := &MatchedRoute{}
	ok, prin, err := ras.Authenticate(req, route)
	require.NoError(t, err)
	require.TrueT(t, ok)
	require.Nil(t, prin)

	require.Equal(t, ra2, *route.Authenticator)

	ra4 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuthenticator: noApplyAuth,
		},
		Schemes: []string{testAuthenticator},
		Scopes:  map[string][]string{testAuthenticator: nil},
	}
	ra5 := RouteAuthenticator{
		allowAnonymous: true,
	}

	ra6 := RouteAuthenticator{
		Authenticator: map[string]runtime.Authenticator{
			testAuth2: failAuth,
		},
		Schemes: []string{testAuth2},
		Scopes:  map[string][]string{testAuth2: nil},
	}

	ras = RouteAuthenticators([]RouteAuthenticator{ra4, ra5, ra6})
	require.TrueT(t, ras.AllowsAnonymous())

	req, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
	route = &MatchedRoute{}
	ok, prin, err = ras.Authenticate(req, route)
	require.Error(t, err)
	require.TrueT(t, ok)
	require.Nil(t, prin)

	require.Equal(t, ra6, *route.Authenticator)
}
