package featuremgmt

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

	gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg"
	"github.com/open-feature/go-sdk/openfeature"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	authlib "github.com/grafana/authlib/authn"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/clientauth/middleware"
	"github.com/grafana/grafana/pkg/setting"
)

func TestCreateProvider(t *testing.T) {
	u, err := url.Parse("http://localhost:10333")
	require.NoError(t, err)

	testCases := []struct {
		name                  string
		cfg                   setting.OpenFeatureSettings
		expectedProvider      string
		expectExchangeRequest *authlib.TokenExchangeRequest
		failSigning           bool
	}{
		{
			name:             "static provider",
			expectedProvider: setting.StaticProviderType,
		},
		{
			name: "goff provider",
			cfg: setting.OpenFeatureSettings{
				ProviderType: setting.GOFFProviderType,
				URL:          u,
				TargetingKey: "grafana",
			},
			expectExchangeRequest: &authlib.TokenExchangeRequest{
				Namespace: "*",
				Audiences: []string{"features.grafana.app"},
			},
			expectedProvider: setting.GOFFProviderType,
		},
		{
			name: "goff provider with failing token exchange",
			cfg: setting.OpenFeatureSettings{
				ProviderType: setting.GOFFProviderType,
				URL:          u,
				TargetingKey: "grafana",
			},
			expectExchangeRequest: &authlib.TokenExchangeRequest{
				Namespace: "*",
				Audiences: []string{"features.grafana.app"},
			},
			expectedProvider: setting.GOFFProviderType,
			failSigning:      true,
		},
		{
			name: "invalid provider",
			cfg: setting.OpenFeatureSettings{
				ProviderType: "some_provider",
			},
			expectedProvider: setting.StaticProviderType,
		},
	}

	require.NoError(t, err)

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := setting.NewCfg()
			cfg.OpenFeature = tc.cfg

			var tokenExchangeClient *fakeTokenExchangeClient

			if tc.expectExchangeRequest != nil {
				tokenExchangeClient = &fakeTokenExchangeClient{
					Mock: &mock.Mock{
						ExpectedCalls: []*mock.Call{
							{
								Method:    "Exchange",
								Arguments: mock.Arguments{mock.Anything, *tc.expectExchangeRequest},
							},
						},
					},
				}

				if tc.failSigning {
					tokenExchangeClient.expectedErr = errors.New("failed signing access token")
				}
			}

			tokenExchangeMiddleware := middleware.TestingTokenExchangeMiddleware(tokenExchangeClient)
			goffClient, err := goffHTTPClient(tokenExchangeMiddleware)
			require.NoError(t, err, "failed to create goff http client")
			provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil, goffClient)
			require.NoError(t, err)

			err = openfeature.SetProviderAndWait(provider)
			require.NoError(t, err, "failed to set provider")

			if tc.expectedProvider == setting.GOFFProviderType {
				_, ok := provider.(*gofeatureflag.Provider)
				assert.True(t, ok, "expected provider to be of type goff.Provider")

				testGoFFProvider(t, tc.failSigning)
			} else {
				_, ok := provider.(*inMemoryBulkProvider)
				assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider")
			}
		})
	}
}

func testGoFFProvider(t *testing.T, failSigning bool) {
	// this tests with a fake identity with * namespace access, but in any case, it proves what the requester
	// is scoped to is what is used to sign the token with
	ctx, _ := identity.WithServiceIdentity(context.Background(), 1)

	// Test that the flag evaluation can be attempted (though it will fail due to non-existent service)
	// The important thing is that the authentication middleware is properly integrated
	_, err := openfeature.NewDefaultClient().BooleanValueDetails(ctx, "test", false, openfeature.NewEvaluationContext("test", map[string]interface{}{"test": "test"}))

	// Error related to the token exchange should be returned if signing fails
	// otherwise, it should return a connection refused error since the goff URL is not set
	if failSigning {
		assert.ErrorContains(t, err, "failed to exchange token: error signing token", "should return an error when signing fails")
	} else {
		assert.ErrorContains(t, err, "connect: connection refused", "should return an error when goff url is not set")
	}
}
