//go:build !nautilus
// +build !nautilus

package admin

import (
	"errors"
	"testing"
	"time"

	"github.com/ceph/go-ceph/internal/commands"
	"github.com/ceph/go-ceph/rbd"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var tr1 = `
{
   "sequence":1,
   "id":"id-1",
   "message":"Removing image pool/test from trash",
   "refs":{
      "action":"trash remove",
      "pool_name":"pool",
      "pool_namespace":"",
      "image_id":"12345"
   },
   "retry_attempts":1,
   "retry_time":"2021-09-13T05:58:24.826408"
}
`

var tr2 = `
{
	"sequence":2,
	"id":"id-2",
	"message":"Removing image pool/test",
	"refs":{
	"action":"remove",
	"pool_name":"pool",
	"pool_namespace":"",
	"image_name":"test",
	"image_id":"123456"
	},
	"in_progress":true,
	"progress":0.70
}
`

var trList = `[
	{
		"sequence":1,
		"id":"id-1",
		"message":"Removing image pool/test from trash",
		"refs":{
		   "action":"trash remove",
		   "pool_name":"pool",
		   "pool_namespace":"",
		   "image_id":"12345"
		},
		"retry_attempts":1,
		"retry_time":"2021-09-13T05:58:24.826408"
	 },
	 {
		"sequence":2,
		"id":"id-2",
		"message":"Removing image pool/test",
		"refs":{
		"action":"remove",
		"pool_name":"pool",
		"pool_namespace":"",
		"image_name":"test",
		"image_id":"123456"
		},
		"in_progress":true,
		"progress":0.70
	}
]`

func TestParseTaskResponse(t *testing.T) {
	type args struct {
		res commands.Response
	}
	tests := []struct {
		name    string
		args    args
		want    TaskResponse
		wantErr bool
	}{
		{
			name: "",
			args: args{
				res: commands.NewResponse([]byte(tr1), "", nil),
			},
			want: TaskResponse{
				Sequence: 1,
				ID:       "id-1",
				Message:  "Removing image pool/test from trash",
				Refs: TaskRefs{
					Action:        "trash remove",
					PoolName:      "pool",
					PoolNamespace: "",
					ImageName:     "",
					ImageID:       "12345",
				},
				InProgress:    false,
				Progress:      0,
				RetryAttempts: 1,
				RetryTime:     "2021-09-13T05:58:24.826408",
				RetryMessage:  "",
			},
			wantErr: false,
		},
		{
			name: "",
			args: args{
				res: commands.NewResponse([]byte(tr2), "", nil),
			},
			want: TaskResponse{
				Sequence: 2,
				ID:       "id-2",
				Message:  "Removing image pool/test",
				Refs: TaskRefs{
					Action:        "remove",
					PoolName:      "pool",
					PoolNamespace: "",
					ImageName:     "test",
					ImageID:       "123456",
				},
				InProgress:    true,
				Progress:      0.70,
				RetryAttempts: 0,
				RetryTime:     "",
				RetryMessage:  "",
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseTaskResponse(tt.args.res)
			assert.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TesParseTaskResponseList(t *testing.T) {
	type args struct {
		res commands.Response
	}
	res1, err := parseTaskResponse(commands.NewResponse([]byte(tr1), "", nil))
	assert.NoError(t, err)
	res2, err := parseTaskResponse(commands.NewResponse([]byte(tr2), "", nil))
	assert.NoError(t, err)

	tests := []struct {
		name    string
		args    args
		want    []TaskResponse
		wantErr bool
	}{
		{
			name: "emptyList",
			args: args{
				res: commands.NewResponse([]byte(`[]`), "", nil),
			},
			want:    []TaskResponse{},
			wantErr: false,
		},
		{
			name: "twoItemList",
			args: args{
				res: commands.NewResponse([]byte(trList), "", nil),
			},
			want:    []TaskResponse{res1, res2},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseTaskResponseList(tt.args.res)
			assert.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}

var (
	imageName = "img"
	testNS    = "test"
)

type args struct {
	pool      string
	namespace string
	imageName string
}

var tests = []struct {
	name string
	args args
}{
	{
		name: "onlyImageName",
		args: args{
			pool:      "",
			namespace: "",
			imageName: imageName,
		},
	},
	{
		name: "Image&PoolName",
		args: args{
			pool:      defaultPoolName,
			namespace: "",
			imageName: imageName,
		},
	},
	{
		name: "AllArgs",
		args: args{
			pool:      defaultPoolName,
			namespace: testNS,
			imageName: imageName,
		},
	},
}

func TestTaskAdminAddRemove(t *testing.T) {
	ensureDefaultPool(t)
	conn := getConn(t)

	ioctx, err := conn.OpenIOContext(defaultPoolName)
	require.NoError(t, err)
	defer ioctx.Destroy()

	assert.NoError(t, rbd.NamespaceCreate(ioctx, testNS))
	defer func() {
		assert.NoError(t, rbd.NamespaceRemove(ioctx, testNS))
	}()

	ta := getAdmin(t).Task()
	options := rbd.NewRbdImageOptions()
	assert.NoError(t,
		options.SetUint64(rbd.ImageOptionOrder, uint64(testImageOrder)))

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {

			ioctx.SetNamespace(tt.args.namespace)
			err = rbd.CreateImage(ioctx, tt.args.imageName, testImageSize, options)
			assert.NoError(t, err)

			tr, err := ta.AddRemove(NewImageSpec(tt.args.pool, tt.args.namespace, tt.args.imageName))
			assert.NoError(t, err)

			assert.Equal(t, tt.args.imageName, tr.Refs.ImageName)
			assert.Equal(t, defaultPoolName, tr.Refs.PoolName)
			assert.Equal(t, tt.args.namespace, tr.Refs.PoolNamespace)
			assert.Equal(t, "remove", tr.Refs.Action)

			found := false
			// wait for the image to be deleted
			for i := 0; i < 35; i++ {
				imgList, err := rbd.GetImageNames(ioctx)
				assert.NoError(t, err)

				found = false
				for _, img := range imgList {
					if img == imageName {
						found = true
						break
					}
				}
				if !found {
					break
				}
				time.Sleep(time.Second)
			}
			assert.Equal(t, false, found)
		})
	}
}

func TestTaskAdminAddTrashRemove(t *testing.T) {
	ensureDefaultPool(t)
	conn := getConn(t)

	ioctx, err := conn.OpenIOContext(defaultPoolName)
	require.NoError(t, err)
	defer ioctx.Destroy()

	assert.NoError(t, rbd.NamespaceCreate(ioctx, testNS))
	defer func() {
		assert.NoError(t, rbd.NamespaceRemove(ioctx, testNS))
	}()

	ta := getAdmin(t).Task()
	options := rbd.NewRbdImageOptions()
	assert.NoError(t,
		options.SetUint64(rbd.ImageOptionOrder, uint64(testImageOrder)))

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {

			ioctx.SetNamespace(tt.args.namespace)
			err = rbd.CreateImage(ioctx, tt.args.imageName, testImageSize, options)
			assert.NoError(t, err)

			img, err := rbd.OpenImage(ioctx, tt.args.imageName, rbd.NoSnapshot)
			assert.NoError(t, err)

			imageID, err := img.GetId()
			assert.NoError(t, err)

			assert.NoError(t, img.Trash(0))
			assert.NoError(t, img.Close())

			tr, err := ta.AddTrashRemove(NewImageSpec(tt.args.pool, tt.args.namespace, imageID))
			assert.NoError(t, err)

			assert.Equal(t, imageID, tr.Refs.ImageID)
			assert.Equal(t, defaultPoolName, tr.Refs.PoolName)
			assert.Equal(t, tt.args.namespace, tr.Refs.PoolNamespace)
			assert.Equal(t, "trash remove", tr.Refs.Action)

			trashList := []rbd.TrashInfo{}
			// wait for the image to be deleted
			for i := 0; i < 35; i++ {
				trashList, err = rbd.GetTrashList(ioctx)
				assert.NoError(t, err)

				if len(trashList) == 0 {
					break
				}
				time.Sleep(time.Second)
			}
			assert.Equal(t, 0, len(trashList))
		})
	}
}

func TestTaskAdminAddFlatten(t *testing.T) {
	parentImageName := "parent"

	ensureDefaultPool(t)
	conn := getConn(t)

	ioctx, err := conn.OpenIOContext(defaultPoolName)
	require.NoError(t, err)
	defer ioctx.Destroy()

	assert.NoError(t, rbd.NamespaceCreate(ioctx, testNS))
	defer func() {
		assert.NoError(t, rbd.NamespaceRemove(ioctx, testNS))
	}()

	ta := getAdmin(t).Task()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ioctx.SetNamespace(tt.args.namespace)

			options := rbd.NewRbdImageOptions()
			assert.NoError(t,
				options.SetUint64(rbd.ImageOptionOrder, uint64(testImageOrder)))
			err = rbd.CreateImage(ioctx, parentImageName, testImageSize, options)
			assert.NoError(t, err)

			parentImage, err := rbd.OpenImage(ioctx, parentImageName, rbd.NoSnapshot)
			assert.NoError(t, err)
			defer func() {
				assert.NoError(t, parentImage.Close())
				assert.NoError(t, parentImage.Remove())
			}()

			snap, err := parentImage.CreateSnapshot(tt.args.imageName)
			assert.NoError(t, err)

			err = snap.Protect()
			assert.NoError(t, err)
			defer func() {
				assert.NoError(t, snap.Unprotect())
				assert.NoError(t, snap.Remove())
			}()
			assert.NoError(t, options.SetUint64(rbd.ImageOptionFormat, uint64(2)))

			assert.NoError(t, rbd.CloneImage(ioctx, parentImageName, tt.args.imageName, ioctx, tt.args.imageName, options))

			childImage, err := rbd.OpenImage(ioctx, tt.args.imageName, rbd.NoSnapshot)
			assert.NoError(t, err)
			defer func() {
				assert.NoError(t, childImage.Close())
				assert.NoError(t, childImage.Remove())
			}()

			parentInfo, err := childImage.GetParent()
			assert.NoError(t, err)
			assert.Equal(t, parentImageName, parentInfo.Image.ImageName)
			assert.Equal(t, tt.args.imageName, parentInfo.Snap.SnapName)

			tr, err := ta.AddFlatten(NewImageSpec(tt.args.pool, tt.args.namespace, tt.args.imageName))
			assert.NoError(t, err)

			assert.Equal(t, tt.args.imageName, tr.Refs.ImageName)
			assert.Equal(t, defaultPoolName, tr.Refs.PoolName)
			assert.Equal(t, tt.args.namespace, tr.Refs.PoolNamespace)
			assert.Equal(t, "flatten", tr.Refs.Action)

			// wait for the image to be flattened
			for i := 0; i < 35; i++ {
				_, err = childImage.GetParent()
				if errors.Is(err, rbd.RbdErrorNotFound) {
					break
				}
				assert.NoError(t, err)

				time.Sleep(time.Second)
			}
			assert.Error(t, err)
		})
	}

}
