package restorer

import (
	"io"
	"testing"

	"github.com/restic/restic/internal/errors"
	"github.com/restic/restic/internal/restic"
	rtest "github.com/restic/restic/internal/test"
)

func assertNotOK(t *testing.T, msg string, err error) {
	rtest.Assert(t, err != nil, msg+" did not fail")
}

func TestBytesWriterSeeker(t *testing.T) {
	wr := &bytesWriteSeeker{data: make([]byte, 10)}

	n, err := wr.Write([]byte{1, 2})
	rtest.OK(t, err)
	rtest.Equals(t, 2, n)
	rtest.Equals(t, []byte{1, 2}, wr.data[0:2])

	n64, err := wr.Seek(0, io.SeekStart)
	rtest.OK(t, err)
	rtest.Equals(t, int64(0), n64)

	n, err = wr.Write([]byte{0, 1, 2, 3, 4})
	rtest.OK(t, err)
	rtest.Equals(t, 5, n)
	n, err = wr.Write([]byte{5, 6, 7, 8, 9})
	rtest.OK(t, err)
	rtest.Equals(t, 5, n)
	rtest.Equals(t, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, wr.data)

	// negative tests
	_, err = wr.Write([]byte{1})
	assertNotOK(t, "write overflow", err)
	_, err = wr.Seek(1, io.SeekStart)
	assertNotOK(t, "unsupported seek", err)
}

func TestPackCacheBasic(t *testing.T) {
	assertReader := func(expected []byte, offset int64, rd io.ReaderAt) {
		actual := make([]byte, len(expected))
		rd.ReadAt(actual, offset)
		rtest.Equals(t, expected, actual)
	}

	c := newPackCache(10)

	id := restic.NewRandomID()

	// load pack to the cache
	rd, err := c.get(id, 10, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		rtest.Equals(t, int64(10), offset)
		rtest.Equals(t, 5, length)
		wr.Write([]byte{1, 2, 3, 4, 5})
		return nil
	})
	rtest.OK(t, err)
	assertReader([]byte{1, 2, 3, 4, 5}, 10, rd)

	// must close pack reader before can request it again
	_, err = c.get(id, 10, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected cache load call")
		return nil
	})
	assertNotOK(t, "double-reservation", err)

	// close the pack reader and get it from cache
	rd.Close()
	rd, err = c.get(id, 10, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected cache load call")
		return nil
	})
	rtest.OK(t, err)
	assertReader([]byte{1, 2, 3, 4, 5}, 10, rd)

	// close the pack reader and remove the pack from cache, assert the pack is loaded on request
	rd.Close()
	c.remove(id)
	rd, err = c.get(id, 10, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		rtest.Equals(t, int64(10), offset)
		rtest.Equals(t, 5, length)
		wr.Write([]byte{1, 2, 3, 4, 5})
		return nil
	})
	rtest.OK(t, err)
	assertReader([]byte{1, 2, 3, 4, 5}, 10, rd)
}

func TestPackCacheInvalidRange(t *testing.T) {
	c := newPackCache(10)

	id := restic.NewRandomID()

	_, err := c.get(id, -1, 1, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected cache load call")
		return nil
	})
	assertNotOK(t, "negative offset request", err)

	_, err = c.get(id, 0, 0, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected cache load call")
		return nil
	})
	assertNotOK(t, "zero length request", err)

	_, err = c.get(id, 0, -1, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected cache load call")
		return nil
	})
	assertNotOK(t, "negative length", err)
}

func TestPackCacheCapacity(t *testing.T) {
	c := newPackCache(10)

	id1, id2, id3 := restic.NewRandomID(), restic.NewRandomID(), restic.NewRandomID()

	// load and reserve pack1
	rd1, err := c.get(id1, 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1, 2, 3, 4, 5})
		return nil
	})
	rtest.OK(t, err)

	// load and reserve pack2
	_, err = c.get(id2, 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1, 2, 3, 4, 5})
		return nil
	})
	rtest.OK(t, err)

	// can't load pack3 because not enough space in the cache
	_, err = c.get(id3, 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected cache load call")
		return nil
	})
	assertNotOK(t, "request over capacity", err)

	// release pack1 and try again
	rd1.Close()
	rd3, err := c.get(id3, 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1, 2, 3, 4, 5})
		return nil
	})
	rtest.OK(t, err)

	// release pack3 and load pack1 (should not come from cache)
	rd3.Close()
	loaded := false
	rd1, err = c.get(id1, 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1, 2, 3, 4, 5})
		loaded = true
		return nil
	})
	rtest.OK(t, err)
	rtest.Equals(t, true, loaded)
}

func TestPackCacheDownsizeRecord(t *testing.T) {
	c := newPackCache(10)

	id := restic.NewRandomID()

	// get bigger range first
	rd, err := c.get(id, 5, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1, 2, 3, 4, 5})
		return nil
	})
	rtest.OK(t, err)
	rd.Close()

	// invalid "resize" requests
	_, err = c.get(id, 5, 10, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	assertNotOK(t, "resize cached record", err)

	// invalid before cached range request
	_, err = c.get(id, 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	assertNotOK(t, "before cached range request", err)

	// invalid after cached range request
	_, err = c.get(id, 10, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	assertNotOK(t, "after cached range request", err)

	// now get smaller "nested" range
	rd, err = c.get(id, 7, 1, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	rtest.OK(t, err)

	// assert expected data
	buf := make([]byte, 1)
	rd.ReadAt(buf, 7)
	rtest.Equals(t, byte(3), buf[0])
	_, err = rd.ReadAt(buf, 0)
	assertNotOK(t, "read before downsized pack range", err)
	_, err = rd.ReadAt(buf, 9)
	assertNotOK(t, "read after downsized pack range", err)

	// can't request downsized record again
	_, err = c.get(id, 7, 1, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	assertNotOK(t, "double-allocation of cache record subrange", err)

	// can't request another subrange of the original record
	_, err = c.get(id, 6, 1, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	assertNotOK(t, "allocation of another subrange of cache record", err)

	// release downsized record and assert the original is back in the cache
	rd.Close()
	rd, err = c.get(id, 5, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		t.Error("unexpected pack load")
		return nil
	})
	rtest.OK(t, err)
	rd.Close()
}

func TestPackCacheFailedDownload(t *testing.T) {
	c := newPackCache(10)
	assertEmpty := func() {
		rtest.Equals(t, 0, len(c.cachedPacks))
		rtest.Equals(t, 10, c.capacity)
		rtest.Equals(t, 0, c.reservedCapacity)
		rtest.Equals(t, 0, c.allocatedCapacity)
	}

	_, err := c.get(restic.NewRandomID(), 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		return errors.Errorf("expected induced test error")
	})
	assertNotOK(t, "not enough bytes read", err)
	assertEmpty()

	_, err = c.get(restic.NewRandomID(), 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1})
		return nil
	})
	assertNotOK(t, "not enough bytes read", err)
	assertEmpty()

	_, err = c.get(restic.NewRandomID(), 0, 5, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1, 2, 3, 4, 5, 6})
		return nil
	})
	assertNotOK(t, "too many bytes read", err)
	assertEmpty()
}

func TestPackCacheInvalidRequests(t *testing.T) {
	c := newPackCache(10)

	id := restic.NewRandomID()

	//
	rd, _ := c.get(id, 0, 1, func(offset int64, length int, wr io.WriteSeeker) error {
		wr.Write([]byte{1})
		return nil
	})
	assertNotOK(t, "remove() reserved pack", c.remove(id))
	rtest.OK(t, rd.Close())
	assertNotOK(t, "multiple reader Close() calls)", rd.Close())

	//
	rtest.OK(t, c.remove(id))
	assertNotOK(t, "double remove() the same pack", c.remove(id))
}

func TestPackCacheRecord(t *testing.T) {
	rd := &packCacheRecord{
		offset: 10,
		data:   []byte{1},
	}
	buf := make([]byte, 1)
	n, err := rd.ReadAt(buf, 10)
	rtest.OK(t, err)
	rtest.Equals(t, 1, n)
	rtest.Equals(t, byte(1), buf[0])

	_, err = rd.ReadAt(buf, 0)
	assertNotOK(t, "read before loaded range", err)

	_, err = rd.ReadAt(buf, 11)
	assertNotOK(t, "read after loaded range", err)

	_, err = rd.ReadAt(make([]byte, 2), 10)
	assertNotOK(t, "read more than available data", err)
}