package command

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/muhlemmer/gu"
	"github.com/stretchr/testify/assert"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/id"
	"github.com/zitadel/zitadel/internal/id/mock"
	"github.com/zitadel/zitadel/internal/repository/user/schema"
	"github.com/zitadel/zitadel/internal/zerrors"
)

func TestCommands_CreateUserSchema(t *testing.T) {
	type fields struct {
		eventstore  func(t *testing.T) *eventstore.Eventstore
		idGenerator id.Generator
	}
	type args struct {
		ctx        context.Context
		userSchema *CreateUserSchema
	}
	type res struct {
		id      string
		details *domain.ObjectDetails
		err     error
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"no type, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx:        authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-DGFj3", "Errors.UserSchema.Type.Missing"),
			},
		},
		{
			"no schema, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					Type: "type",
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
			},
		},
		{
			"invalid authenticator, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					Type:   "type",
					Schema: json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUnspecified,
					},
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-Gh652", "Errors.UserSchema.Authenticator.Invalid"),
			},
		},
		{
			"no resourceOwner, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					Type:   "type",
					Schema: json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing"),
			},
		},
		{
			"empty user schema created",
			fields{
				eventstore: expectEventstore(
					expectPush(
						schema.NewCreatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							"type",
							json.RawMessage(`{}`),
							[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					ResourceOwner: "instanceID",
					Type:          "type",
					Schema:        json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				id: "id1",
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
		{
			"user schema created",
			fields{
				eventstore: expectEventstore(
					expectPush(
						schema.NewCreatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							"type",
							json.RawMessage(`{
								"$schema": "urn:zitadel:schema:v1",
								"type": "object",
								"properties": {
									"name": {
										"type": "string"
									}
								}
							}`),
							[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					ResourceOwner: "instanceID",
					Type:          "type",
					Schema: json.RawMessage(`{
						"$schema": "urn:zitadel:schema:v1",
						"type": "object",
						"properties": {
							"name": {
								"type": "string"
							}
						}
					}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				id: "id1",
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
		{
			"user schema with invalid permission, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					ResourceOwner: "instanceID",
					Type:          "type",
					Schema: json.RawMessage(`{
						"$schema": "urn:zitadel:schema:v1",
						"type": "object",
						"properties": {
							"name": {
								"type": "string",
								"urn:zitadel:schema:permission": true
							}
						}
					}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
			},
		},
		{
			"user schema with permission created",
			fields{
				eventstore: expectEventstore(
					expectPush(
						schema.NewCreatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							"type",
							json.RawMessage(`{
								"$schema": "urn:zitadel:schema:v1",
								"type": "object",
								"properties": {
									"name": {
										"type": "string",
										"urn:zitadel:schema:permission": {
											"self": "rw"
										}
									}
								}
							}`),
							[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &CreateUserSchema{
					ResourceOwner: "instanceID",
					Type:          "type",
					Schema: json.RawMessage(`{
						"$schema": "urn:zitadel:schema:v1",
						"type": "object",
						"properties": {
							"name": {
								"type": "string",
								"urn:zitadel:schema:permission": {
									"self": "rw"
								}
							}
						}
					}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				id: "id1",
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore:  tt.fields.eventstore(t),
				idGenerator: tt.fields.idGenerator,
			}
			gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema)
			assert.Equal(t, tt.res.id, gotID)
			assert.Equal(t, tt.res.details, gotDetails)
			assert.ErrorIs(t, err, tt.res.err)
		})
	}
}

func TestCommands_UpdateUserSchema(t *testing.T) {
	type fields struct {
		eventstore func(t *testing.T) *eventstore.Eventstore
	}
	type args struct {
		ctx        context.Context
		userSchema *UpdateUserSchema
	}
	type res struct {
		details *domain.ObjectDetails
		err     error
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"missing id, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx:        authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing"),
			},
		},
		{
			"empty type, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID:   "id1",
					Type: gu.Ptr(""),
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-G43gn", "Errors.UserSchema.Type.Missing"),
			},
		},
		{
			"no schema, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID: "id1",
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
			},
		},
		{
			"invalid schema, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID: "id1",
					Schema: json.RawMessage(`{
						"properties": {
							"name": {
								"type":     "string",
								"required": true,
							}
						}
					}`),
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid"),
			},
		},
		{
			"invalid authenticator, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID:     "id1",
					Schema: json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUnspecified,
					},
				},
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-WF4hg", "Errors.UserSchema.Authenticator.Invalid"),
			},
		},
		{
			"not active / exists, error",
			fields{
				eventstore: expectEventstore(
					expectFilter(),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID:     "id1",
					Type:   gu.Ptr("type"),
					Schema: json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				err: zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive"),
			},
		},
		{
			"no changes",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID:     "id1",
					Type:   gu.Ptr("type"),
					Schema: json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
					},
				},
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
		{
			"update type",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
					),
					expectPush(
						schema.NewUpdatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							[]schema.Changes{schema.ChangeSchemaType("type", "newType")},
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID:     "id1",
					Schema: json.RawMessage(`{}`),
					Type:   gu.Ptr("newType"),
				},
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
		{
			"update schema",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{
									"$schema": "urn:zitadel:schema:v1",
									"type": "object",
									"properties": {
										"name": {
											"type": "string",
											"urn:zitadel:schema:permission": {
												"self": "rw"
											}
										}
									}
								}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
					),
					expectPush(
						schema.NewUpdatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							[]schema.Changes{schema.ChangeSchema(json.RawMessage(`{
								"$schema": "urn:zitadel:schema:v1",
								"type": "object",
								"properties": {
									"name": {
										"type": "string",
										"urn:zitadel:schema:permission": {
											"self": "rw"
										}
									},
									"description": {
										"type": "string",
										"urn:zitadel:schema:permission": {
											"self": "rw"
										}
									}
								}
							}`))},
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID: "id1",
					Schema: json.RawMessage(`{
						"$schema": "urn:zitadel:schema:v1",
						"type": "object",
						"properties": {
							"name": {
								"type": "string",
								"urn:zitadel:schema:permission": {
									"self": "rw"
								}
							},
							"description": {
								"type": "string",
								"urn:zitadel:schema:permission": {
									"self": "rw"
								}
							}
						}
					}`),
					Type: nil,
				},
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
		{
			"update possible authenticators",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
					),
					expectPush(
						schema.NewUpdatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							[]schema.Changes{schema.ChangePossibleAuthenticators([]domain.AuthenticatorType{
								domain.AuthenticatorTypeUsername,
								domain.AuthenticatorTypePassword,
							})},
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				userSchema: &UpdateUserSchema{
					ID:     "id1",
					Schema: json.RawMessage(`{}`),
					PossibleAuthenticators: []domain.AuthenticatorType{
						domain.AuthenticatorTypeUsername,
						domain.AuthenticatorTypePassword,
					},
				},
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore(t),
			}
			got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema)
			assert.ErrorIs(t, err, tt.res.err)
			assert.Equal(t, tt.res.details, got)
		})
	}
}

func TestCommands_DeactivateUserSchema(t *testing.T) {
	type fields struct {
		eventstore func(t *testing.T) *eventstore.Eventstore
	}
	type args struct {
		ctx           context.Context
		id            string
		resourceOwner string
	}
	type res struct {
		details *domain.ObjectDetails
		err     error
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"missing id, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "",
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing"),
			},
		},
		{
			"not active / exists, error",
			fields{
				eventstore: expectEventstore(
					expectFilter(),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "id1",
			},
			res{
				err: zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive"),
			},
		},
		{
			"deactivate ok",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
					),
					expectPush(
						schema.NewDeactivatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "id1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore(t),
			}
			got, err := c.DeactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
			assert.ErrorIs(t, err, tt.res.err)
			assert.Equal(t, tt.res.details, got)
		})
	}
}

func TestCommands_ReactivateUserSchema(t *testing.T) {
	type fields struct {
		eventstore func(t *testing.T) *eventstore.Eventstore
	}
	type args struct {
		ctx           context.Context
		id            string
		resourceOwner string
	}
	type res struct {
		details *domain.ObjectDetails
		err     error
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"missing id, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "",
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing"),
			},
		},
		{
			"not deactivated / exists, error",
			fields{
				eventstore: expectEventstore(
					expectFilter(),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "id1",
			},
			res{
				err: zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive"),
			},
		},
		{
			"reactivate ok",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
						eventFromEventPusher(
							schema.NewDeactivatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
							),
						),
					),
					expectPush(
						schema.NewReactivatedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "id1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore(t),
			}
			got, err := c.ReactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
			assert.ErrorIs(t, err, tt.res.err)
			assert.Equal(t, tt.res.details, got)
		})
	}
}

func TestCommands_DeleteUserSchema(t *testing.T) {
	type fields struct {
		eventstore func(t *testing.T) *eventstore.Eventstore
	}
	type args struct {
		ctx           context.Context
		id            string
		resourceOwner string
	}
	type res struct {
		details *domain.ObjectDetails
		err     error
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"missing id, error",
			fields{
				eventstore: expectEventstore(),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "",
			},
			res{
				err: zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing"),
			},
		},
		{
			"not exists, error",
			fields{
				eventstore: expectEventstore(
					expectFilter(),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "id1",
			},
			res{
				err: zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists"),
			},
		},
		{
			"delete ok",
			fields{
				eventstore: expectEventstore(
					expectFilter(
						eventFromEventPusher(
							schema.NewCreatedEvent(
								context.Background(),
								&schema.NewAggregate("id1", "instanceID").Aggregate,
								"type",
								json.RawMessage(`{}`),
								[]domain.AuthenticatorType{domain.AuthenticatorTypeUsername},
							),
						),
					),
					expectPush(
						schema.NewDeletedEvent(
							context.Background(),
							&schema.NewAggregate("id1", "instanceID").Aggregate,
							"type",
						),
					),
				),
			},
			args{
				ctx: authz.NewMockContext("instanceID", "", ""),
				id:  "id1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "instanceID",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore(t),
			}
			got, err := c.DeleteUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
			assert.ErrorIs(t, err, tt.res.err)
			assert.Equal(t, tt.res.details, got)
		})
	}
}