package repository

import (
	"context"
	"sync"
	"testing"

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

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

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

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		sku  = entity.Sku(123)
		item = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   sku,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item)
	require.NoError(t, err, "check add item error")

	assert.Len(t, repo.storage, 1, "check map length")
	assert.Len(t, repo.storage[uid].Items, 1, "check list length")
	assert.Equal(t, uint32(2), repo.storage[uid].ItemCount[sku], "check count value")
	assert.Equal(t, uid, repo.storage[uid].UserID, "check uid")
}

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

	var (
		repo  = NewInMemoryRepository(10)
		uid   = entity.UID(1337)
		item1 = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   1,
			},
			Count: 1,
		}
		item2 = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   2,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item1)
	require.NoError(t, err, "check add item error")

	err = repo.AddItem(ctx, uid, item2)
	require.NoError(t, err, "check add item error")

	assert.Len(t, repo.storage, 1, "check map length")
	assert.Len(t, repo.storage[uid].Items, 2, "check list length")
	assert.Equal(t, uint32(1), repo.storage[uid].ItemCount[1], "check count value of item 1")
	assert.Equal(t, uint32(2), repo.storage[uid].ItemCount[2], "check count value of item 2")
	assert.Equal(t, uid, repo.storage[uid].UserID, "check uid")
}

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

	var (
		repo  = NewInMemoryRepository(10)
		uid   = entity.UID(1337)
		item1 = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   1,
			},
			Count: 1,
		}
		item2 = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   2,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item1)
	require.NoError(t, err, "check add item error")

	err = repo.AddItem(ctx, uid, item2)
	require.NoError(t, err, "check add item error")

	cart, err := repo.GetItemsByUserID(ctx, uid)
	require.NoError(t, err, "check get items error")

	require.Equal(t, uid, cart.UserID, "check cart uid")
	require.Len(t, cart.Items, 2, "check cart len")
	require.Equal(t, uint32(1), cart.ItemCount[item1.Product.Sku], "check item 1 count")
	require.Equal(t, uint32(2), cart.ItemCount[item2.Product.Sku], "check item 2 count")
}

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		ctx  = context.Background()
	)

	cart, err := repo.GetItemsByUserID(ctx, uid)
	require.NoError(t, err, "check get items error")

	require.Equal(t, entity.Cart{}, cart, "check cart")
}

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		sku  = entity.Sku(123)
		item = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   sku,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item)
	require.NoError(t, err, "check add item error")

	assert.Len(t, repo.storage, 1, "check map length")
	assert.Len(t, repo.storage[uid].Items, 1, "check list length")

	err = repo.DeleteItem(ctx, uid, item.Product.Sku)
	require.NoError(t, err, "check delete item error")

	assert.Len(t, repo.storage[uid].Items, 0, "check list length")
}

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		sku  = entity.Sku(123)
		ctx  = context.Background()
	)

	err := repo.DeleteItem(ctx, uid, sku)
	require.ErrorIs(t, err, model.ErrCartNotFound, "check cart not found err")
}

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		sku  = entity.Sku(123)
		item = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   sku,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item)
	require.NoError(t, err, "check add item error")

	assert.Len(t, repo.storage, 1, "check map length")
	assert.Len(t, repo.storage[uid].Items, 1, "check list length")

	err = repo.DeleteItem(ctx, uid, 1)
	require.ErrorIs(t, err, model.ErrItemNotFoundInCart, "check delete item error")

	assert.Len(t, repo.storage[uid].Items, 1, "check list length")
}

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		sku  = entity.Sku(123)
		item = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   sku,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item)
	require.NoError(t, err, "check add item error")

	assert.Len(t, repo.storage, 1, "check map length")
	assert.Len(t, repo.storage[uid].Items, 1, "check list length")

	err = repo.DeleteItem(ctx, uid, item.Product.Sku)
	require.NoError(t, err, "check delete item error")

	assert.Len(t, repo.storage[uid].Items, 0, "check list length")

	err = repo.DeleteItem(ctx, uid, item.Product.Sku)
	require.NoError(t, err, "check empty cart no err")
}

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

	var (
		repo = NewInMemoryRepository(10)
		uid  = entity.UID(1337)
		sku  = entity.Sku(123)
		item = &model.Item{
			Product: &model.Product{
				Name:  "",
				Price: 0,
				Sku:   sku,
			},
			Count: 2,
		}
		ctx = context.Background()
	)

	err := repo.AddItem(ctx, uid, item)
	require.NoError(t, err, "check add item error")

	assert.Len(t, repo.storage, 1, "check map length")
	assert.Len(t, repo.storage[uid].Items, 1, "check list length")

	err = repo.DeleteItemsByUserID(ctx, uid)
	require.NoError(t, err, "check delete items error")

	assert.Len(t, repo.storage, 0, "check storage length")
}

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

	const (
		goroutines = 100
		userID     = entity.UID(42)
		sku        = entity.Sku(777)
	)

	repo := NewInMemoryRepository(1)
	ctx := context.Background()

	var wg sync.WaitGroup

	wg.Add(goroutines)

	for i := 0; i < goroutines; i++ {
		go func() {
			defer wg.Done()

			item := &model.Item{
				Product: &model.Product{Sku: sku},
				Count:   1,
			}
			require.NoError(t, repo.AddItem(ctx, userID, item))
		}()
	}

	wg.Wait()

	cart, err := repo.GetItemsByUserID(ctx, userID)
	require.NoError(t, err)

	assert.Len(t, cart.Items, 1, "only one item should exist")
	assert.Equal(t, uint32(goroutines), cart.ItemCount[sku], "wrong count")
}

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

	const (
		users       = 50
		addsPerUser = 10
	)

	repo := NewInMemoryRepository(users)
	ctx := context.Background()

	var wg sync.WaitGroup

	for u := 0; u < users; u++ {
		uid := entity.UID(u)
		wg.Add(1)

		go func(id entity.UID) {
			defer wg.Done()

			for i := 0; i < addsPerUser; i++ {
				item := &model.Item{
					Product: &model.Product{Sku: entity.Sku(i)},
					Count:   1,
				}
				require.NoError(t, repo.AddItem(ctx, id, item))
			}
		}(uid)
	}

	wg.Wait()

	for u := 0; u < users; u++ {
		uid := entity.UID(u)
		cart, err := repo.GetItemsByUserID(ctx, uid)
		require.NoError(t, err)

		assert.Lenf(t, cart.Items, addsPerUser, "user %d has wrong item count", u)
		for i := 0; i < addsPerUser; i++ {
			sku := entity.Sku(i)
			assert.Equalf(t, uint32(1), cart.ItemCount[sku], "user %d: sku %d count", u, sku)
		}
	}
}

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

	repo := NewInMemoryRepository(1)
	ctx := context.Background()
	uid := entity.UID(1)
	sku := entity.Sku(9)

	const (
		writerGoroutines = 20
		readerGoroutines = 20
		iterations       = 100
	)

	var wg sync.WaitGroup

	for i := 0; i < writerGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				item := &model.Item{
					Product: &model.Product{Sku: sku},
					Count:   1,
				}
				require.NoError(t, repo.AddItem(ctx, uid, item))
			}
		}()
	}

	for i := 0; i < readerGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < iterations; j++ {
				_, err := repo.GetItemsByUserID(ctx, uid)
				require.NoError(t, err)
			}
		}()
	}

	wg.Wait()

	cart, err := repo.GetItemsByUserID(ctx, uid)
	require.NoError(t, err)
	expected := uint32(writerGoroutines * iterations)

	assert.Equal(t, expected, cart.ItemCount[sku], "wrong item count")
}
