package rbd

import (
	"sync"
	"testing"
	"time"

	"github.com/ceph/go-ceph/rados"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestDiffIterate(t *testing.T) {
	conn := radosConnect(t)
	defer conn.Shutdown()

	poolname := GetUUID()
	err := conn.MakePool(poolname)
	assert.NoError(t, err)
	defer conn.DeletePool(poolname)

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

	t.Run("basic", func(t *testing.T) {
		testDiffIterateBasic(t, ioctx)
	})
	t.Run("twoAtOnce", func(t *testing.T) {
		testDiffIterateTwoAtOnce(t, ioctx)
	})
	t.Run("earlyExit", func(t *testing.T) {
		testDiffIterateEarlyExit(t, ioctx)
	})
	t.Run("snapshot", func(t *testing.T) {
		testDiffIterateSnapshot(t, ioctx)
	})
	t.Run("callbackData", func(t *testing.T) {
		testDiffIterateCallbackData(t, ioctx)
	})
	t.Run("badImage", func(t *testing.T) {
		var gotCalled int
		img := GetImage(ioctx, "bob")
		err := img.DiffIterate(
			DiffIterateConfig{
				Offset: 0,
				Length: uint64(1 << 22),
				Callback: func(_, _ uint64, _ int, _ interface{}) int {
					gotCalled++
					return 0
				},
			})
		assert.Error(t, err)
		assert.EqualValues(t, 0, gotCalled)
	})
	t.Run("missingCallback", func(t *testing.T) {
		name := GetUUID()
		isize := uint64(1 << 23) // 8MiB
		iorder := 20             // 1MiB
		options := NewRbdImageOptions()
		defer options.Destroy()
		assert.NoError(t,
			options.SetUint64(RbdImageOptionOrder, uint64(iorder)))
		err := CreateImage(ioctx, name, isize, options)
		assert.NoError(t, err)

		img, err := OpenImage(ioctx, name, NoSnapshot)
		assert.NoError(t, err)
		defer func() {
			assert.NoError(t, img.Close())
			assert.NoError(t, img.Remove())
		}()

		var gotCalled int
		err = img.DiffIterate(
			DiffIterateConfig{
				Offset: 0,
				Length: uint64(1 << 22),
			})
		assert.Error(t, err)
		assert.EqualValues(t, 0, gotCalled)
	})
}

func testDiffIterateBasic(t *testing.T, ioctx *rados.IOContext) {
	name := GetUUID()
	isize := uint64(1 << 23) // 8MiB
	iorder := 20             // 1MiB
	options := NewRbdImageOptions()
	defer options.Destroy()
	assert.NoError(t,
		options.SetUint64(RbdImageOptionOrder, uint64(iorder)))
	err := CreateImage(ioctx, name, isize, options)
	assert.NoError(t, err)

	img, err := OpenImage(ioctx, name, NoSnapshot)
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, img.Close())
		assert.NoError(t, img.Remove())
	}()

	type diResult struct {
		offset uint64
		length uint64
	}
	calls := []diResult{}

	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	// Image is new, empty. Callback will not be called
	assert.Len(t, calls, 0)

	_, err = img.WriteAt([]byte("sometimes you feel like a nut"), 0)
	assert.NoError(t, err)

	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 1) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 29, calls[0].length)
	}

	_, err = img.WriteAt([]byte("sometimes you don't"), 32)
	assert.NoError(t, err)

	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	if assert.NoError(t, err) {
		assert.Len(t, calls, 1)
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 51, calls[0].length)
	}

	// dirty a 2nd chunk
	newOffset := 3145728 // 3MiB
	_, err = img.WriteAt([]byte("alright, alright, alright"), int64(newOffset))
	assert.NoError(t, err)

	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 2) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 51, calls[0].length)
		assert.EqualValues(t, newOffset, calls[1].offset)
		assert.EqualValues(t, 25, calls[1].length)
	}

	// dirty a 3rd chunk
	newOffset2 := 5242880 + 1024 // 5MiB + 1KiB
	_, err = img.WriteAt([]byte("zowie!"), int64(newOffset2))
	assert.NoError(t, err)

	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 3) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 51, calls[0].length)
		assert.EqualValues(t, newOffset, calls[1].offset)
		assert.EqualValues(t, 25, calls[1].length)
		assert.EqualValues(t, newOffset2-1024, calls[2].offset)
		assert.EqualValues(t, 6+1024, calls[2].length)
	}
}

// testDiffIterateTwoAtOnce aims to verify that multiple DiffIterate
// callbacks can be executed at the same time without error.
func testDiffIterateTwoAtOnce(t *testing.T, ioctx *rados.IOContext) {
	isize := uint64(1 << 23) // 8MiB
	iorder := 20             // 1MiB
	options := NewRbdImageOptions()
	defer options.Destroy()
	assert.NoError(t,
		options.SetUint64(RbdImageOptionOrder, uint64(iorder)))

	name1 := GetUUID()
	err := CreateImage(ioctx, name1, isize, options)
	assert.NoError(t, err)

	img1, err := OpenImage(ioctx, name1, NoSnapshot)
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, img1.Close())
		assert.NoError(t, img1.Remove())
	}()

	name2 := GetUUID()
	err = CreateImage(ioctx, name2, isize, options)
	assert.NoError(t, err)

	img2, err := OpenImage(ioctx, name2, NoSnapshot)
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, img2.Close())
		assert.NoError(t, img2.Remove())
	}()

	type diResult struct {
		offset uint64
		length uint64
	}

	diffTest := func(wg *sync.WaitGroup, inbuf []byte, img *Image) {
		_, err = img.WriteAt(inbuf[0:3], 0)
		assert.NoError(t, err)
		_, err = img.WriteAt(inbuf[3:6], 3145728)
		assert.NoError(t, err)
		_, err = img.WriteAt(inbuf[6:9], 5242880)
		assert.NoError(t, err)

		calls := []diResult{}
		err = img.DiffIterate(
			DiffIterateConfig{
				Offset: 0,
				Length: isize,
				Callback: func(o, l uint64, _ int, _ interface{}) int {
					time.Sleep(8 * time.Millisecond)
					calls = append(calls, diResult{offset: o, length: l})
					return 0
				},
			})
		assert.NoError(t, err)
		if assert.Len(t, calls, 3) {
			assert.EqualValues(t, 0, calls[0].offset)
			assert.EqualValues(t, 3, calls[0].length)
			assert.EqualValues(t, 3145728, calls[1].offset)
			assert.EqualValues(t, 3, calls[1].length)
			assert.EqualValues(t, 5242880, calls[2].offset)
			assert.EqualValues(t, 3, calls[2].length)
		}

		wg.Done()
	}

	wg := &sync.WaitGroup{}
	wg.Add(1)
	go diffTest(wg, []byte("foobarbaz"), img1)
	wg.Add(1)
	go diffTest(wg, []byte("abcdefghi"), img2)
	wg.Wait()
}

// testDiffIterateEarlyExit checks that returning an error from the callback
// function triggers the DiffIterate call to stop.
func testDiffIterateEarlyExit(t *testing.T, ioctx *rados.IOContext) {
	isize := uint64(1 << 23) // 8MiB
	iorder := 20             // 1MiB
	options := NewRbdImageOptions()
	defer options.Destroy()
	assert.NoError(t,
		options.SetUint64(RbdImageOptionOrder, uint64(iorder)))

	name := GetUUID()
	err := CreateImage(ioctx, name, isize, options)
	assert.NoError(t, err)

	img, err := OpenImage(ioctx, name, NoSnapshot)
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, img.Close())
		assert.NoError(t, img.Remove())
	}()

	type diResult struct {
		offset uint64
		length uint64
	}

	// "damage" the image
	inbuf := []byte("xxxyyyzzz")
	_, err = img.WriteAt(inbuf[0:3], 0)
	assert.NoError(t, err)
	_, err = img.WriteAt(inbuf[3:6], 3145728)
	assert.NoError(t, err)
	_, err = img.WriteAt(inbuf[6:9], 5242880)
	assert.NoError(t, err)

	// if the offset is less than zero the callback will return an "error" and
	// that will abort the DiffIterate call early and it will return the error
	// code our callback used.
	calls := []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				if o > 1 {
					return -5
				}
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.Error(t, err)
	if errno, ok := err.(interface{ ErrorCode() int }); assert.True(t, ok) {
		assert.EqualValues(t, -5, errno.ErrorCode())
	}
	if assert.Len(t, calls, 1) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 3, calls[0].length)
	}
}

func testDiffIterateSnapshot(t *testing.T, ioctx *rados.IOContext) {
	name := GetUUID()
	isize := uint64(1 << 23) // 8MiB
	iorder := 20             // 1MiB
	options := NewRbdImageOptions()
	defer options.Destroy()
	assert.NoError(t,
		options.SetUint64(RbdImageOptionOrder, uint64(iorder)))
	err := CreateImage(ioctx, name, isize, options)
	assert.NoError(t, err)

	img, err := OpenImage(ioctx, name, NoSnapshot)
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, img.Close())
		assert.NoError(t, img.Remove())
	}()

	type diResult struct {
		offset uint64
		length uint64
	}
	calls := []diResult{}

	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	// Image is new, empty. Callback will not be called
	assert.Len(t, calls, 0)

	_, err = img.WriteAt([]byte("sometimes you feel like a nut"), 0)
	assert.NoError(t, err)

	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 1) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 29, calls[0].length)
	}

	ss1, err := img.CreateSnapshot("ss1")
	assert.NoError(t, err)
	defer func() { assert.NoError(t, ss1.Remove()) }()

	// there should be no differences between "now" and "ss1"
	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			SnapName: "ss1",
			Offset:   0,
			Length:   isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	assert.Len(t, calls, 0)

	// this next check was shamelessly cribbed from the pybind
	// tests for diff_iterate out of the ceph tree
	// it discards the current image, makes a 2nd snap, and compares
	// the diff between snapshots 1 & 2.
	_, err = img.Discard(0, isize)
	assert.NoError(t, err)

	ss2, err := img.CreateSnapshot("ss2")
	assert.NoError(t, err)
	defer func() { assert.NoError(t, ss2.Remove()) }()
	err = ss2.Set() // caution: this side-effects img!
	assert.NoError(t, err)

	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			SnapName: "ss1",
			Offset:   0,
			Length:   isize,
			Callback: func(o, l uint64, _ int, _ interface{}) int {
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 1) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 29, calls[0].length)
	}
}

func testDiffIterateCallbackData(t *testing.T, ioctx *rados.IOContext) {
	name := GetUUID()
	isize := uint64(1 << 23) // 8MiB
	iorder := 20             // 1MiB
	options := NewRbdImageOptions()
	defer options.Destroy()
	assert.NoError(t,
		options.SetUint64(RbdImageOptionOrder, uint64(iorder)))
	err := CreateImage(ioctx, name, isize, options)
	assert.NoError(t, err)

	img, err := OpenImage(ioctx, name, NoSnapshot)
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, img.Close())
		assert.NoError(t, img.Remove())
	}()

	type diResult struct {
		offset uint64
		length uint64
	}
	calls := []diResult{}

	_, err = img.WriteAt([]byte("sometimes you feel like a nut"), 0)
	assert.NoError(t, err)

	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, x interface{}) int {
				if v, ok := x.(int); ok {
					assert.EqualValues(t, 77, v)
				} else {
					t.Fatalf("incorrect type")
				}
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
			Data: 77,
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 1) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 29, calls[0].length)
	}

	calls = []diResult{}
	err = img.DiffIterate(
		DiffIterateConfig{
			Offset: 0,
			Length: isize,
			Callback: func(o, l uint64, _ int, x interface{}) int {
				if v, ok := x.(string); ok {
					assert.EqualValues(t, "bob", v)
				} else {
					t.Fatalf("incorrect type")
				}
				calls = append(calls, diResult{offset: o, length: l})
				return 0
			},
			Data: "bob",
		})
	assert.NoError(t, err)
	if assert.Len(t, calls, 1) {
		assert.EqualValues(t, 0, calls[0].offset)
		assert.EqualValues(t, 29, calls[0].length)
	}
}
