package packfile

import (
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/storage/memory"

	. "gopkg.in/check.v1"
)

type DeltaSelectorSuite struct {
	ds     *deltaSelector
	store  *memory.Storage
	hashes map[string]plumbing.Hash
}

var _ = Suite(&DeltaSelectorSuite{})

func (s *DeltaSelectorSuite) SetUpTest(c *C) {
	s.store = memory.NewStorage()
	s.createTestObjects()
	s.ds = newDeltaSelector(s.store)
}

func (s *DeltaSelectorSuite) TestSort(c *C) {
	var o1 = newObjectToPack(newObject(plumbing.BlobObject, []byte("00000")))
	var o4 = newObjectToPack(newObject(plumbing.BlobObject, []byte("0000")))
	var o6 = newObjectToPack(newObject(plumbing.BlobObject, []byte("00")))
	var o9 = newObjectToPack(newObject(plumbing.BlobObject, []byte("0")))
	var o8 = newObjectToPack(newObject(plumbing.TreeObject, []byte("000")))
	var o2 = newObjectToPack(newObject(plumbing.TreeObject, []byte("00")))
	var o3 = newObjectToPack(newObject(plumbing.TreeObject, []byte("0")))
	var o5 = newObjectToPack(newObject(plumbing.CommitObject, []byte("0000")))
	var o7 = newObjectToPack(newObject(plumbing.CommitObject, []byte("00")))

	toSort := []*ObjectToPack{o1, o2, o3, o4, o5, o6, o7, o8, o9}
	s.ds.sort(toSort)
	expected := []*ObjectToPack{o1, o4, o6, o9, o8, o2, o3, o5, o7}
	c.Assert(toSort, DeepEquals, expected)
}

type testObject struct {
	id     string
	object plumbing.EncodedObject
}

var testObjects []*testObject = []*testObject{{
	id: "base",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1000,
			val:   "a",
		}, {
			times: 1000,
			val:   "b",
		}})),
}, {
	id: "smallBase",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1,
			val:   "a",
		}, {
			times: 1,
			val:   "b",
		}, {
			times: 6,
			val:   "c",
		}})),
}, {
	id: "smallTarget",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1,
			val:   "a",
		}, {
			times: 1,
			val:   "c",
		}})),
}, {
	id: "target",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1000,
			val:   "a",
		}, {
			times: 1000,
			val:   "b",
		}, {
			times: 1000,
			val:   "c",
		}})),
}, {
	id: "o1",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1000,
			val:   "a",
		}, {
			times: 1000,
			val:   "b",
		}})),
}, {
	id: "o2",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1000,
			val:   "a",
		}, {
			times: 500,
			val:   "b",
		}})),
}, {
	id: "o3",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1000,
			val:   "a",
		}, {
			times: 499,
			val:   "b",
		}})),
}, {
	id: "bigBase",
	object: newObject(plumbing.BlobObject,
		genBytes([]piece{{
			times: 1000000,
			val:   "a",
		}})),
}, {
	id: "treeType",
	object: newObject(plumbing.TreeObject,
		[]byte("I am a tree!")),
}}

func (s *DeltaSelectorSuite) createTestObjects() {
	s.hashes = make(map[string]plumbing.Hash)
	for _, o := range testObjects {
		h, err := s.store.SetEncodedObject(o.object)
		if err != nil {
			panic(err)
		}
		s.hashes[o.id] = h
	}
}

func (s *DeltaSelectorSuite) TestObjectsToPack(c *C) {
	// Different type
	hashes := []plumbing.Hash{s.hashes["base"], s.hashes["treeType"]}
	deltaWindowSize := uint(10)
	otp, err := s.ds.ObjectsToPack(hashes, deltaWindowSize)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, 2)
	c.Assert(otp[0].Object, Equals, s.store.Objects[s.hashes["base"]])
	c.Assert(otp[1].Object, Equals, s.store.Objects[s.hashes["treeType"]])

	// Size radically different
	hashes = []plumbing.Hash{s.hashes["bigBase"], s.hashes["target"]}
	otp, err = s.ds.ObjectsToPack(hashes, deltaWindowSize)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, 2)
	c.Assert(otp[0].Object, Equals, s.store.Objects[s.hashes["bigBase"]])
	c.Assert(otp[1].Object, Equals, s.store.Objects[s.hashes["target"]])

	// Delta Size Limit with no best delta yet
	hashes = []plumbing.Hash{s.hashes["smallBase"], s.hashes["smallTarget"]}
	otp, err = s.ds.ObjectsToPack(hashes, deltaWindowSize)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, 2)
	c.Assert(otp[0].Object, Equals, s.store.Objects[s.hashes["smallBase"]])
	c.Assert(otp[1].Object, Equals, s.store.Objects[s.hashes["smallTarget"]])

	// It will create the delta
	hashes = []plumbing.Hash{s.hashes["base"], s.hashes["target"]}
	otp, err = s.ds.ObjectsToPack(hashes, deltaWindowSize)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, 2)
	c.Assert(otp[0].Object, Equals, s.store.Objects[s.hashes["target"]])
	c.Assert(otp[0].IsDelta(), Equals, false)
	c.Assert(otp[1].Original, Equals, s.store.Objects[s.hashes["base"]])
	c.Assert(otp[1].IsDelta(), Equals, true)
	c.Assert(otp[1].Depth, Equals, 1)

	// If our base is another delta, the depth will increase by one
	hashes = []plumbing.Hash{
		s.hashes["o1"],
		s.hashes["o2"],
		s.hashes["o3"],
	}
	otp, err = s.ds.ObjectsToPack(hashes, deltaWindowSize)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, 3)
	c.Assert(otp[0].Object, Equals, s.store.Objects[s.hashes["o1"]])
	c.Assert(otp[0].IsDelta(), Equals, false)
	c.Assert(otp[1].Original, Equals, s.store.Objects[s.hashes["o2"]])
	c.Assert(otp[1].IsDelta(), Equals, true)
	c.Assert(otp[1].Depth, Equals, 1)
	c.Assert(otp[2].Original, Equals, s.store.Objects[s.hashes["o3"]])
	c.Assert(otp[2].IsDelta(), Equals, true)
	c.Assert(otp[2].Depth, Equals, 2)

	// Check that objects outside of the sliding window don't produce
	// a delta.
	hashes = make([]plumbing.Hash, 0, deltaWindowSize+2)
	hashes = append(hashes, s.hashes["base"])
	for i := uint(0); i < deltaWindowSize; i++ {
		hashes = append(hashes, s.hashes["smallTarget"])
	}
	hashes = append(hashes, s.hashes["target"])

	// Don't sort so we can easily check the sliding window without
	// creating a bunch of new objects.
	otp, err = s.ds.objectsToPack(hashes, deltaWindowSize)
	c.Assert(err, IsNil)
	err = s.ds.walk(otp, deltaWindowSize)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, int(deltaWindowSize)+2)
	targetIdx := len(otp) - 1
	c.Assert(otp[targetIdx].IsDelta(), Equals, false)

	// Check that no deltas are created, and the objects are unsorted,
	// if compression is off.
	hashes = []plumbing.Hash{s.hashes["base"], s.hashes["target"]}
	otp, err = s.ds.ObjectsToPack(hashes, 0)
	c.Assert(err, IsNil)
	c.Assert(len(otp), Equals, 2)
	c.Assert(otp[0].Object, Equals, s.store.Objects[s.hashes["base"]])
	c.Assert(otp[0].IsDelta(), Equals, false)
	c.Assert(otp[1].Original, Equals, s.store.Objects[s.hashes["target"]])
	c.Assert(otp[1].IsDelta(), Equals, false)
	c.Assert(otp[1].Depth, Equals, 0)
}

func (s *DeltaSelectorSuite) TestMaxDepth(c *C) {
	dsl := s.ds.deltaSizeLimit(0, 0, int(maxDepth), true)
	c.Assert(dsl, Equals, int64(0))
}
