//go:build linux
// +build linux

package local // import "github.com/docker/docker/volume/local"

import (
	"os"
	"path/filepath"
	"strconv"
	"testing"

	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/pkg/idtools"
	"github.com/docker/docker/quota"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

const quotaSize = 1024 * 1024
const quotaSizeLiteral = "1M"

func TestQuota(t *testing.T) {
	if msg, ok := quota.CanTestQuota(); !ok {
		t.Skip(msg)
	}

	// get sparse xfs test image
	imageFileName, err := quota.PrepareQuotaTestImage(t)
	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove(imageFileName)

	t.Run("testVolWithQuota", quota.WrapMountTest(imageFileName, true, testVolWithQuota))
	t.Run("testVolQuotaUnsupported", quota.WrapMountTest(imageFileName, false, testVolQuotaUnsupported))
}

func testVolWithQuota(t *testing.T, mountPoint, backingFsDev, testDir string) {
	r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
	if err != nil {
		t.Fatal(err)
	}
	assert.Assert(t, r.quotaCtl != nil)

	vol, err := r.Create("testing", map[string]string{"size": quotaSizeLiteral})
	if err != nil {
		t.Fatal(err)
	}

	dir, err := vol.Mount("1234")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := vol.Unmount("1234"); err != nil {
			t.Fatal(err)
		}
	}()

	testfile := filepath.Join(dir, "testfile")

	// test writing file smaller than quota
	assert.NilError(t, os.WriteFile(testfile, make([]byte, quotaSize/2), 0644))
	assert.NilError(t, os.Remove(testfile))

	// test writing fiel larger than quota
	err = os.WriteFile(testfile, make([]byte, quotaSize+1), 0644)
	assert.ErrorContains(t, err, "")
	if _, err := os.Stat(testfile); err == nil {
		assert.NilError(t, os.Remove(testfile))
	}
}

func testVolQuotaUnsupported(t *testing.T, mountPoint, backingFsDev, testDir string) {
	r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
	if err != nil {
		t.Fatal(err)
	}
	assert.Assert(t, is.Nil(r.quotaCtl))

	_, err = r.Create("testing", map[string]string{"size": quotaSizeLiteral})
	assert.ErrorContains(t, err, "no quota support")

	vol, err := r.Create("testing", nil)
	if err != nil {
		t.Fatal(err)
	}

	// this could happen if someone moves volumes from storage with
	// quota support to some place without
	lv, ok := vol.(*localVolume)
	assert.Assert(t, ok)
	lv.opts = &optsConfig{
		Quota: quota.Quota{Size: quotaSize},
	}

	_, err = vol.Mount("1234")
	assert.ErrorContains(t, err, "no quota support")
}

func TestVolCreateValidation(t *testing.T) {
	r, err := New(t.TempDir(), idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
	if err != nil {
		t.Fatal(err)
	}

	mandatoryOpts = map[string][]string{
		"device": {"type"},
		"type":   {"device"},
		"o":      {"device", "type"},
	}

	tests := []struct {
		doc         string
		name        string
		opts        map[string]string
		expectedErr string
	}{
		{
			doc:  "invalid: name too short",
			name: "a",
			opts: map[string]string{
				"type":   "foo",
				"device": "foo",
			},
			expectedErr: `volume name is too short, names should be at least two alphanumeric characters`,
		},
		{
			doc:  "invalid: name invalid characters",
			name: "hello world",
			opts: map[string]string{
				"type":   "foo",
				"device": "foo",
			},
			expectedErr: `"hello world" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path`,
		},
		{
			doc:         "invalid: unknown option",
			opts:        map[string]string{"hello": "world"},
			expectedErr: `invalid option: "hello"`,
		},
		{
			doc:         "invalid: invalid size",
			opts:        map[string]string{"size": "hello"},
			expectedErr: `invalid size: 'hello'`,
		},
		{
			doc:         "invalid: size, but no quotactl",
			opts:        map[string]string{"size": "1234"},
			expectedErr: `quota size requested but no quota support`,
		},
		{
			doc: "invalid: device without type",
			opts: map[string]string{
				"device": "foo",
			},
			expectedErr: `missing required option: "type"`,
		},
		{
			doc: "invalid: type without device",
			opts: map[string]string{
				"type": "foo",
			},
			expectedErr: `missing required option: "device"`,
		},
		{
			doc: "invalid: o without device",
			opts: map[string]string{
				"o":    "foo",
				"type": "foo",
			},
			expectedErr: `missing required option: "device"`,
		},
		{
			doc: "invalid: o without type",
			opts: map[string]string{
				"o":      "foo",
				"device": "foo",
			},
			expectedErr: `missing required option: "type"`,
		},
		{
			doc:  "valid: short name, no options",
			name: "ab",
		},
		{
			doc: "valid: device and type",
			opts: map[string]string{
				"type":   "foo",
				"device": "foo",
			},
		},
		{
			doc: "valid: device, type, and o",
			opts: map[string]string{
				"type":   "foo",
				"device": "foo",
				"o":      "foo",
			},
		},
	}

	for i, tc := range tests {
		tc := tc
		t.Run(tc.doc, func(t *testing.T) {
			if tc.name == "" {
				tc.name = "vol-" + strconv.Itoa(i)
			}
			v, err := r.Create(tc.name, tc.opts)
			if v != nil {
				defer assert.Check(t, r.Remove(v))
			}
			if tc.expectedErr == "" {
				assert.NilError(t, err)
			} else {
				assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T", err)
				assert.ErrorContains(t, err, tc.expectedErr)
			}
		})
	}
}
