// 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 binary

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"reflect"
	"strings"
	"testing"
)

func newInt32(i int32) *int32 {
	return &i
}

func TestSize(t *testing.T) {
	if got, want := Size(uint32(10)), uintptr(4); got != want {
		t.Errorf("Got = %d, want = %d", got, want)
	}
}

func TestPanic(t *testing.T) {
	tests := []struct {
		name string
		f    func([]byte, binary.ByteOrder, any)
		data any
		want string
	}{
		{"Unmarshal int", Unmarshal, 5, "invalid type: int"},
		{"Unmarshal []int", Unmarshal, []int{5}, "invalid type: int"},
		{"Marshal int", func(_ []byte, bo binary.ByteOrder, d any) { Marshal(nil, bo, d) }, 5, "invalid type: int"},
		{"Marshal int[]", func(_ []byte, bo binary.ByteOrder, d any) { Marshal(nil, bo, d) }, []int{5}, "invalid type: int"},
		{"Unmarshal short buffer", Unmarshal, newInt32(5), "runtime error: index out of range"},
		{"Unmarshal long buffer", func(_ []byte, bo binary.ByteOrder, d any) { Unmarshal(make([]byte, 50), bo, d) }, newInt32(5), "buffer too long by 46 bytes"},
		{"marshal int", func(_ []byte, bo binary.ByteOrder, d any) { marshal(nil, bo, reflect.ValueOf(d)) }, 5, "invalid type: int"},
		{"Size int", func(_ []byte, _ binary.ByteOrder, d any) { Size(d) }, 5, "invalid type: int"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			defer func() {
				r := recover()
				if got := fmt.Sprint(r); !strings.HasPrefix(got, test.want) {
					t.Errorf("Got recover() = %q, want prefix = %q", got, test.want)
				}
			}()

			test.f(nil, LittleEndian, test.data)
		})
	}
}

type inner struct {
	Field int32
}

type outer struct {
	Int8   int8
	Int16  int16
	Int32  int32
	Int64  int64
	Uint8  uint8
	Uint16 uint16
	Uint32 uint32
	Uint64 uint64

	Slice  []int32
	Array  [5]int32
	Struct inner
}

func TestMarshalUnmarshal(t *testing.T) {
	want := outer{
		1, 2, 3, 4, 5, 6, 7, 8,
		[]int32{9, 10, 11},
		[5]int32{12, 13, 14, 15, 16},
		inner{17},
	}
	buf := Marshal(nil, LittleEndian, want)
	got := outer{Slice: []int32{0, 0, 0}}
	Unmarshal(buf, LittleEndian, &got)
	if !reflect.DeepEqual(&got, &want) {
		t.Errorf("Got = %#v, want = %#v", got, want)
	}
}

type outerBenchmark struct {
	Int8   int8
	Int16  int16
	Int32  int32
	Int64  int64
	Uint8  uint8
	Uint16 uint16
	Uint32 uint32
	Uint64 uint64

	Array  [5]int32
	Struct inner
}

func BenchmarkMarshalUnmarshal(b *testing.B) {
	b.ReportAllocs()

	in := outerBenchmark{
		1, 2, 3, 4, 5, 6, 7, 8,
		[5]int32{9, 10, 11, 12, 13},
		inner{14},
	}
	buf := make([]byte, Size(&in))
	out := outerBenchmark{}

	for i := 0; i < b.N; i++ {
		buf := Marshal(buf[:0], LittleEndian, &in)
		Unmarshal(buf, LittleEndian, &out)
	}
}

func BenchmarkReadWrite(b *testing.B) {
	b.ReportAllocs()

	in := outerBenchmark{
		1, 2, 3, 4, 5, 6, 7, 8,
		[5]int32{9, 10, 11, 12, 13},
		inner{14},
	}
	buf := bytes.NewBuffer(make([]byte, binary.Size(&in)))
	out := outerBenchmark{}

	for i := 0; i < b.N; i++ {
		buf.Reset()
		if err := binary.Write(buf, LittleEndian, &in); err != nil {
			b.Error("Write:", err)
		}
		if err := binary.Read(buf, LittleEndian, &out); err != nil {
			b.Error("Read:", err)
		}
	}
}

type outerPadding struct {
	_ int8
	_ int16
	_ int32
	_ int64
	_ uint8
	_ uint16
	_ uint32
	_ uint64

	_ []int32
	_ [5]int32
	_ inner
}

func TestMarshalUnmarshalPadding(t *testing.T) {
	var want outerPadding
	buf := Marshal(nil, LittleEndian, want)
	var got outerPadding
	Unmarshal(buf, LittleEndian, &got)
	if !reflect.DeepEqual(&got, &want) {
		t.Errorf("Got = %#v, want = %#v", got, want)
	}
}

// Numbers with bits in every byte that distinguishable in big and little endian.
const (
	want16 = 64<<8 | 128
	want32 = 16<<24 | 32<<16 | want16
	want64 = 1<<56 | 2<<48 | 4<<40 | 8<<32 | want32
)

func TestReadWriteUint16(t *testing.T) {
	const want = uint16(want16)
	var buf bytes.Buffer
	if err := WriteUint16(&buf, LittleEndian, want); err != nil {
		t.Error("WriteUint16:", err)
	}
	got, err := ReadUint16(&buf, LittleEndian)
	if err != nil {
		t.Error("ReadUint16:", err)
	}
	if got != want {
		t.Errorf("got = %d, want = %d", got, want)
	}
}

func TestReadWriteUint32(t *testing.T) {
	const want = uint32(want32)
	var buf bytes.Buffer
	if err := WriteUint32(&buf, LittleEndian, want); err != nil {
		t.Error("WriteUint32:", err)
	}
	got, err := ReadUint32(&buf, LittleEndian)
	if err != nil {
		t.Error("ReadUint32:", err)
	}
	if got != want {
		t.Errorf("got = %d, want = %d", got, want)
	}
}

func TestReadWriteUint64(t *testing.T) {
	const want = uint64(want64)
	var buf bytes.Buffer
	if err := WriteUint64(&buf, LittleEndian, want); err != nil {
		t.Error("WriteUint64:", err)
	}
	got, err := ReadUint64(&buf, LittleEndian)
	if err != nil {
		t.Error("ReadUint64:", err)
	}
	if got != want {
		t.Errorf("got = %d, want = %d", got, want)
	}
}

type readWriter struct {
	err error
}

func (rw *readWriter) Write([]byte) (int, error) {
	return 0, rw.err
}

func (rw *readWriter) Read([]byte) (int, error) {
	return 0, rw.err
}

func TestReadWriteError(t *testing.T) {
	tests := []struct {
		name string
		f    func(rw io.ReadWriter) error
	}{
		{"WriteUint16", func(rw io.ReadWriter) error { return WriteUint16(rw, LittleEndian, 0) }},
		{"ReadUint16", func(rw io.ReadWriter) error { _, err := ReadUint16(rw, LittleEndian); return err }},
		{"WriteUint32", func(rw io.ReadWriter) error { return WriteUint32(rw, LittleEndian, 0) }},
		{"ReadUint32", func(rw io.ReadWriter) error { _, err := ReadUint32(rw, LittleEndian); return err }},
		{"WriteUint64", func(rw io.ReadWriter) error { return WriteUint64(rw, LittleEndian, 0) }},
		{"ReadUint64", func(rw io.ReadWriter) error { _, err := ReadUint64(rw, LittleEndian); return err }},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			want := errors.New("want")
			if got := test.f(&readWriter{want}); got != want {
				t.Errorf("got = %v, want = %v", got, want)
			}
		})
	}
}
