package service

import (
	"context"
	"errors"
	"testing"

	"github.com/gojuno/minimock/v3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"route256/cart/internal/domain/entity"
	"route256/cart/internal/domain/model"
	"route256/cart/internal/domain/service/mock"
)

const (
	validPrice = 123
	validName  = "some product name"
)

type productServiceFake struct{}

func (f *productServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) {
	if sku%2 == 0 {
		return nil, errors.New("empty shelf")
	}

	return &model.Product{
		Name:  validName,
		Price: validPrice,
		Sku:   sku,
	}, nil
}

type lomsServiceFake struct{}

func (f *lomsServiceFake) StocksInfo(_ context.Context, sku entity.Sku) (uint32, error) {
	if sku == 1111 {
		return 0, errors.New("stock error")
	}

	return 100, nil
}

func (f *lomsServiceFake) OrderCreate(_ context.Context, cart *model.Cart) (int64, error) {
	if cart.UserID == 1111 {
		return 0, errors.New("order create error")
	}

	return 1234, nil
}

func TestCartService_AddItem(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	mc := minimock.NewController(t)

	testSKU := entity.Sku(199)
	testItem := model.Item{
		Product: &model.Product{
			Name:  validName,
			Price: 123,
			Sku:   testSKU,
		},
		Count: 1,
	}

	testSKULomsFailing := entity.Sku(1111)
	testItemLomsFailing := model.Item{
		Product: &model.Product{
			Name:  validName,
			Price: validPrice,
			Sku:   testSKULomsFailing,
		},
		Count: 1,
	}

	type fields struct {
		repository     Repository
		productService ProductService
		lomsService    LomsService
	}
	type args struct {
		ctx    context.Context
		item   *model.Item
		userID entity.UID
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					AddItemMock.
					Expect(ctx, 1337, &testItem).
					Return(nil),
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args: args{
				ctx:    ctx,
				item:   &testItem,
				userID: 1337,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid item",
			fields: fields{
				repository:     nil,
				productService: nil,
			},
			args: args{
				ctx: ctx,
				item: &model.Item{
					Product: &model.Product{
						Name:  "name",
						Price: 123,
						Sku:   0,
					},
					Count: 0,
				},
				userID: 0,
			},
			wantErr: require.Error,
		},
		{
			name: "invalid user id",
			fields: fields{
				repository:     nil,
				productService: nil,
			},
			args: args{
				ctx:    ctx,
				item:   &testItem,
				userID: 0,
			},
			wantErr: require.Error,
		},
		{
			name: "product service error",
			fields: fields{
				repository:     nil,
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args: args{
				ctx: ctx,
				item: &model.Item{
					Product: &model.Product{
						Name:  "",
						Price: 0,
						Sku:   4,
					},
					Count: 1,
				},
				userID: 1337,
			},
			wantErr: require.Error,
		},
		{
			name: "repository error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					AddItemMock.
					Expect(ctx, 1337, &testItem).
					Return(assert.AnError),
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args: args{
				ctx:    ctx,
				item:   &testItem,
				userID: 1337,
			},
			wantErr: require.Error,
		},
		{
			name: "stocks acquiring error",
			fields: fields{
				repository:     nil,
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args: args{
				ctx:    ctx,
				item:   &testItemLomsFailing,
				userID: 1337,
			},
			wantErr: require.Error,
		},
		{
			name: "not enough stocks",
			fields: fields{
				repository:     nil,
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args: args{
				ctx: ctx,
				item: &model.Item{
					Product: &model.Product{
						Name:  validName,
						Price: validPrice,
						Sku:   testSKU,
					},
					Count: 10000,
				},
				userID: 1337,
			},
			wantErr: require.Error,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			s := &CartService{
				repository:     tt.fields.repository,
				productService: tt.fields.productService,
				lomsService:    tt.fields.lomsService,
			}

			err := s.AddItem(tt.args.ctx, tt.args.userID, tt.args.item)
			tt.wantErr(t, err, "check add review error")
		})
	}
}

func TestCartService_GetItemsByUserID(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	mc := minimock.NewController(t)

	testUID := entity.UID(1337)
	testSKU := entity.Sku(199)
	testModelCart := model.Cart{
		UserID: testUID,
		Items: []*model.Item{
			{
				Product: &model.Product{
					Name:  validName,
					Price: validPrice,
					Sku:   testSKU,
				},
				Count: 1,
			},
		},
		TotalPrice: validPrice,
	}

	testEntityCart := entity.Cart{
		UserID:    testUID,
		Items:     []entity.Sku{testSKU},
		ItemCount: map[entity.Sku]uint32{testSKU: 1},
	}

	type fields struct {
		repository     Repository
		productService ProductService
	}
	type args struct {
		ctx    context.Context
		userID entity.UID
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *model.Cart
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success 1 item",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(testEntityCart, nil),
				productService: &productServiceFake{},
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want:    &testModelCart,
			wantErr: require.NoError,
		},
		{
			name: "success 2 items",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(entity.Cart{
						UserID:    testUID,
						Items:     []entity.Sku{testSKU},
						ItemCount: map[entity.Sku]uint32{testSKU: 2},
					}, nil),
				productService: &productServiceFake{},
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want: &model.Cart{
				UserID: testUID,
				Items: []*model.Item{
					{
						Product: &model.Product{
							Name:  validName,
							Price: validPrice,
							Sku:   testSKU,
						},
						Count: 2,
					},
				},
				TotalPrice: validPrice * 2,
			},
			wantErr: require.NoError,
		},
		{
			name: "success 2 different items",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(entity.Cart{
						UserID:    testUID,
						Items:     []entity.Sku{testSKU, 1},
						ItemCount: map[entity.Sku]uint32{testSKU: 1, 1: 1},
					}, nil),
				productService: &productServiceFake{},
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want: &model.Cart{
				UserID: testUID,
				Items: []*model.Item{
					{
						Product: &model.Product{
							Name:  validName,
							Price: validPrice,
							Sku:   1,
						},
						Count: 1,
					},
					{
						Product: &model.Product{
							Name:  validName,
							Price: validPrice,
							Sku:   testSKU,
						},
						Count: 1,
					},
				},
				TotalPrice: validPrice * 2,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid user id",
			fields: fields{
				repository:     nil,
				productService: nil,
			},
			args: args{
				ctx:    ctx,
				userID: 0,
			},
			want:    nil,
			wantErr: require.Error,
		},
		{
			name: "repository error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(entity.Cart{}, assert.AnError),
				productService: nil,
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want:    nil,
			wantErr: require.Error,
		},
		{
			name: "empty cart",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(entity.Cart{}, nil),
				productService: nil,
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want:    nil,
			wantErr: require.Error,
		},
		{
			name: "product service error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(entity.Cart{
						UserID:    testUID,
						Items:     []entity.Sku{2},
						ItemCount: map[entity.Sku]uint32{2: 1},
					}, nil),
				productService: &productServiceFake{},
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want:    nil,
			wantErr: require.Error,
		},
		{
			name: "product service error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(entity.Cart{
						UserID:    testUID,
						Items:     []entity.Sku{2},
						ItemCount: map[entity.Sku]uint32{2: 1},
					}, nil),
				productService: &productServiceFake{},
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			want:    nil,
			wantErr: require.Error,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			s := &CartService{
				repository:     tt.fields.repository,
				productService: tt.fields.productService,
			}

			got, err := s.GetItemsByUserID(tt.args.ctx, tt.args.userID)

			tt.wantErr(t, err, "check add review error")

			assert.True(t, assert.EqualExportedValues(t, tt.want, got), "got unexpected cart")
		})
	}
}

func TestCartService_DeleteItem(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	mc := minimock.NewController(t)

	testSKU := entity.Sku(199)
	testUID := entity.UID(1337)

	type fields struct {
		repository Repository
	}
	type args struct {
		ctx    context.Context
		userID entity.UID
		sku    entity.Sku
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					DeleteItemMock.
					Expect(ctx, testUID, testSKU).
					Return(nil),
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
				sku:    testSKU,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid user id",
			fields: fields{
				repository: nil,
			},
			args: args{
				ctx:    ctx,
				userID: 0,
				sku:    testSKU,
			},
			wantErr: require.Error,
		},
		{
			name: "invalid sku",
			fields: fields{
				repository: nil,
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
				sku:    0,
			},
			wantErr: require.Error,
		},
		{
			name: "repository error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					DeleteItemMock.
					Expect(ctx, testUID, testSKU).
					Return(assert.AnError),
			},
			args: args{
				ctx:    ctx,
				sku:    testSKU,
				userID: testUID,
			},
			wantErr: require.Error,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			s := &CartService{
				repository: tt.fields.repository,
			}

			err := s.DeleteItem(tt.args.ctx, tt.args.userID, tt.args.sku)
			tt.wantErr(t, err, "check add review error")
		})
	}
}

func TestCartService_DeleteItemsByUserID(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	mc := minimock.NewController(t)

	testUID := entity.UID(1337)

	type fields struct {
		repository Repository
	}
	type args struct {
		ctx    context.Context
		userID entity.UID
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					DeleteItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(nil),
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid user id",
			fields: fields{
				repository: nil,
			},
			args: args{
				ctx:    ctx,
				userID: 0,
			},
			wantErr: require.Error,
		},
		{
			name: "repository error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					DeleteItemsByUserIDMock.
					Expect(ctx, testUID).
					Return(assert.AnError),
			},
			args: args{
				ctx:    ctx,
				userID: testUID,
			},
			wantErr: require.Error,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			s := &CartService{
				repository: tt.fields.repository,
			}

			err := s.DeleteItemsByUserID(tt.args.ctx, tt.args.userID)
			tt.wantErr(t, err, "check add review error")
		})
	}
}

func TestCartService_CheckoutUserCart(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	mc := minimock.NewController(t)

	testSKU := entity.Sku(199)
	testCart := entity.Cart{
		Items:     []entity.Sku{testSKU},
		ItemCount: map[entity.Sku]uint32{testSKU: 1},
	}

	type fields struct {
		repository     Repository
		productService ProductService
		lomsService    LomsService
	}

	type args struct {
		ctx    context.Context
		userID entity.UID
	}

	tests := []struct {
		name        string
		fields      fields
		args        args
		wantOrderID int64
		wantErr     require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, 1337).
					Return(testCart, nil).
					DeleteItemsByUserIDMock.
					Expect(ctx, 1337).
					Return(nil),
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args:        args{ctx: ctx, userID: 1337},
			wantOrderID: 1234,
			wantErr:     require.NoError,
		},
		{
			name:    "invalid user id",
			fields:  fields{},
			args:    args{ctx: ctx, userID: 0},
			wantErr: require.Error,
		},
		{
			name: "get cart error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, 1337).
					Return(entity.Cart{}, assert.AnError),
			},
			args:    args{ctx: ctx, userID: 1337},
			wantErr: require.Error,
		},
		{
			name: "order create error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, 1111).
					Return(testCart, nil),
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args:    args{ctx: ctx, userID: 1111},
			wantErr: require.Error,
		},
		{
			name: "delete order error",
			fields: fields{
				repository: mock.NewRepositoryMock(mc).
					GetItemsByUserIDMock.
					Expect(ctx, 1337).
					Return(testCart, nil).
					DeleteItemsByUserIDMock.
					Expect(ctx, 1337).
					Return(assert.AnError),
				productService: &productServiceFake{},
				lomsService:    &lomsServiceFake{},
			},
			args:    args{ctx: ctx, userID: 1337},
			wantErr: require.Error,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			svc := &CartService{
				repository:     tt.fields.repository,
				productService: tt.fields.productService,
				lomsService:    tt.fields.lomsService,
			}

			orderID, err := svc.CheckoutUserCart(tt.args.ctx, tt.args.userID)
			tt.wantErr(t, err)
			if err == nil {
				require.Equal(t, tt.wantOrderID, orderID)
			}
		})
	}
}
