// Copyright 2020 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 buffer

import (
	"bytes"
	"io"
	"strings"
	"testing"
)

func fillAppend(v *View, data []byte) {
	v.Append(data)
}

func fillAppendEnd(v *View, data []byte) {
	v.Grow(bufferSize-1, false)
	v.Append(data)
	v.TrimFront(bufferSize - 1)
}

func fillWriteFromReader(v *View, data []byte) {
	b := bytes.NewBuffer(data)
	v.WriteFromReader(b, int64(len(data)))
}

func fillWriteFromReaderEnd(v *View, data []byte) {
	v.Grow(bufferSize-1, false)
	b := bytes.NewBuffer(data)
	v.WriteFromReader(b, int64(len(data)))
	v.TrimFront(bufferSize - 1)
}

var fillFuncs = map[string]func(*View, []byte){
	"append":             fillAppend,
	"appendEnd":          fillAppendEnd,
	"writeFromReader":    fillWriteFromReader,
	"writeFromReaderEnd": fillWriteFromReaderEnd,
}

func testReadAt(t *testing.T, v *View, offset int64, n int, wantStr string, wantErr error) {
	t.Helper()
	d := make([]byte, n)
	n, err := v.ReadAt(d, offset)
	if n != len(wantStr) {
		t.Errorf("got %d, want %d", n, len(wantStr))
	}
	if err != wantErr {
		t.Errorf("got err %v, want %v", err, wantErr)
	}
	if !bytes.Equal(d[:n], []byte(wantStr)) {
		t.Errorf("got %q, want %q", string(d[:n]), wantStr)
	}
}

func TestView(t *testing.T) {
	testCases := []struct {
		name   string
		input  string
		output string
		op     func(*testing.T, *View)
	}{
		// Preconditions.
		{
			name:   "truncate-check",
			input:  "hello",
			output: "hello", // Not touched.
			op: func(t *testing.T, v *View) {
				defer func() {
					if r := recover(); r == nil {
						t.Errorf("Truncate(-1) did not panic")
					}
				}()
				v.Truncate(-1)
			},
		},
		{
			name:   "grow-check",
			input:  "hello",
			output: "hello", // Not touched.
			op: func(t *testing.T, v *View) {
				defer func() {
					if r := recover(); r == nil {
						t.Errorf("Grow(-1) did not panic")
					}
				}()
				v.Grow(-1, false)
			},
		},
		{
			name:   "advance-check",
			input:  "hello",
			output: "", // Consumed.
			op: func(t *testing.T, v *View) {
				defer func() {
					if r := recover(); r == nil {
						t.Errorf("advanceRead(Size()+1) did not panic")
					}
				}()
				v.advanceRead(v.Size() + 1)
			},
		},

		// Prepend.
		{
			name:   "prepend",
			input:  "world",
			output: "hello world",
			op: func(t *testing.T, v *View) {
				v.Prepend([]byte("hello "))
			},
		},
		{
			name:   "prepend-backfill-full",
			input:  "hello world",
			output: "jello world",
			op: func(t *testing.T, v *View) {
				v.TrimFront(1)
				v.Prepend([]byte("j"))
			},
		},
		{
			name:   "prepend-backfill-under",
			input:  "hello world",
			output: "hola world",
			op: func(t *testing.T, v *View) {
				v.TrimFront(5)
				v.Prepend([]byte("hola"))
			},
		},
		{
			name:   "prepend-backfill-over",
			input:  "hello world",
			output: "smello world",
			op: func(t *testing.T, v *View) {
				v.TrimFront(1)
				v.Prepend([]byte("sm"))
			},
		},
		{
			name:   "prepend-fill",
			input:  strings.Repeat("1", bufferSize-1),
			output: "0" + strings.Repeat("1", bufferSize-1),
			op: func(t *testing.T, v *View) {
				v.Prepend([]byte("0"))
			},
		},
		{
			name:   "prepend-overflow",
			input:  strings.Repeat("1", bufferSize),
			output: "0" + strings.Repeat("1", bufferSize),
			op: func(t *testing.T, v *View) {
				v.Prepend([]byte("0"))
			},
		},
		{
			name:   "prepend-multiple-buffers",
			input:  strings.Repeat("1", bufferSize-1),
			output: strings.Repeat("0", bufferSize*3) + strings.Repeat("1", bufferSize-1),
			op: func(t *testing.T, v *View) {
				v.Prepend([]byte(strings.Repeat("0", bufferSize*3)))
			},
		},

		// Append and write.
		{
			name:   "append",
			input:  "hello",
			output: "hello world",
			op: func(t *testing.T, v *View) {
				v.Append([]byte(" world"))
			},
		},
		{
			name:   "append-fill",
			input:  strings.Repeat("1", bufferSize-1),
			output: strings.Repeat("1", bufferSize-1) + "0",
			op: func(t *testing.T, v *View) {
				v.Append([]byte("0"))
			},
		},
		{
			name:   "append-overflow",
			input:  strings.Repeat("1", bufferSize),
			output: strings.Repeat("1", bufferSize) + "0",
			op: func(t *testing.T, v *View) {
				v.Append([]byte("0"))
			},
		},
		{
			name:   "append-multiple-buffers",
			input:  strings.Repeat("1", bufferSize-1),
			output: strings.Repeat("1", bufferSize-1) + strings.Repeat("0", bufferSize*3),
			op: func(t *testing.T, v *View) {
				v.Append([]byte(strings.Repeat("0", bufferSize*3)))
			},
		},

		// Truncate.
		{
			name:   "truncate",
			input:  "hello world",
			output: "hello",
			op: func(t *testing.T, v *View) {
				v.Truncate(5)
			},
		},
		{
			name:   "truncate-noop",
			input:  "hello world",
			output: "hello world",
			op: func(t *testing.T, v *View) {
				v.Truncate(v.Size() + 1)
			},
		},
		{
			name:   "truncate-multiple-buffers",
			input:  strings.Repeat("1", bufferSize*2),
			output: strings.Repeat("1", bufferSize*2-1),
			op: func(t *testing.T, v *View) {
				v.Truncate(bufferSize*2 - 1)
			},
		},
		{
			name:   "truncate-multiple-buffers-to-one",
			input:  strings.Repeat("1", bufferSize*2),
			output: "11111",
			op: func(t *testing.T, v *View) {
				v.Truncate(5)
			},
		},

		// TrimFront.
		{
			name:   "trim",
			input:  "hello world",
			output: "world",
			op: func(t *testing.T, v *View) {
				v.TrimFront(6)
			},
		},
		{
			name:   "trim-too-large",
			input:  "hello world",
			output: "",
			op: func(t *testing.T, v *View) {
				v.TrimFront(v.Size() + 1)
			},
		},
		{
			name:   "trim-multiple-buffers",
			input:  strings.Repeat("1", bufferSize*2),
			output: strings.Repeat("1", bufferSize*2-1),
			op: func(t *testing.T, v *View) {
				v.TrimFront(1)
			},
		},
		{
			name:   "trim-multiple-buffers-to-one-buffer",
			input:  strings.Repeat("1", bufferSize*2),
			output: "1",
			op: func(t *testing.T, v *View) {
				v.TrimFront(bufferSize*2 - 1)
			},
		},

		// Grow.
		{
			name:   "grow",
			input:  "hello world",
			output: "hello world",
			op: func(t *testing.T, v *View) {
				v.Grow(1, true)
			},
		},
		{
			name:   "grow-from-zero",
			output: strings.Repeat("\x00", 1024),
			op: func(t *testing.T, v *View) {
				v.Grow(1024, true)
			},
		},
		{
			name:   "grow-from-non-zero",
			input:  strings.Repeat("1", bufferSize),
			output: strings.Repeat("1", bufferSize) + strings.Repeat("\x00", bufferSize),
			op: func(t *testing.T, v *View) {
				v.Grow(bufferSize*2, true)
			},
		},

		// Copy.
		{
			name:   "copy",
			input:  "hello",
			output: "hello",
			op: func(t *testing.T, v *View) {
				other := v.Copy()
				bs := other.Flatten()
				want := []byte("hello")
				if !bytes.Equal(bs, want) {
					t.Errorf("expected %v, got %v", want, bs)
				}
			},
		},
		{
			name:   "copy-large",
			input:  strings.Repeat("1", bufferSize+1),
			output: strings.Repeat("1", bufferSize+1),
			op: func(t *testing.T, v *View) {
				other := v.Copy()
				bs := other.Flatten()
				want := []byte(strings.Repeat("1", bufferSize+1))
				if !bytes.Equal(bs, want) {
					t.Errorf("expected %v, got %v", want, bs)
				}
			},
		},

		// Merge.
		{
			name:   "merge",
			input:  "hello",
			output: "hello world",
			op: func(t *testing.T, v *View) {
				var other View
				other.Append([]byte(" world"))
				v.Merge(&other)
				if sz := other.Size(); sz != 0 {
					t.Errorf("expected 0, got %d", sz)
				}
			},
		},
		{
			name:   "merge-large",
			input:  strings.Repeat("1", bufferSize+1),
			output: strings.Repeat("1", bufferSize+1) + strings.Repeat("0", bufferSize+1),
			op: func(t *testing.T, v *View) {
				var other View
				other.Append([]byte(strings.Repeat("0", bufferSize+1)))
				v.Merge(&other)
				if sz := other.Size(); sz != 0 {
					t.Errorf("expected 0, got %d", sz)
				}
			},
		},

		// ReadAt.
		{
			name:   "readat",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, 0, 6, "hello", io.EOF) },
		},
		{
			name:   "readat-long",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, 0, 8, "hello", io.EOF) },
		},
		{
			name:   "readat-short",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, 0, 3, "hel", nil) },
		},
		{
			name:   "readat-offset",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, 2, 3, "llo", io.EOF) },
		},
		{
			name:   "readat-long-offset",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, 2, 8, "llo", io.EOF) },
		},
		{
			name:   "readat-short-offset",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, 2, 2, "ll", nil) },
		},
		{
			name:   "readat-skip-all",
			input:  "hello",
			output: "hello",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, bufferSize+1, 1, "", io.EOF) },
		},
		{
			name:   "readat-second-buffer",
			input:  strings.Repeat("0", bufferSize+1) + "12",
			output: strings.Repeat("0", bufferSize+1) + "12",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, bufferSize+1, 1, "1", nil) },
		},
		{
			name:   "readat-second-buffer-end",
			input:  strings.Repeat("0", bufferSize+1) + "12",
			output: strings.Repeat("0", bufferSize+1) + "12",
			op:     func(t *testing.T, v *View) { testReadAt(t, v, bufferSize+1, 2, "12", io.EOF) },
		},
	}

	for _, tc := range testCases {
		for fillName, fn := range fillFuncs {
			t.Run(fillName+"/"+tc.name, func(t *testing.T) {
				// Construct & fill the view.
				var view View
				fn(&view, []byte(tc.input))

				// Run the operation.
				if tc.op != nil {
					tc.op(t, &view)
				}

				// Flatten and validate.
				out := view.Flatten()
				if !bytes.Equal([]byte(tc.output), out) {
					t.Errorf("expected %q, got %q", tc.output, string(out))
				}

				// Ensure the size is correct.
				if len(out) != int(view.Size()) {
					t.Errorf("size is wrong: expected %d, got %d", len(out), view.Size())
				}

				// Calculate contents via apply.
				var appliedOut []byte
				view.Apply(func(b []byte) {
					appliedOut = append(appliedOut, b...)
				})
				if len(appliedOut) != len(out) {
					t.Errorf("expected %d, got %d", len(out), len(appliedOut))
				}
				if !bytes.Equal(appliedOut, out) {
					t.Errorf("expected %v, got %v", out, appliedOut)
				}

				// Calculate contents via ReadToWriter.
				var b bytes.Buffer
				n, err := view.ReadToWriter(&b, int64(len(out)))
				if n != int64(len(out)) {
					t.Errorf("expected %d, got %d", len(out), n)
				}
				if err != nil {
					t.Errorf("expected nil, got %v", err)
				}
				if !bytes.Equal(b.Bytes(), out) {
					t.Errorf("expected %v, got %v", out, b.Bytes())
				}
			})
		}
	}
}