package command

import (
	"context"
	"testing"
	"time"

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

	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/eventstore/v1/models"
	"github.com/zitadel/zitadel/internal/id"
	"github.com/zitadel/zitadel/internal/id/mock"
	"github.com/zitadel/zitadel/internal/repository/target"
	"github.com/zitadel/zitadel/internal/zerrors"
)

func TestCommands_AddTarget(t *testing.T) {
	type fields struct {
		eventstore  *eventstore.Eventstore
		idGenerator id.Generator
	}
	type args struct {
		ctx           context.Context
		add           *AddTarget
		resourceOwner string
	}
	type res struct {
		id      string
		details *domain.ObjectDetails
		err     func(error) bool
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"no resourceowner, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx:           context.Background(),
				add:           &AddTarget{},
				resourceOwner: "",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"no name, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx:           context.Background(),
				add:           &AddTarget{},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"no timeout, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name: "name",
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"no url, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name:    "name",
					Timeout: time.Second,
					URL:     "",
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"no parsable url, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name:    "name",
					Timeout: time.Second,
					URL:     "://",
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"unique constraint failed, error",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(),
					expectPushFailed(
						zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
						target.NewAddedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							"name",
							domain.TargetTypeWebhook,
							"https://example.com",
							time.Second,
							false,
							false,
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name:       "name",
					URL:        "https://example.com",
					Timeout:    time.Second,
					TargetType: domain.TargetTypeWebhook,
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsPreconditionFailed,
			},
		},
		{
			"already existing",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						target.NewAddedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							"name",
							domain.TargetTypeWebhook,
							"https://example.com",
							time.Second,
							false,
							false,
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name:       "name",
					TargetType: domain.TargetTypeWebhook,
					Timeout:    time.Second,
					URL:        "https://example.com",
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorAlreadyExists,
			},
		},
		{
			"push ok",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(),
					expectPush(
						target.NewAddedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							"name",
							domain.TargetTypeWebhook,
							"https://example.com",
							time.Second,
							false,
							false,
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name:       "name",
					TargetType: domain.TargetTypeWebhook,
					Timeout:    time.Second,
					URL:        "https://example.com",
				},
				resourceOwner: "org1",
			},
			res{
				id: "id1",
				details: &domain.ObjectDetails{
					ResourceOwner: "org1",
				},
			},
		},
		{
			"push full ok",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(),
					expectPush(
						target.NewAddedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							"name",
							domain.TargetTypeWebhook,
							"https://example.com",
							time.Second,
							true,
							true,
						),
					),
				),
				idGenerator: mock.ExpectID(t, "id1"),
			},
			args{
				ctx: context.Background(),
				add: &AddTarget{
					Name:             "name",
					TargetType:       domain.TargetTypeWebhook,
					URL:              "https://example.com",
					Timeout:          time.Second,
					Async:            true,
					InterruptOnError: true,
				},
				resourceOwner: "org1",
			},
			res{
				id: "id1",
				details: &domain.ObjectDetails{
					ResourceOwner: "org1",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore:  tt.fields.eventstore,
				idGenerator: tt.fields.idGenerator,
			}
			details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
			if tt.res.err == nil {
				assert.NoError(t, err)
			}
			if tt.res.err != nil && !tt.res.err(err) {
				t.Errorf("got wrong err: %v ", err)
			}
			if tt.res.err == nil {
				assert.Equal(t, tt.res.id, tt.args.add.AggregateID)
				assert.Equal(t, tt.res.details, details)
			}
		})
	}
}

func TestCommands_ChangeTarget(t *testing.T) {
	type fields struct {
		eventstore *eventstore.Eventstore
	}
	type args struct {
		ctx           context.Context
		change        *ChangeTarget
		resourceOwner string
	}
	type res struct {
		details *domain.ObjectDetails
		err     func(error) bool
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"resourceowner missing, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx:           context.Background(),
				change:        &ChangeTarget{},
				resourceOwner: "",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"id missing, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx:           context.Background(),
				change:        &ChangeTarget{},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"name empty, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					Name: gu.Ptr(""),
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"timeout empty, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					Timeout: gu.Ptr(time.Duration(0)),
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"url empty, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					URL: gu.Ptr(""),
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"url not parsable, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					URL: gu.Ptr("://"),
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"not found, error",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(),
				),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					ObjectRoot: models.ObjectRoot{
						AggregateID: "id1",
					},
					Name: gu.Ptr("name"),
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsNotFound,
			},
		},
		{
			"no changes",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusher(
							target.NewAddedEvent(context.Background(),
								target.NewAggregate("id1", "org1"),
								"name",
								domain.TargetTypeWebhook,
								"https://example.com",
								0,
								false,
								false,
							),
						),
					),
				),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					ObjectRoot: models.ObjectRoot{
						AggregateID: "id1",
					},
					TargetType: gu.Ptr(domain.TargetTypeWebhook),
				},
				resourceOwner: "org1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "org1",
				},
			},
		},
		{
			"unique constraint failed, error",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusher(
							target.NewAddedEvent(context.Background(),
								target.NewAggregate("id1", "org1"),
								"name",
								domain.TargetTypeWebhook,
								"https://example.com",
								0,
								false,
								false,
							),
						),
					),
					expectPushFailed(
						zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
						target.NewChangedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							[]target.Changes{
								target.ChangeName("name", "name2"),
							},
						),
					),
				),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					ObjectRoot: models.ObjectRoot{
						AggregateID: "id1",
					},
					Name: gu.Ptr("name2"),
				},
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsPreconditionFailed,
			},
		},
		{
			"push ok",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusher(
							target.NewAddedEvent(context.Background(),
								target.NewAggregate("id1", "org1"),
								"name",
								domain.TargetTypeWebhook,
								"https://example.com",
								0,
								false,
								false,
							),
						),
					),
					expectPush(
						target.NewChangedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							[]target.Changes{
								target.ChangeName("name", "name2"),
							},
						),
					),
				),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					ObjectRoot: models.ObjectRoot{
						AggregateID: "id1",
					},
					Name: gu.Ptr("name2"),
				},
				resourceOwner: "org1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "org1",
				},
			},
		},
		{
			"push full ok",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusher(
							target.NewAddedEvent(context.Background(),
								target.NewAggregate("id1", "org1"),
								"name",
								domain.TargetTypeWebhook,
								"https://example.com",
								0,
								false,
								false,
							),
						),
					),
					expectPush(
						target.NewChangedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							[]target.Changes{
								target.ChangeName("name", "name2"),
								target.ChangeURL("https://example2.com"),
								target.ChangeTargetType(domain.TargetTypeRequestResponse),
								target.ChangeTimeout(time.Second),
								target.ChangeAsync(true),
								target.ChangeInterruptOnError(true),
							},
						),
					),
				),
			},
			args{
				ctx: context.Background(),
				change: &ChangeTarget{
					ObjectRoot: models.ObjectRoot{
						AggregateID: "id1",
					},
					Name:             gu.Ptr("name2"),
					URL:              gu.Ptr("https://example2.com"),
					TargetType:       gu.Ptr(domain.TargetTypeRequestResponse),
					Timeout:          gu.Ptr(time.Second),
					Async:            gu.Ptr(true),
					InterruptOnError: gu.Ptr(true),
				},
				resourceOwner: "org1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "org1",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore,
			}
			details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
			if tt.res.err == nil {
				assert.NoError(t, err)
			}
			if tt.res.err != nil && !tt.res.err(err) {
				t.Errorf("got wrong err: %v ", err)
			}
			if tt.res.err == nil {
				assert.Equal(t, tt.res.details, details)
			}
		})
	}
}

func TestCommands_DeleteTarget(t *testing.T) {
	type fields struct {
		eventstore *eventstore.Eventstore
	}
	type args struct {
		ctx           context.Context
		id            string
		resourceOwner string
	}
	type res struct {
		details *domain.ObjectDetails
		err     func(error) bool
	}
	tests := []struct {
		name   string
		fields fields
		args   args
		res    res
	}{
		{
			"id missing, error",
			fields{
				eventstore: eventstoreExpect(t),
			},
			args{
				ctx:           context.Background(),
				id:            "",
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsErrorInvalidArgument,
			},
		},
		{
			"not found, error",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(),
				),
			},
			args{
				ctx:           context.Background(),
				id:            "id1",
				resourceOwner: "org1",
			},
			res{
				err: zerrors.IsNotFound,
			},
		},
		{
			"remove ok",
			fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusher(
							target.NewAddedEvent(context.Background(),
								target.NewAggregate("id1", "org1"),
								"name",
								domain.TargetTypeWebhook,
								"https://example.com",
								0,
								false,
								false,
							),
						),
					),
					expectPush(
						target.NewRemovedEvent(context.Background(),
							target.NewAggregate("id1", "org1"),
							"name",
						),
					),
				),
			},
			args{
				ctx:           context.Background(),
				id:            "id1",
				resourceOwner: "org1",
			},
			res{
				details: &domain.ObjectDetails{
					ResourceOwner: "org1",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore,
			}
			details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
			if tt.res.err == nil {
				assert.NoError(t, err)
			}
			if tt.res.err != nil && !tt.res.err(err) {
				t.Errorf("got wrong err: %v ", err)
			}
			if tt.res.err == nil {
				assert.Equal(t, tt.res.details, details)
			}
		})
	}
}