//go:build integration

package feature_test

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/muhlemmer/gu"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/zitadel/zitadel/internal/integration"
	"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
	"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)

var (
	SystemCTX context.Context
	IamCTX    context.Context
	OrgCTX    context.Context
	Instance  *integration.Instance
	Client    feature.FeatureServiceClient
)

func TestMain(m *testing.M) {
	os.Exit(func() int {
		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
		defer cancel()

		Instance = integration.NewInstance(ctx)
		Client = Instance.Client.FeatureV2

		SystemCTX = integration.WithSystemAuthorization(ctx)
		IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
		OrgCTX = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner)

		return m.Run()
	}())
}

func TestServer_SetSystemFeatures(t *testing.T) {
	type args struct {
		ctx context.Context
		req *feature.SetSystemFeaturesRequest
	}
	tests := []struct {
		name    string
		args    args
		want    *feature.SetSystemFeaturesResponse
		wantErr bool
	}{
		{
			name: "permission error",
			args: args{
				ctx: IamCTX,
				req: &feature.SetSystemFeaturesRequest{
					OidcTriggerIntrospectionProjections: gu.Ptr(true),
				},
			},
			wantErr: true,
		},
		{
			name: "no changes error",
			args: args{
				ctx: SystemCTX,
				req: &feature.SetSystemFeaturesRequest{},
			},
			wantErr: true,
		},
		{
			name: "success",
			args: args{
				ctx: SystemCTX,
				req: &feature.SetSystemFeaturesRequest{
					OidcTriggerIntrospectionProjections: gu.Ptr(true),
				},
			},
			want: &feature.SetSystemFeaturesResponse{
				Details: &object.Details{
					ChangeDate:    timestamppb.Now(),
					ResourceOwner: "SYSTEM",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(func() {
				// make sure we have a clean state after each test
				_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
				require.NoError(t, err)
			})
			got, err := Client.SetSystemFeatures(tt.args.ctx, tt.args.req)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			integration.AssertDetails(t, tt.want, got)
		})
	}
}

func TestServer_ResetSystemFeatures(t *testing.T) {
	_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
		LoginDefaultOrg: gu.Ptr(true),
	})
	require.NoError(t, err)

	tests := []struct {
		name    string
		ctx     context.Context
		want    *feature.ResetSystemFeaturesResponse
		wantErr bool
	}{
		{
			name:    "permission error",
			ctx:     IamCTX,
			wantErr: true,
		},
		{
			name: "success",
			ctx:  SystemCTX,
			want: &feature.ResetSystemFeaturesResponse{
				Details: &object.Details{
					ChangeDate:    timestamppb.Now(),
					ResourceOwner: "SYSTEM",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := Client.ResetSystemFeatures(tt.ctx, &feature.ResetSystemFeaturesRequest{})
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			integration.AssertDetails(t, tt.want, got)
		})
	}
}

func TestServer_GetSystemFeatures(t *testing.T) {
	type args struct {
		ctx context.Context
		req *feature.GetSystemFeaturesRequest
	}
	tests := []struct {
		name    string
		prepare func(t *testing.T)
		args    args
		want    *feature.GetSystemFeaturesResponse
		wantErr bool
	}{
		{
			name: "permission error",
			args: args{
				ctx: IamCTX,
				req: &feature.GetSystemFeaturesRequest{},
			},
			wantErr: true,
		},
		{
			name: "nothing set",
			args: args{
				ctx: SystemCTX,
				req: &feature.GetSystemFeaturesRequest{},
			},
			want: &feature.GetSystemFeaturesResponse{},
		},
		{
			name: "some features",
			prepare: func(t *testing.T) {
				_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
					LoginDefaultOrg:                     gu.Ptr(true),
					OidcTriggerIntrospectionProjections: gu.Ptr(false),
				})
				require.NoError(t, err)
			},
			args: args{
				ctx: SystemCTX,
				req: &feature.GetSystemFeaturesRequest{},
			},
			want: &feature.GetSystemFeaturesResponse{
				LoginDefaultOrg: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_SYSTEM,
				},
				OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_SYSTEM,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(func() {
				// make sure we have a clean state after each test
				_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
				require.NoError(t, err)
			})
			if tt.prepare != nil {
				tt.prepare(t)
			}
			got, err := Client.GetSystemFeatures(tt.args.ctx, tt.args.req)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
			assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
			assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
			assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
			assertFeatureFlag(t, tt.want.Actions, got.Actions)
		})
	}
}

func TestServer_SetInstanceFeatures(t *testing.T) {
	type args struct {
		ctx context.Context
		req *feature.SetInstanceFeaturesRequest
	}
	tests := []struct {
		name    string
		args    args
		want    *feature.SetInstanceFeaturesResponse
		wantErr bool
	}{
		{
			name: "permission error",
			args: args{
				ctx: OrgCTX,
				req: &feature.SetInstanceFeaturesRequest{
					OidcTriggerIntrospectionProjections: gu.Ptr(true),
				},
			},
			wantErr: true,
		},
		{
			name: "no changes error",
			args: args{
				ctx: IamCTX,
				req: &feature.SetInstanceFeaturesRequest{},
			},
			wantErr: true,
		},
		{
			name: "success",
			args: args{
				ctx: IamCTX,
				req: &feature.SetInstanceFeaturesRequest{
					OidcTriggerIntrospectionProjections: gu.Ptr(true),
				},
			},
			want: &feature.SetInstanceFeaturesResponse{
				Details: &object.Details{
					ChangeDate:    timestamppb.Now(),
					ResourceOwner: Instance.ID(),
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(func() {
				// make sure we have a clean state after each test
				_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
				require.NoError(t, err)
			})
			got, err := Client.SetInstanceFeatures(tt.args.ctx, tt.args.req)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			integration.AssertDetails(t, tt.want, got)
		})
	}
}

func TestServer_ResetInstanceFeatures(t *testing.T) {
	_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
		LoginDefaultOrg: gu.Ptr(true),
	})
	require.NoError(t, err)

	tests := []struct {
		name    string
		ctx     context.Context
		want    *feature.ResetInstanceFeaturesResponse
		wantErr bool
	}{
		{
			name:    "permission error",
			ctx:     OrgCTX,
			wantErr: true,
		},
		{
			name: "success",
			ctx:  IamCTX,
			want: &feature.ResetInstanceFeaturesResponse{
				Details: &object.Details{
					ChangeDate:    timestamppb.Now(),
					ResourceOwner: Instance.ID(),
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := Client.ResetInstanceFeatures(tt.ctx, &feature.ResetInstanceFeaturesRequest{})
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			integration.AssertDetails(t, tt.want, got)
		})
	}
}

func TestServer_GetInstanceFeatures(t *testing.T) {
	_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
		OidcLegacyIntrospection: gu.Ptr(true),
	})
	require.NoError(t, err)
	t.Cleanup(func() {
		_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
		require.NoError(t, err)
	})

	type args struct {
		ctx context.Context
		req *feature.GetInstanceFeaturesRequest
	}
	tests := []struct {
		name    string
		prepare func(t *testing.T)
		args    args
		want    *feature.GetInstanceFeaturesResponse
		wantErr bool
	}{
		{
			name: "permission error",
			args: args{
				ctx: OrgCTX,
				req: &feature.GetInstanceFeaturesRequest{},
			},
			wantErr: true,
		},
		{
			name: "defaults, no inheritance",
			args: args{
				ctx: IamCTX,
				req: &feature.GetInstanceFeaturesRequest{},
			},
			want: &feature.GetInstanceFeaturesResponse{},
		},
		{
			name: "defaults, inheritance",
			args: args{
				ctx: IamCTX,
				req: &feature.GetInstanceFeaturesRequest{
					Inheritance: true,
				},
			},
			want: &feature.GetInstanceFeaturesResponse{
				LoginDefaultOrg: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
				OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
				OidcLegacyIntrospection: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_SYSTEM,
				},
				UserSchema: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
				Actions: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
			},
		},
		{
			name: "some features, no inheritance",
			prepare: func(t *testing.T) {
				_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
					LoginDefaultOrg:                     gu.Ptr(true),
					OidcTriggerIntrospectionProjections: gu.Ptr(false),
					UserSchema:                          gu.Ptr(true),
					Actions:                             gu.Ptr(true),
				})
				require.NoError(t, err)
			},
			args: args{
				ctx: IamCTX,
				req: &feature.GetInstanceFeaturesRequest{},
			},
			want: &feature.GetInstanceFeaturesResponse{
				LoginDefaultOrg: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_INSTANCE,
				},
				OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_INSTANCE,
				},
				UserSchema: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_INSTANCE,
				},
				Actions: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_INSTANCE,
				},
			},
		},
		{
			name: "one feature, inheritance",
			prepare: func(t *testing.T) {
				_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
					LoginDefaultOrg: gu.Ptr(true),
				})
				require.NoError(t, err)
			},
			args: args{
				ctx: IamCTX,
				req: &feature.GetInstanceFeaturesRequest{
					Inheritance: true,
				},
			},
			want: &feature.GetInstanceFeaturesResponse{
				LoginDefaultOrg: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_INSTANCE,
				},
				OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
				OidcLegacyIntrospection: &feature.FeatureFlag{
					Enabled: true,
					Source:  feature.Source_SOURCE_SYSTEM,
				},
				UserSchema: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
				Actions: &feature.FeatureFlag{
					Enabled: false,
					Source:  feature.Source_SOURCE_UNSPECIFIED,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Cleanup(func() {
				// make sure we have a clean state after each test
				_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
				require.NoError(t, err)
			})
			if tt.prepare != nil {
				tt.prepare(t)
			}
			got, err := Client.GetInstanceFeatures(tt.args.ctx, tt.args.req)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
			assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
			assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
			assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
		})
	}
}

func assertFeatureFlag(t *testing.T, expected, actual *feature.FeatureFlag) {
	t.Helper()
	assert.Equal(t, expected.GetEnabled(), actual.GetEnabled(), "enabled")
	assert.Equal(t, expected.GetSource(), actual.GetSource(), "source")
}