package command

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/domain"
	caos_errs "github.com/zitadel/zitadel/internal/errors"
	"github.com/zitadel/zitadel/internal/eventstore"
	"github.com/zitadel/zitadel/internal/eventstore/repository"
	"github.com/zitadel/zitadel/internal/id"
	id_mock "github.com/zitadel/zitadel/internal/id/mock"
	"github.com/zitadel/zitadel/internal/repository/deviceauth"
)

func TestCommands_AddDeviceAuth(t *testing.T) {
	ctx := authz.WithInstanceID(context.Background(), "instance1")
	idErr := errors.New("idErr")
	pushErr := errors.New("pushErr")
	now := time.Now()

	unique := deviceauth.NewAddUniqueConstraints("client_id", "123", "456")
	require.Len(t, unique, 2)

	type fields struct {
		eventstore  *eventstore.Eventstore
		idGenerator id.Generator
	}
	type args struct {
		ctx        context.Context
		clientID   string
		deviceCode string
		userCode   string
		expires    time.Time
		scopes     []string
	}
	tests := []struct {
		name        string
		fields      fields
		args        args
		wantID      string
		wantDetails *domain.ObjectDetails
		wantErr     error
	}{
		{
			name: "idGenerator error",
			fields: fields{
				eventstore: eventstoreExpect(t),
				idGenerator: func() id.Generator {
					m := id_mock.NewMockGenerator(gomock.NewController(t))
					m.EXPECT().Next().Return("", idErr)
					return m
				}(),
			},
			args: args{
				ctx:        ctx,
				clientID:   "client_id",
				deviceCode: "123",
				userCode:   "456",
				expires:    now,
				scopes:     []string{"a", "b", "c"},
			},
			wantErr: idErr,
		},
		{
			name: "success",
			fields: fields{
				eventstore: eventstoreExpect(t, expectPush(
					[]*repository.Event{
						eventFromEventPusherWithInstanceID("instance1", deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						)),
					},
					uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
					uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
				)),
				idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"),
			},
			args: args{
				ctx:        authz.WithInstanceID(context.Background(), "instance1"),
				clientID:   "client_id",
				deviceCode: "123",
				userCode:   "456",
				expires:    now,
				scopes:     []string{"a", "b", "c"},
			},
			wantID: "1999",
			wantDetails: &domain.ObjectDetails{
				ResourceOwner: "instance1",
			},
		},
		{
			name: "push error",
			fields: fields{
				eventstore: eventstoreExpect(t, expectPushFailed(pushErr,
					[]*repository.Event{
						eventFromEventPusherWithInstanceID("instance1", deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						)),
					},
					uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
					uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
				)),
				idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"),
			},
			args: args{
				ctx:        authz.WithInstanceID(context.Background(), "instance1"),
				clientID:   "client_id",
				deviceCode: "123",
				userCode:   "456",
				expires:    now,
				scopes:     []string{"a", "b", "c"},
			},
			wantErr: pushErr,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore:  tt.fields.eventstore,
				idGenerator: tt.fields.idGenerator,
			}
			gotID, gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes)
			require.ErrorIs(t, err, tt.wantErr)
			assert.Equal(t, gotID, tt.wantID)
			assert.Equal(t, gotDetails, tt.wantDetails)
		})
	}
}

func TestCommands_ApproveDeviceAuth(t *testing.T) {
	ctx := authz.WithInstanceID(context.Background(), "instance1")
	now := time.Now()
	pushErr := errors.New("pushErr")

	type fields struct {
		eventstore *eventstore.Eventstore
	}
	type args struct {
		ctx     context.Context
		id      string
		subject string
	}
	tests := []struct {
		name        string
		fields      fields
		args        args
		wantDetails *domain.ObjectDetails
		wantErr     error
	}{
		{
			name: "not found error",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusherWithInstanceID("instance1",
							deviceauth.NewAddedEvent(
								ctx,
								deviceauth.NewAggregate("1999", "instance1"),
								"client_id", "123", "456", now,
								[]string{"a", "b", "c"},
							),
						),
						eventFromEventPusherWithInstanceID("instance1",
							deviceauth.NewRemovedEvent(
								ctx,
								deviceauth.NewAggregate("1999", "instance1"),
								"client_id", "123", "456",
							),
						),
					),
				),
			},
			args:    args{ctx, "1999", "subj"},
			wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"),
		},
		{
			name: "push error",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPushFailed(pushErr,
						[]*repository.Event{eventFromEventPusherWithInstanceID(
							"instance1", deviceauth.NewApprovedEvent(
								ctx, deviceauth.NewAggregate("1999", "instance1"), "subj",
							),
						)},
					),
				),
			},
			args:    args{ctx, "1999", "subj"},
			wantErr: pushErr,
		},
		{
			name: "success",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPush([]*repository.Event{eventFromEventPusherWithInstanceID(
						"instance1", deviceauth.NewApprovedEvent(
							ctx, deviceauth.NewAggregate("1999", "instance1"), "subj",
						),
					)}),
				),
			},
			args: args{ctx, "1999", "subj"},
			wantDetails: &domain.ObjectDetails{
				ResourceOwner: "instance1",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore,
			}
			gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.subject)
			require.ErrorIs(t, err, tt.wantErr)
			assert.Equal(t, gotDetails, tt.wantDetails)
		})
	}
}

func TestCommands_CancelDeviceAuth(t *testing.T) {
	ctx := authz.WithInstanceID(context.Background(), "instance1")
	now := time.Now()
	pushErr := errors.New("pushErr")

	type fields struct {
		eventstore *eventstore.Eventstore
	}
	type args struct {
		ctx    context.Context
		id     string
		reason domain.DeviceAuthCanceled
	}
	tests := []struct {
		name        string
		fields      fields
		args        args
		wantDetails *domain.ObjectDetails
		wantErr     error
	}{
		{
			name: "not found error",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(
						eventFromEventPusherWithInstanceID("instance1",
							deviceauth.NewAddedEvent(
								ctx,
								deviceauth.NewAggregate("1999", "instance1"),
								"client_id", "123", "456", now,
								[]string{"a", "b", "c"},
							),
						),
						eventFromEventPusherWithInstanceID("instance1",
							deviceauth.NewRemovedEvent(
								ctx,
								deviceauth.NewAggregate("1999", "instance1"),
								"client_id", "123", "456",
							),
						),
					),
				),
			},
			args:    args{ctx, "1999", domain.DeviceAuthCanceledDenied},
			wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound"),
		},
		{
			name: "push error",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPushFailed(pushErr,
						[]*repository.Event{eventFromEventPusherWithInstanceID(
							"instance1", deviceauth.NewCanceledEvent(
								ctx, deviceauth.NewAggregate("1999", "instance1"),
								domain.DeviceAuthCanceledDenied,
							),
						)},
					),
				),
			},
			args:    args{ctx, "1999", domain.DeviceAuthCanceledDenied},
			wantErr: pushErr,
		},
		{
			name: "success/denied",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPush([]*repository.Event{eventFromEventPusherWithInstanceID(
						"instance1", deviceauth.NewCanceledEvent(
							ctx, deviceauth.NewAggregate("1999", "instance1"),
							domain.DeviceAuthCanceledDenied,
						),
					)}),
				),
			},
			args: args{ctx, "1999", domain.DeviceAuthCanceledDenied},
			wantDetails: &domain.ObjectDetails{
				ResourceOwner: "instance1",
			},
		},
		{
			name: "success/expired",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPush([]*repository.Event{eventFromEventPusherWithInstanceID(
						"instance1", deviceauth.NewCanceledEvent(
							ctx, deviceauth.NewAggregate("1999", "instance1"),
							domain.DeviceAuthCanceledExpired,
						),
					)}),
				),
			},
			args: args{ctx, "1999", domain.DeviceAuthCanceledExpired},
			wantDetails: &domain.ObjectDetails{
				ResourceOwner: "instance1",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore,
			}
			gotDetails, err := c.CancelDeviceAuth(tt.args.ctx, tt.args.id, tt.args.reason)
			require.ErrorIs(t, err, tt.wantErr)
			assert.Equal(t, gotDetails, tt.wantDetails)
		})
	}
}

func TestCommands_RemoveDeviceAuth(t *testing.T) {
	ctx := authz.WithInstanceID(context.Background(), "instance1")
	now := time.Now()
	pushErr := errors.New("pushErr")

	unique := deviceauth.NewRemoveUniqueConstraints("client_id", "123", "456")
	require.Len(t, unique, 2)

	type fields struct {
		eventstore *eventstore.Eventstore
	}
	type args struct {
		ctx context.Context
		id  string
	}
	tests := []struct {
		name        string
		fields      fields
		args        args
		wantDetails *domain.ObjectDetails
		wantErr     error
	}{
		{
			name: "push error",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPushFailed(pushErr,
						[]*repository.Event{eventFromEventPusherWithInstanceID(
							"instance1", deviceauth.NewRemovedEvent(
								ctx, deviceauth.NewAggregate("1999", "instance1"),
								"client_id", "123", "456",
							),
						)},
						uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
						uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
					),
				),
			},
			args:    args{ctx, "1999"},
			wantErr: pushErr,
		},
		{
			name: "success",
			fields: fields{
				eventstore: eventstoreExpect(t,
					expectFilter(eventFromEventPusherWithInstanceID(
						"instance1",
						deviceauth.NewAddedEvent(
							ctx,
							deviceauth.NewAggregate("1999", "instance1"),
							"client_id", "123", "456", now,
							[]string{"a", "b", "c"},
						),
					)),
					expectPush(
						[]*repository.Event{eventFromEventPusherWithInstanceID(
							"instance1", deviceauth.NewRemovedEvent(
								ctx, deviceauth.NewAggregate("1999", "instance1"),
								"client_id", "123", "456",
							),
						)},
						uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]),
						uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]),
					),
				),
			},
			args: args{ctx, "1999"},
			wantDetails: &domain.ObjectDetails{
				ResourceOwner: "instance1",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &Commands{
				eventstore: tt.fields.eventstore,
			}
			gotDetails, err := c.RemoveDeviceAuth(tt.args.ctx, tt.args.id)
			require.ErrorIs(t, err, tt.wantErr)
			assert.Equal(t, gotDetails, tt.wantDetails)
		})
	}
}