package service

import (
	"context"
	"errors"
	"testing"

	"github.com/gojuno/minimock/v3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"

	"route256/loms/internal/domain/entity"
	"route256/loms/internal/domain/model"
	mock "route256/loms/internal/domain/service/mock"

	pb "route256/pkg/api/loms/v1"
)

const (
	testUser = entity.ID(1337)
	testSku  = entity.Sku(199)
)

type mockTxManager struct{}

func (t *mockTxManager) WriteWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error) {
	return fn(ctx)
}

func (t *mockTxManager) ReadWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error) {
	return fn(ctx)
}

func (t *mockTxManager) WriteWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error) {
	return fn(ctx)
}

func (t *mockTxManager) ReadWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error) {
	return fn(ctx)
}

type mockKafkaProducer struct{}

func (kp mockKafkaProducer) Send(_ context.Context, _ entity.ID, _ string) error {
	return nil
}

func TestMain(m *testing.M) {
	goleak.VerifyTestMain(m)
}

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

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

	goodReq := &pb.OrderCreateRequest{
		UserId: int64(testUser),
		Items: []*pb.OrderItem{
			{
				Sku:   int64(testSku),
				Count: 2,
			},
		},
	}

	badItemReq := &pb.OrderCreateRequest{
		UserId: int64(testUser),
		Items: []*pb.OrderItem{
			{
				Sku:   0,
				Count: 0,
			},
		},
	}

	type fields struct {
		orders OrderRepository
		stocks StockRepository
	}
	type args struct {
		req *pb.OrderCreateRequest
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderCreateMock.Return(1, nil).
					OrderSetStatusMock.Return(nil),
				stocks: mock.NewStockRepositoryMock(mc).
					StockReserveMock.Return(nil),
			},
			args: args{
				req: goodReq,
			},
			wantErr: require.NoError,
		},
		{
			name:   "invalid input",
			fields: fields{},
			args: args{
				req: &pb.OrderCreateRequest{
					UserId: 0,
				},
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrInvalidInput)
			},
		},
		{
			name:   "invalid input with bad items",
			fields: fields{},
			args: args{
				req: badItemReq,
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrInvalidInput)
			},
		},
		{
			name: "order create error",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderCreateMock.Return(0, errors.New("order create error")),
				stocks: nil,
			},
			args: args{
				req: goodReq,
			},
			wantErr: require.Error,
		},
		{
			name: "stock reserve error",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderCreateMock.Return(1, nil).
					OrderSetStatusMock.Return(errors.New("status update error")),
				stocks: mock.NewStockRepositoryMock(mc),
			},
			args: args{
				req: goodReq,
			},
			wantErr: require.Error,
		},
		{
			name: "final status update error",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderCreateMock.Return(1, nil).
					OrderSetStatusMock.Return(errors.New("unexpected error")),
				stocks: mock.NewStockRepositoryMock(mc),
			},
			args: args{
				req: goodReq,
			},
			wantErr: require.Error,
		},
	}

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

			svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
			_, err := svc.OrderCreate(ctx, tt.args.req)
			tt.wantErr(t, err)
		})
	}
}

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

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

	awaitingOrder := &entity.Order{
		OrderID: 1,
		Status:  pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(),
		Items: []entity.OrderItem{{
			ID:    testSku,
			Count: 2,
		}},
	}

	payedOrder := &entity.Order{OrderID: 2, Status: pb.OrderStatus_ORDER_STATUS_PAYED.String()}
	badStatusOrder := &entity.Order{OrderID: 3, Status: pb.OrderStatus_ORDER_STATUS_FAILED.String()}

	type fields struct {
		orders OrderRepository
		stocks StockRepository
	}
	type args struct {
		id entity.ID
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(awaitingOrder, nil).
					OrderSetStatusMock.Return(nil),
				stocks: mock.NewStockRepositoryMock(mc).
					StockReserveRemoveMock.Return(nil),
			},
			args: args{
				id: awaitingOrder.OrderID,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid input",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc),
				stocks: mock.NewStockRepositoryMock(mc),
			},
			args: args{
				id: 0,
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrInvalidInput)
			},
		},
		{
			name: "already payed",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(payedOrder, nil),
			},
			args: args{
				id: payedOrder.OrderID,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid status",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(badStatusOrder, nil),
			},
			args: args{
				id: badStatusOrder.OrderID,
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrOrderInvalidStatus)
			},
		},
		{
			name: "unexpected logged error on updating stocks reserves",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(awaitingOrder, nil).
					OrderSetStatusMock.Return(nil),
				stocks: mock.NewStockRepositoryMock(mc).
					StockReserveRemoveMock.Return(errors.New("unexpected error")),
			},
			args: args{
				id: badStatusOrder.OrderID,
			},
			wantErr: require.NoError,
		},
	}

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

			svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
			err := svc.OrderPay(ctx, tt.args.id)
			tt.wantErr(t, err)
		})
	}
}

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

	svc := NewLomsService(
		nil,
		nil,
		&mockTxManager{},
		&mockKafkaProducer{},
	)

	_, err := svc.OrderInfo(context.Background(), 0)
	require.ErrorIs(t, err, model.ErrInvalidInput)
}

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

	order := &entity.Order{
		OrderID: 123,
		Status:  "payed",
		UserID:  1337,
		Items:   []entity.OrderItem{},
	}

	mc := minimock.NewController(t)
	svc := NewLomsService(
		mock.NewOrderRepositoryMock(mc).OrderGetByIDMock.Return(order, nil),
		nil,
		&mockTxManager{},
		&mockKafkaProducer{},
	)

	gotOrder, err := svc.OrderInfo(context.Background(), 123)
	require.NoError(t, err)
	require.Equal(t, order, gotOrder)
}

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

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

	awaiting := &entity.Order{
		OrderID: 1,
		Status:  pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(),
		Items: []entity.OrderItem{{
			ID: testSku, Count: 1,
		}},
	}
	cancelled := &entity.Order{
		OrderID: 2,
		Status:  pb.OrderStatus_ORDER_STATUS_CANCELLED.String(),
	}
	payed := &entity.Order{
		OrderID: 3,
		Status:  pb.OrderStatus_ORDER_STATUS_PAYED.String(),
	}

	type fields struct {
		orders OrderRepository
		stocks StockRepository
	}
	type args struct {
		id entity.ID
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(awaiting, nil).
					OrderSetStatusMock.Return(nil),
				stocks: mock.NewStockRepositoryMock(mc).
					StockCancelMock.Return(nil),
			},
			args: args{
				id: awaiting.OrderID,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid input",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc),
				stocks: mock.NewStockRepositoryMock(mc),
			},
			args: args{
				id: 0,
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrInvalidInput)
			},
		},
		{
			name: "already cancelled",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(cancelled, nil),
			},
			args: args{
				id: cancelled.OrderID,
			},
			wantErr: require.NoError,
		},
		{
			name: "invalid status",
			fields: fields{
				orders: mock.NewOrderRepositoryMock(mc).
					OrderGetByIDMock.Return(payed, nil),
			},
			args: args{
				id: payed.OrderID,
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrOrderInvalidStatus)
			},
		},
	}

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

			svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
			err := svc.OrderCancel(ctx, tt.args.id)
			tt.wantErr(t, err)
		})
	}
}

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

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

	type fields struct{ stocks StockRepository }
	type args struct{ sku entity.Sku }

	tests := []struct {
		name    string
		fields  fields
		args    args
		want    uint32
		wantErr require.ErrorAssertionFunc
	}{
		{
			name: "success",
			fields: fields{
				stocks: mock.NewStockRepositoryMock(mc).
					StockGetByIDMock.Return(&entity.Stock{
					Item: entity.OrderItem{
						Count: 7,
					},
				}, nil),
			},
			args: args{
				sku: testSku,
			},
			want:    7,
			wantErr: require.NoError,
		},
		{
			name:   "invalid sku",
			fields: fields{},
			args: args{
				sku: 0,
			},
			wantErr: func(t require.TestingT, err error, _ ...interface{}) {
				require.ErrorIs(t, err, model.ErrInvalidInput)
			},
		},
		{
			name: "get by id error",
			fields: fields{
				stocks: mock.NewStockRepositoryMock(mc).
					StockGetByIDMock.Return(nil, errors.New("unexpected error")),
			},
			args: args{
				sku: testSku,
			},
			wantErr: require.Error,
		},
	}

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

			svc := NewLomsService(nil, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
			got, err := svc.StocksInfo(ctx, tt.args.sku)
			tt.wantErr(t, err)
			if err == nil {
				assert.Equal(t, tt.want, got)
			}
		})
	}
}
