package system

import (
	"net/netip"
	"testing"

	"github.com/google/go-cmp/cmp/cmpopts"
	containertypes "github.com/moby/moby/api/types/container"
	"github.com/moby/moby/client"
	"github.com/moby/moby/v2/integration/internal/container"
	"github.com/moby/moby/v2/internal/testutil"
	"github.com/moby/moby/v2/internal/testutil/daemon"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/skip"
)

func TestDiskUsage(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType == "windows") // d.Start fails on Windows with `protocol not available`

	t.Parallel()

	ctx := testutil.StartSpan(baseContext, t)

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

	var stepDU client.DiskUsageResult
	for _, step := range []struct {
		doc  string
		next func(t *testing.T, prev client.DiskUsageResult) client.DiskUsageResult
	}{
		{
			doc: "empty",
			next: func(t *testing.T, _ client.DiskUsageResult) client.DiskUsageResult {
				du, err := apiClient.DiskUsage(ctx, client.DiskUsageOptions{
					Images:     true,
					Containers: true,
					BuildCache: true,
					Volumes:    true,
					Verbose:    true,
				})
				assert.NilError(t, err)

				assert.DeepEqual(t, du, client.DiskUsageResult{
					Containers: client.ContainersDiskUsage{},
					Images: client.ImagesDiskUsage{
						TotalSize: 0,
					},
					BuildCache: client.BuildCacheDiskUsage{},
					Volumes:    client.VolumesDiskUsage{},
				})
				return du
			},
		},
		{
			doc: "after LoadBusybox",
			next: func(t *testing.T, _ client.DiskUsageResult) client.DiskUsageResult {
				d.LoadBusybox(ctx, t)

				du, err := apiClient.DiskUsage(ctx, client.DiskUsageOptions{
					Images:     true,
					Containers: true,
					BuildCache: true,
					Volumes:    true,
					Verbose:    true,
				})
				assert.NilError(t, err)

				assert.Equal(t, du.Images.ActiveCount, int64(0))
				assert.Equal(t, du.Images.TotalCount, int64(1))

				assert.Equal(t, du.Images.TotalSize, du.Images.Reclaimable)
				assert.Assert(t, du.Images.TotalSize > 0)
				assert.Equal(t, len(du.Images.Items), 1)
				assert.Equal(t, len(du.Images.Items[0].RepoTags), 1)
				assert.Check(t, is.Equal(du.Images.Items[0].RepoTags[0], "busybox:latest"))

				// With a single image, the aggregate image size matches the image item size.
				assert.Equal(t, du.Images.TotalSize, du.Images.Items[0].Size)

				return du
			},
		},
		{
			doc: "after container.Run",
			next: func(t *testing.T, prev client.DiskUsageResult) client.DiskUsageResult {
				cID := container.Run(ctx, t, apiClient)

				du, err := apiClient.DiskUsage(ctx, client.DiskUsageOptions{
					Images:     true,
					Containers: true,
					BuildCache: true,
					Volumes:    true,
					Verbose:    true,
				})
				assert.NilError(t, err)

				assert.Equal(t, du.Containers.ActiveCount, int64(1))
				assert.Equal(t, du.Containers.TotalCount, int64(1))
				assert.Equal(t, len(du.Containers.Items), 1)
				assert.Equal(t, len(du.Containers.Items[0].Names), 1)
				assert.Assert(t, len(prev.Images.Items) > 0)
				assert.Check(t, du.Containers.Items[0].Created >= prev.Images.Items[0].Created)

				// Additional container layer could add to the size
				assert.Check(t, du.Images.TotalSize >= prev.Images.TotalSize)

				assert.Equal(t, du.Images.ActiveCount, int64(1))
				assert.Equal(t, du.Images.TotalCount, int64(1))
				assert.Equal(t, du.Images.Reclaimable, int64(0))
				assert.Equal(t, len(du.Images.Items), 1)
				assert.Equal(t, du.Images.Items[0].Containers, prev.Images.Items[0].Containers+1)

				assert.Check(t, is.Equal(du.Containers.Items[0].ID, cID))
				assert.Check(t, is.Equal(du.Containers.Items[0].Image, "busybox"))
				assert.Check(t, is.Equal(du.Containers.Items[0].ImageID, prev.Images.Items[0].ID))

				// ImageManifestDescriptor should NOT be populated.
				assert.Check(t, is.Nil(du.Containers.Items[0].ImageManifestDescriptor))

				return du
			},
		},
	} {
		t.Run(step.doc, func(t *testing.T) {
			ctx := testutil.StartSpan(ctx, t)
			stepDU = step.next(t, stepDU)

			for _, tc := range []struct {
				doc      string
				options  client.DiskUsageOptions
				expected client.DiskUsageResult
			}{
				{
					doc: "container types",
					options: client.DiskUsageOptions{
						Containers: true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: stepDU.Containers,
						Images:     client.ImagesDiskUsage{},
						BuildCache: client.BuildCacheDiskUsage{},
						Volumes:    client.VolumesDiskUsage{},
					},
				},
				{
					doc: "image types",
					options: client.DiskUsageOptions{
						Images:  true,
						Verbose: true,
					},
					expected: client.DiskUsageResult{
						Containers: client.ContainersDiskUsage{},
						Images:     stepDU.Images,
						BuildCache: client.BuildCacheDiskUsage{},
						Volumes:    client.VolumesDiskUsage{},
					},
				},
				{
					doc: "volume types",
					options: client.DiskUsageOptions{
						Volumes: true,
						Verbose: true,
					},
					expected: client.DiskUsageResult{
						Containers: client.ContainersDiskUsage{},
						Images:     client.ImagesDiskUsage{},
						BuildCache: client.BuildCacheDiskUsage{},
						Volumes:    stepDU.Volumes,
					},
				},
				{
					doc: "build-cache types",
					options: client.DiskUsageOptions{
						BuildCache: true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: client.ContainersDiskUsage{},
						Images:     client.ImagesDiskUsage{},
						BuildCache: stepDU.BuildCache,
						Volumes:    client.VolumesDiskUsage{},
					},
				},
				{
					doc: "container, volume types",
					options: client.DiskUsageOptions{
						Containers: true,
						Volumes:    true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: stepDU.Containers,
						Images:     client.ImagesDiskUsage{},
						BuildCache: client.BuildCacheDiskUsage{},
						Volumes:    stepDU.Volumes,
					},
				},
				{
					doc: "image, build-cache types",
					options: client.DiskUsageOptions{
						Images:     true,
						BuildCache: true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: client.ContainersDiskUsage{},
						Images:     stepDU.Images,
						BuildCache: stepDU.BuildCache,
						Volumes:    client.VolumesDiskUsage{},
					},
				},
				{
					doc: "container, volume, build-cache types",
					options: client.DiskUsageOptions{
						Containers: true,
						BuildCache: true,
						Volumes:    true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: stepDU.Containers,
						Images:     client.ImagesDiskUsage{},
						BuildCache: stepDU.BuildCache,
						Volumes:    stepDU.Volumes,
					},
				},
				{
					doc: "image, volume, build-cache types",
					options: client.DiskUsageOptions{
						Images:     true,
						BuildCache: true,
						Volumes:    true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: client.ContainersDiskUsage{},
						Images:     stepDU.Images,
						BuildCache: stepDU.BuildCache,
						Volumes:    stepDU.Volumes,
					},
				},
				{
					doc: "container, image, volume types",
					options: client.DiskUsageOptions{
						Containers: true,
						Images:     true,
						Volumes:    true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: stepDU.Containers,
						Images:     stepDU.Images,
						BuildCache: client.BuildCacheDiskUsage{},
						Volumes:    stepDU.Volumes,
					},
				},
				{
					doc: "container, image, volume, build-cache types",
					options: client.DiskUsageOptions{
						Containers: true,
						Images:     true,
						BuildCache: true,
						Volumes:    true,
						Verbose:    true,
					},
					expected: client.DiskUsageResult{
						Containers: stepDU.Containers,
						Images:     stepDU.Images,
						BuildCache: stepDU.BuildCache,
						Volumes:    stepDU.Volumes,
					},
				},
			} {
				t.Run(tc.doc, func(t *testing.T) {
					ctx := testutil.StartSpan(ctx, t)
					// TODO: Run in parallel once https://github.com/moby/moby/pull/42560 is merged.

					du, err := apiClient.DiskUsage(ctx, tc.options)
					assert.NilError(t, err)
					assert.DeepEqual(t, du, tc.expected,
						cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}),
						cmpopts.IgnoreFields(containertypes.Summary{}, "Status"),
					)
				})
			}
		})
	}
}