// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package safemem

import (
	"bytes"
	"reflect"
	"testing"
)

type blockSeqTest struct {
	desc string

	pieces     []string
	haveOffset bool
	offset     uint64
	haveLimit  bool
	limit      uint64

	want string
}

func (t blockSeqTest) NonEmptyByteSlices() [][]byte {
	// t is a value, so we can mutate it freely.
	slices := make([][]byte, 0, len(t.pieces))
	for _, str := range t.pieces {
		if t.haveOffset {
			strOff := t.offset
			if strOff > uint64(len(str)) {
				strOff = uint64(len(str))
			}
			str = str[strOff:]
			t.offset -= strOff
		}
		if t.haveLimit {
			strLim := t.limit
			if strLim > uint64(len(str)) {
				strLim = uint64(len(str))
			}
			str = str[:strLim]
			t.limit -= strLim
		}
		if len(str) != 0 {
			slices = append(slices, []byte(str))
		}
	}
	return slices
}

func (t blockSeqTest) BlockSeq() BlockSeq {
	blocks := make([]Block, 0, len(t.pieces))
	for _, str := range t.pieces {
		blocks = append(blocks, BlockFromSafeSlice([]byte(str)))
	}
	bs := BlockSeqFromSlice(blocks)
	if t.haveOffset {
		bs = bs.DropFirst64(t.offset)
	}
	if t.haveLimit {
		bs = bs.TakeFirst64(t.limit)
	}
	return bs
}

var blockSeqTests = []blockSeqTest{
	{
		desc: "Empty sequence",
	},
	{
		desc:   "Sequence of length 1",
		pieces: []string{"foobar"},
		want:   "foobar",
	},
	{
		desc:   "Sequence of length 2",
		pieces: []string{"foo", "bar"},
		want:   "foobar",
	},
	{
		desc:   "Empty Blocks",
		pieces: []string{"", "foo", "", "", "bar", ""},
		want:   "foobar",
	},
	{
		desc:       "Sequence with non-zero offset",
		pieces:     []string{"foo", "bar"},
		haveOffset: true,
		offset:     2,
		want:       "obar",
	},
	{
		desc:      "Sequence with non-maximal limit",
		pieces:    []string{"foo", "bar"},
		haveLimit: true,
		limit:     5,
		want:      "fooba",
	},
	{
		desc:       "Sequence with offset and limit",
		pieces:     []string{"foo", "bar"},
		haveOffset: true,
		offset:     2,
		haveLimit:  true,
		limit:      3,
		want:       "oba",
	},
}

func TestBlockSeqNumBytes(t *testing.T) {
	for _, test := range blockSeqTests {
		t.Run(test.desc, func(t *testing.T) {
			if got, want := test.BlockSeq().NumBytes(), uint64(len(test.want)); got != want {
				t.Errorf("NumBytes: got %d, wanted %d", got, want)
			}
		})
	}
}

func TestBlockSeqIterBlocks(t *testing.T) {
	// Tests BlockSeq iteration using Head/Tail.
	for _, test := range blockSeqTests {
		t.Run(test.desc, func(t *testing.T) {
			srcs := test.BlockSeq()
			// "Note that a non-nil empty slice and a nil slice ... are not
			// deeply equal." - reflect
			slices := make([][]byte, 0, 0)
			for !srcs.IsEmpty() {
				src := srcs.Head()
				slices = append(slices, src.ToSlice())
				nextSrcs := srcs.Tail()
				if got, want := nextSrcs.NumBytes(), srcs.NumBytes()-uint64(src.Len()); got != want {
					t.Fatalf("%v.Tail(): got %v (%d bytes), wanted %d bytes", srcs, nextSrcs, got, want)
				}
				srcs = nextSrcs
			}
			if wantSlices := test.NonEmptyByteSlices(); !reflect.DeepEqual(slices, wantSlices) {
				t.Errorf("Accumulated slices: got %v, wanted %v", slices, wantSlices)
			}
		})
	}
}

func TestBlockSeqIterBytes(t *testing.T) {
	// Tests BlockSeq iteration using Head/DropFirst.
	for _, test := range blockSeqTests {
		t.Run(test.desc, func(t *testing.T) {
			srcs := test.BlockSeq()
			var dst bytes.Buffer
			for !srcs.IsEmpty() {
				src := srcs.Head()
				var b [1]byte
				n, err := Copy(BlockFromSafeSlice(b[:]), src)
				if n != 1 || err != nil {
					t.Fatalf("Copy: got (%v, %v), wanted (1, nil)", n, err)
				}
				dst.WriteByte(b[0])
				nextSrcs := srcs.DropFirst(1)
				if got, want := nextSrcs.NumBytes(), srcs.NumBytes()-1; got != want {
					t.Fatalf("%v.DropFirst(1): got %v (%d bytes), wanted %d bytes", srcs, nextSrcs, got, want)
				}
				srcs = nextSrcs
			}
			if got := string(dst.Bytes()); got != test.want {
				t.Errorf("Copied string: got %q, wanted %q", got, test.want)
			}
		})
	}
}

func TestBlockSeqDropBeyondLimit(t *testing.T) {
	blocks := []Block{BlockFromSafeSlice([]byte("123")), BlockFromSafeSlice([]byte("4"))}
	bs := BlockSeqFromSlice(blocks)
	if got, want := bs.NumBytes(), uint64(4); got != want {
		t.Errorf("%v.NumBytes(): got %d, wanted %d", bs, got, want)
	}
	bs = bs.TakeFirst(1)
	if got, want := bs.NumBytes(), uint64(1); got != want {
		t.Errorf("%v.NumBytes(): got %d, wanted %d", bs, got, want)
	}
	bs = bs.DropFirst(2)
	if got, want := bs.NumBytes(), uint64(0); got != want {
		t.Errorf("%v.NumBytes(): got %d, wanted %d", bs, got, want)
	}
}