package system

import (
	"fmt"
	"sync"
	"testing"
	"time"

	"github.com/moby/moby/client"
	"github.com/moby/moby/v2/integration/internal/image"
	"github.com/moby/moby/v2/internal/testutil"
	"github.com/moby/moby/v2/internal/testutil/daemon"
	"github.com/moby/moby/v2/internal/testutil/specialimage"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"gotest.tools/v3/assert"
	"gotest.tools/v3/skip"
)

// TestDiskUsageConcurrentPrune tests that running DiskUsage concurrently with
// image removal does not cause an error.
// Regression test for https://github.com/moby/moby/issues/51978
func TestDiskUsageConcurrentPrune(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot start multiple daemons on windows")
	skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
	skip.If(t, !testEnv.UsingSnapshotter(), "only happens with containerd image store")

	t.Parallel()

	ctx := testutil.StartSpan(baseContext, t)

	d := daemon.New(t)
	d.Start(t, "--iptables=false", "--ip6tables=false")
	defer d.Stop(t)

	apiClient := d.NewClientT(t)

	// Load unique images with multiple layers each.
	// More snapshots = higher chance of hitting the race.
	const numImages = 10
	const layersPerImage = 10
	imageNames := make([]string, 0, numImages)
	for i := range numImages {
		var layers []specialimage.SingleFileLayer
		for j := range layersPerImage {
			layers = append(layers, specialimage.SingleFileLayer{
				Name:    fmt.Sprintf("file-%d-%d", i, j),
				Content: fmt.Appendf(nil, "content-%d-%d", i, j),
			})
		}
		imageName := fmt.Sprintf("test-image-%d:latest", i)
		imageNames = append(imageNames, imageName)
		image.Load(ctx, t, apiClient, func(dir string) (*ocispec.Index, error) {
			return specialimage.MultiLayerCustom(dir, imageName, layers)
		})
	}

	t.Cleanup(func() {
		for _, name := range imageNames {
			_, _ = apiClient.ImageRemove(ctx, name, client.ImageRemoveOptions{Force: true})
		}
	})

	const diskUsageCount = 5
	var wg sync.WaitGroup
	errCh := make(chan error, diskUsageCount)
	removeStarted := make(chan struct{})

	for range diskUsageCount {
		wg.Go(func() {
			<-removeStarted
			time.Sleep(time.Millisecond)
			_, err := apiClient.DiskUsage(ctx, client.DiskUsageOptions{
				Images: true,
			})
			if err != nil {
				errCh <- err
			}
		})
	}

	wg.Go(func() {
		close(removeStarted)
		for _, name := range imageNames {
			_, _ = apiClient.ImageRemove(ctx, name, client.ImageRemoveOptions{Force: true})
		}
	})

	wg.Wait()
	close(errCh)

	for err := range errCh {
		assert.NilError(t, err, "DiskUsage should not error when images are being removed concurrently")
	}
}