package testcontainers_test

import (
	"context"
	"testing"

	"github.com/moby/moby/api/types/mount"
	"github.com/moby/moby/client"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/testcontainers/testcontainers-go"
)

func TestVolumeMount(t *testing.T) {
	t.Parallel()
	type args struct {
		volumeName  string
		mountTarget testcontainers.ContainerMountTarget
	}
	tests := []struct {
		name string
		args args
		want testcontainers.ContainerMount
	}{
		{
			name: "sample-data:/data",
			args: args{volumeName: "sample-data", mountTarget: "/data"},
			want: testcontainers.ContainerMount{Source: testcontainers.GenericVolumeMountSource{Name: "sample-data"}, Target: "/data"},
		},
		{
			name: "web:/var/nginx/html",
			args: args{volumeName: "web", mountTarget: "/var/nginx/html"},
			want: testcontainers.ContainerMount{Source: testcontainers.GenericVolumeMountSource{Name: "web"}, Target: "/var/nginx/html"},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			assert.Equalf(t, tt.want, testcontainers.VolumeMount(tt.args.volumeName, tt.args.mountTarget), "VolumeMount(%v, %v)", tt.args.volumeName, tt.args.mountTarget)
		})
	}
}

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

	t.Run("valid-image-mount", func(t *testing.T) {
		t.Parallel()
		m := testcontainers.ImageMount("nginx:latest", "var/www/html", "/var/www/html")
		// the source is a GenericImageMountSource, which does implement the Validator interface
		if v, ok := m.Source.(testcontainers.Validator); ok {
			require.NoError(t, v.Validate())
		}

		require.Equal(t, testcontainers.ContainerMount{
			Source: testcontainers.NewGenericImageMountSource("nginx:latest", "var/www/html"),
			Target: "/var/www/html",
		}, m)
	})

	t.Run("invalid-image-mount", func(t *testing.T) {
		t.Parallel()
		m := testcontainers.ImageMount("nginx:latest", "../var/www/html", "/var/www/invalid")
		// the source is a GenericImageMountSource, which does implement the Validator interface
		if v, ok := m.Source.(testcontainers.Validator); ok {
			require.Error(t, v.Validate())
		}

		require.Equal(t, testcontainers.ContainerMount{
			Source: testcontainers.NewGenericImageMountSource("nginx:latest", "../var/www/html"),
			Target: "/var/www/invalid",
		}, m)
	})
}

func TestContainerMounts_PrepareMounts(t *testing.T) {
	volumeOptions := &mount.VolumeOptions{
		Labels: testcontainers.GenericLabels(),
	}

	expectedLabels := testcontainers.GenericLabels()
	expectedLabels["hello"] = "world"

	t.Parallel()
	tests := []struct {
		name   string
		mounts testcontainers.ContainerMounts
		want   []mount.Mount
	}{
		{
			name:   "Empty",
			mounts: nil,
			want:   []mount.Mount{},
		},
		{
			name:   "Single volume mount",
			mounts: testcontainers.ContainerMounts{{Source: testcontainers.GenericVolumeMountSource{Name: "app-data"}, Target: "/data"}},
			want: []mount.Mount{
				{
					Type:          mount.TypeVolume,
					Source:        "app-data",
					Target:        "/data",
					VolumeOptions: volumeOptions,
				},
			},
		},
		{
			name:   "Single volume mount - read-only",
			mounts: testcontainers.ContainerMounts{{Source: testcontainers.GenericVolumeMountSource{Name: "app-data"}, Target: "/data", ReadOnly: true}},
			want: []mount.Mount{
				{
					Type:          mount.TypeVolume,
					Source:        "app-data",
					Target:        "/data",
					ReadOnly:      true,
					VolumeOptions: volumeOptions,
				},
			},
		},
		{
			name: "Single volume mount - with options",
			mounts: testcontainers.ContainerMounts{
				{
					Source: testcontainers.DockerVolumeMountSource{
						Name: "app-data",
						VolumeOptions: &mount.VolumeOptions{
							NoCopy: true,
							Labels: map[string]string{
								"hello": "world",
							},
						},
					},
					Target: "/data",
				},
			},
			want: []mount.Mount{
				{
					Type:   mount.TypeVolume,
					Source: "app-data",
					Target: "/data",
					VolumeOptions: &mount.VolumeOptions{
						NoCopy: true,
						Labels: expectedLabels,
					},
				},
			},
		},

		{
			name:   "Single tmpfs mount",
			mounts: testcontainers.ContainerMounts{{Source: testcontainers.GenericTmpfsMountSource{}, Target: "/data"}},
			want: []mount.Mount{
				{
					Type:   mount.TypeTmpfs,
					Target: "/data",
				},
			},
		},
		{
			name:   "Single volume mount - read-only",
			mounts: testcontainers.ContainerMounts{{Source: testcontainers.GenericTmpfsMountSource{}, Target: "/data", ReadOnly: true}},
			want: []mount.Mount{
				{
					Type:     mount.TypeTmpfs,
					Target:   "/data",
					ReadOnly: true,
				},
			},
		},
		{
			name: "Single tmpfs mount - with options",
			mounts: testcontainers.ContainerMounts{
				{
					Source: testcontainers.DockerTmpfsMountSource{
						TmpfsOptions: &mount.TmpfsOptions{
							SizeBytes: 50 * 1024 * 1024,
							Mode:      0o644,
						},
					},
					Target: "/data",
				},
			},
			want: []mount.Mount{
				{
					Type:   mount.TypeTmpfs,
					Target: "/data",
					TmpfsOptions: &mount.TmpfsOptions{
						SizeBytes: 50 * 1024 * 1024,
						Mode:      0o644,
					},
				},
			},
		},
		{
			name: "Image mount",
			mounts: testcontainers.ContainerMounts{
				{
					Source: testcontainers.NewDockerImageMountSource("my-custom-image:latest", "data"),
					Target: "/data",
				},
			},
			want: []mount.Mount{
				{
					Source: "my-custom-image:latest",
					Type:   mount.TypeImage,
					Target: "/data",
					ImageOptions: &mount.ImageOptions{
						Subpath: "data",
					},
				},
			},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			assert.Equalf(t, tt.want, tt.mounts.PrepareMounts(), "PrepareMounts()")
		})
	}
}

func TestCreateContainerWithVolume(t *testing.T) {
	// volumeMounts {
	volumeName := "test-volume"
	ctx := context.Background()

	c, err := testcontainers.Run(ctx, "alpine",
		testcontainers.WithMounts(testcontainers.ContainerMount{
			Source: testcontainers.GenericVolumeMountSource{
				Name: volumeName,
			},
			Target: "/data",
		}),
	)
	// }
	testcontainers.CleanupContainer(t, c, testcontainers.RemoveVolumes(volumeName))
	require.NoError(t, err)

	// Check if volume is created
	apiClient, err := testcontainers.NewDockerClientWithOpts(ctx)
	require.NoError(t, err)
	defer apiClient.Close()

	res, err := apiClient.VolumeInspect(ctx, "test-volume", client.VolumeInspectOptions{})
	require.NoError(t, err)
	assert.Equal(t, "test-volume", res.Volume.Name)
}

func TestMountsReceiveRyukLabels(t *testing.T) {
	volumeName := "app-data"
	ctx := context.Background()

	apiClient, err := testcontainers.NewDockerClientWithOpts(ctx)
	require.NoError(t, err)
	defer apiClient.Close()

	// Ensure the volume is removed before creating the container
	// otherwise the volume will be reused and the labels won't be set.
	_, err = apiClient.VolumeRemove(ctx, volumeName, client.VolumeRemoveOptions{
		Force: true,
	})
	require.NoError(t, err)

	c, err := testcontainers.Run(ctx, "alpine",
		testcontainers.WithMounts(testcontainers.ContainerMount{
			Source: testcontainers.GenericVolumeMountSource{
				Name: volumeName,
			},
			Target: "/data",
		}),
	)
	testcontainers.CleanupContainer(t, c, testcontainers.RemoveVolumes(volumeName))
	require.NoError(t, err)

	// Check if volume is created with the expected labels.
	res, err := apiClient.VolumeInspect(ctx, volumeName, client.VolumeInspectOptions{})
	require.NoError(t, err)
	require.Equal(t, testcontainers.GenericLabels(), res.Volume.Labels)
}
