// Copyright (C) MongoDB, Inc. 2017-present.
//
// 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

package bsonrw

import (
	"bytes"
	"fmt"
	"io"
	"math"
	"testing"

	"github.com/google/go-cmp/cmp"
	"go.mongodb.org/mongo-driver/bson/bsontype"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)

func TestValueReader(t *testing.T) {
	t.Run("ReadBinary", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			btype  byte
			b      []byte
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				0,
				nil,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Binary),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				0,
				nil,
				io.EOF,
				bsontype.Binary,
			},
			{
				"no byte available",
				[]byte{0x00, 0x00, 0x00, 0x00},
				0,
				0,
				nil,
				io.EOF,
				bsontype.Binary,
			},
			{
				"not enough bytes for binary",
				[]byte{0x05, 0x00, 0x00, 0x00, 0x00},
				0,
				0,
				nil,
				io.EOF,
				bsontype.Binary,
			},
			{
				"success",
				[]byte{0x03, 0x00, 0x00, 0x00, 0xEA, 0x01, 0x02, 0x03},
				0,
				0xEA,
				[]byte{0x01, 0x02, 0x03},
				nil,
				bsontype.Binary,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				b, btype, err := vr.ReadBinary()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if btype != tc.btype {
					t.Errorf("Incorrect binary type returned. got %v; want %v", btype, tc.btype)
				}
				if !bytes.Equal(b, tc.b) {
					t.Errorf("Binary data does not match. got %v; want %v", b, tc.b)
				}
			})
		}
	})
	t.Run("ReadBoolean", func(t *testing.T) {
		testCases := []struct {
			name    string
			data    []byte
			offset  int64
			boolean bool
			err     error
			vType   bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				false,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Boolean),
				bsontype.EmbeddedDocument,
			},
			{
				"no byte available",
				[]byte{},
				0,
				false,
				io.EOF,
				bsontype.Boolean,
			},
			{
				"invalid byte for boolean",
				[]byte{0x03},
				0,
				false,
				fmt.Errorf("invalid byte for boolean, %b", 0x03),
				bsontype.Boolean,
			},
			{
				"success",
				[]byte{0x01},
				0,
				true,
				nil,
				bsontype.Boolean,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				boolean, err := vr.ReadBoolean()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if boolean != tc.boolean {
					t.Errorf("Incorrect boolean returned. got %v; want %v", boolean, tc.boolean)
				}
			})
		}
	})
	t.Run("ReadDocument", func(t *testing.T) {
		t.Run("TopLevel", func(t *testing.T) {
			doc := []byte{0x05, 0x00, 0x00, 0x00, 0x00}
			vr := &valueReader{
				offset: 0,
				stack:  []vrState{{mode: mTopLevel}},
				frame:  0,
			}

			// invalid length
			vr.d = []byte{0x00, 0x00}
			_, err := vr.ReadDocument()
			if err != io.EOF {
				t.Errorf("Expected io.EOF with document length too small. got %v; want %v", err, io.EOF)
			}

			vr.d = doc
			_, err = vr.ReadDocument()
			noerr(t, err)
			if vr.stack[vr.frame].end != 5 {
				t.Errorf("Incorrect end for document. got %d; want %d", vr.stack[vr.frame].end, 5)
			}
		})
		t.Run("EmbeddedDocument", func(t *testing.T) {
			vr := &valueReader{
				offset: 0,
				stack: []vrState{
					{mode: mTopLevel},
					{mode: mElement, vType: bsontype.Boolean},
				},
				frame: 1,
			}

			var wanterr = (&valueReader{stack: []vrState{{mode: mElement, vType: bsontype.Boolean}}}).typeError(bsontype.EmbeddedDocument)
			_, err := vr.ReadDocument()
			if err == nil || err.Error() != wanterr.Error() {
				t.Errorf("Incorrect returned error. got %v; want %v", err, wanterr)
			}

			vr.stack[1].mode = mArray
			wanterr = vr.invalidTransitionErr(mDocument, "ReadDocument", []mode{mTopLevel, mElement, mValue})
			_, err = vr.ReadDocument()
			if err == nil || err.Error() != wanterr.Error() {
				t.Errorf("Incorrect returned error. got %v; want %v", err, wanterr)
			}

			vr.stack[1].mode, vr.stack[1].vType = mElement, bsontype.EmbeddedDocument
			vr.d = []byte{0x0A, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00}
			vr.offset = 4
			_, err = vr.ReadDocument()
			noerr(t, err)
			if len(vr.stack) != 3 {
				t.Errorf("Incorrect number of stack frames. got %d; want %d", len(vr.stack), 3)
			}
			if vr.stack[2].mode != mDocument {
				t.Errorf("Incorrect mode set. got %v; want %v", vr.stack[2].mode, mDocument)
			}
			if vr.stack[2].end != 9 {
				t.Errorf("End of embedded document is not correct. got %d; want %d", vr.stack[2].end, 9)
			}
			if vr.offset != 8 {
				t.Errorf("Offset not incremented correctly. got %d; want %d", vr.offset, 8)
			}

			vr.frame--
			_, err = vr.ReadDocument()
			if err != io.EOF {
				t.Errorf("Should return error when attempting to read length with not enough bytes. got %v; want %v", err, io.EOF)
			}
		})
	})
	t.Run("ReadBinary", func(t *testing.T) {
		codeWithScope := []byte{
			0x11, 0x00, 0x00, 0x00, // total length
			0x4, 0x00, 0x00, 0x00, // string length
			'f', 'o', 'o', 0x00, // string
			0x05, 0x00, 0x00, 0x00, 0x00, // document
		}
		mismatchCodeWithScope := []byte{
			0x11, 0x00, 0x00, 0x00, // total length
			0x4, 0x00, 0x00, 0x00, // string length
			'f', 'o', 'o', 0x00, // string
			0x07, 0x00, 0x00, 0x00, // document
			0x0A, 0x00, // null element, empty key
			0x00, // document end
		}
		invalidCodeWithScope := []byte{
			0x7, 0x00, 0x00, 0x00, // total length
			0x0, 0x00, 0x00, 0x00, // string length = 0
			0x05, 0x00, 0x00, 0x00, 0x00, // document
		}
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.CodeWithScope),
				bsontype.EmbeddedDocument,
			},
			{
				"total length not enough bytes",
				[]byte{},
				0,
				io.EOF,
				bsontype.CodeWithScope,
			},
			{
				"string length not enough bytes",
				codeWithScope[:4],
				0,
				io.EOF,
				bsontype.CodeWithScope,
			},
			{
				"not enough string bytes",
				codeWithScope[:8],
				0,
				io.EOF,
				bsontype.CodeWithScope,
			},
			{
				"document length not enough bytes",
				codeWithScope[:12],
				0,
				io.EOF,
				bsontype.CodeWithScope,
			},
			{
				"length mismatch",
				mismatchCodeWithScope,
				0,
				fmt.Errorf("length of CodeWithScope does not match lengths of components; total: %d; components: %d", 17, 19),
				bsontype.CodeWithScope,
			},
			{
				"invalid strLength",
				invalidCodeWithScope,
				0,
				fmt.Errorf("invalid string length: %d", 0),
				bsontype.CodeWithScope,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				_, _, err := vr.ReadCodeWithScope()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
			})
		}

		t.Run("success", func(t *testing.T) {
			doc := []byte{0x00, 0x00, 0x00, 0x00}
			doc = append(doc, codeWithScope...)
			doc = append(doc, 0x00)
			vr := &valueReader{
				offset: 4,
				d:      doc,
				stack: []vrState{
					{mode: mTopLevel},
					{mode: mElement, vType: bsontype.CodeWithScope},
				},
				frame: 1,
			}

			code, _, err := vr.ReadCodeWithScope()
			noerr(t, err)
			if code != "foo" {
				t.Errorf("Code does not match. got %s; want %s", code, "foo")
			}
			if len(vr.stack) != 3 {
				t.Errorf("Incorrect number of stack frames. got %d; want %d", len(vr.stack), 3)
			}
			if vr.stack[2].mode != mCodeWithScope {
				t.Errorf("Incorrect mode set. got %v; want %v", vr.stack[2].mode, mDocument)
			}
			if vr.stack[2].end != 21 {
				t.Errorf("End of scope is not correct. got %d; want %d", vr.stack[2].end, 21)
			}
			if vr.offset != 20 {
				t.Errorf("Offset not incremented correctly. got %d; want %d", vr.offset, 20)
			}
		})
	})
	t.Run("ReadDBPointer", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			ns     string
			oid    primitive.ObjectID
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				"",
				primitive.ObjectID{},
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.DBPointer),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				"",
				primitive.ObjectID{},
				io.EOF,
				bsontype.DBPointer,
			},
			{
				"not enough bytes for namespace",
				[]byte{0x04, 0x00, 0x00, 0x00},
				0,
				"",
				primitive.ObjectID{},
				io.EOF,
				bsontype.DBPointer,
			},
			{
				"not enough bytes for objectID",
				[]byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00},
				0,
				"",
				primitive.ObjectID{},
				io.EOF,
				bsontype.DBPointer,
			},
			{
				"success",
				[]byte{
					0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00,
					0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
				},
				0,
				"foo",
				primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C},
				nil,
				bsontype.DBPointer,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				ns, oid, err := vr.ReadDBPointer()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if ns != tc.ns {
					t.Errorf("Incorrect namespace returned. got %v; want %v", ns, tc.ns)
				}
				if oid != tc.oid {
					t.Errorf("ObjectIDs did not match. got %v; want %v", oid, tc.oid)
				}
			})
		}
	})
	t.Run("ReadDateTime", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			dt     int64
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				0,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.DateTime),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				0,
				io.EOF,
				bsontype.DateTime,
			},
			{
				"success",
				[]byte{0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
				0,
				255,
				nil,
				bsontype.DateTime,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				dt, err := vr.ReadDateTime()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if dt != tc.dt {
					t.Errorf("Incorrect datetime returned. got %d; want %d", dt, tc.dt)
				}
			})
		}
	})
	t.Run("ReadDecimal128", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			dc128  primitive.Decimal128
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				primitive.Decimal128{},
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Decimal128),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				primitive.Decimal128{},
				io.EOF,
				bsontype.Decimal128,
			},
			{
				"success",
				[]byte{
					0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Low
					0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // High
				},
				0,
				primitive.NewDecimal128(65280, 255),
				nil,
				bsontype.Decimal128,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				dc128, err := vr.ReadDecimal128()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				gotHigh, gotLow := dc128.GetBytes()
				wantHigh, wantLow := tc.dc128.GetBytes()
				if gotHigh != wantHigh {
					t.Errorf("Retuired high byte does not match. got %d; want %d", gotHigh, wantHigh)
				}
				if gotLow != wantLow {
					t.Errorf("Returned low byte does not match. got %d; want %d", gotLow, wantLow)
				}
			})
		}
	})
	t.Run("ReadDouble", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			double float64
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				0,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Double),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				0,
				io.EOF,
				bsontype.Double,
			},
			{
				"success",
				[]byte{0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
				0,
				math.Float64frombits(255),
				nil,
				bsontype.Double,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				double, err := vr.ReadDouble()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if double != tc.double {
					t.Errorf("Incorrect double returned. got %f; want %f", double, tc.double)
				}
			})
		}
	})
	t.Run("ReadInt32", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			i32    int32
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				0,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Int32),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				0,
				io.EOF,
				bsontype.Int32,
			},
			{
				"success",
				[]byte{0xFF, 0x00, 0x00, 0x00},
				0,
				255,
				nil,
				bsontype.Int32,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				i32, err := vr.ReadInt32()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if i32 != tc.i32 {
					t.Errorf("Incorrect int32 returned. got %d; want %d", i32, tc.i32)
				}
			})
		}
	})
	t.Run("ReadInt32", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			i64    int64
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				0,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Int64),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				0,
				io.EOF,
				bsontype.Int64,
			},
			{
				"success",
				[]byte{0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
				0,
				255,
				nil,
				bsontype.Int64,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				i64, err := vr.ReadInt64()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if i64 != tc.i64 {
					t.Errorf("Incorrect int64 returned. got %d; want %d", i64, tc.i64)
				}
			})
		}
	})
	t.Run("ReadJavascript/ReadString/ReadSymbol", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			fn     func(*valueReader) (string, error)
			css    string // code, string, symbol :P
			err    error
			vType  bsontype.Type
		}{
			{
				"ReadJavascript/incorrect type",
				[]byte{},
				0,
				(*valueReader).ReadJavascript,
				"",
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.JavaScript),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadString/incorrect type",
				[]byte{},
				0,
				(*valueReader).ReadString,
				"",
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.String),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadSymbol/incorrect type",
				[]byte{},
				0,
				(*valueReader).ReadSymbol,
				"",
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Symbol),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadJavascript/length too short",
				[]byte{},
				0,
				(*valueReader).ReadJavascript,
				"",
				io.EOF,
				bsontype.JavaScript,
			},
			{
				"ReadString/length too short",
				[]byte{},
				0,
				(*valueReader).ReadString,
				"",
				io.EOF,
				bsontype.String,
			},
			{
				"ReadSymbol/length too short",
				[]byte{},
				0,
				(*valueReader).ReadSymbol,
				"",
				io.EOF,
				bsontype.Symbol,
			},
			{
				"ReadJavascript/incorrect end byte",
				[]byte{0x01, 0x00, 0x00, 0x00, 0x05},
				0,
				(*valueReader).ReadJavascript,
				"",
				fmt.Errorf("string does not end with null byte, but with %v", 0x05),
				bsontype.JavaScript,
			},
			{
				"ReadString/incorrect end byte",
				[]byte{0x01, 0x00, 0x00, 0x00, 0x05},
				0,
				(*valueReader).ReadString,
				"",
				fmt.Errorf("string does not end with null byte, but with %v", 0x05),
				bsontype.String,
			},
			{
				"ReadSymbol/incorrect end byte",
				[]byte{0x01, 0x00, 0x00, 0x00, 0x05},
				0,
				(*valueReader).ReadSymbol,
				"",
				fmt.Errorf("string does not end with null byte, but with %v", 0x05),
				bsontype.Symbol,
			},
			{
				"ReadJavascript/success",
				[]byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00},
				0,
				(*valueReader).ReadJavascript,
				"foo",
				nil,
				bsontype.JavaScript,
			},
			{
				"ReadString/success",
				[]byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00},
				0,
				(*valueReader).ReadString,
				"foo",
				nil,
				bsontype.String,
			},
			{
				"ReadSymbol/success",
				[]byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00},
				0,
				(*valueReader).ReadSymbol,
				"foo",
				nil,
				bsontype.Symbol,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				css, err := tc.fn(vr)
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if css != tc.css {
					t.Errorf("Incorrect (JavaScript,String,Symbol) returned. got %s; want %s", css, tc.css)
				}
			})
		}
	})
	t.Run("ReadMaxKey/ReadMinKey/ReadNull/ReadUndefined", func(t *testing.T) {
		testCases := []struct {
			name  string
			fn    func(*valueReader) error
			err   error
			vType bsontype.Type
		}{
			{
				"ReadMaxKey/incorrect type",
				(*valueReader).ReadMaxKey,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.MaxKey),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadMaxKey/success",
				(*valueReader).ReadMaxKey,
				nil,
				bsontype.MaxKey,
			},
			{
				"ReadMinKey/incorrect type",
				(*valueReader).ReadMinKey,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.MinKey),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadMinKey/success",
				(*valueReader).ReadMinKey,
				nil,
				bsontype.MinKey,
			},
			{
				"ReadNull/incorrect type",
				(*valueReader).ReadNull,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Null),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadNull/success",
				(*valueReader).ReadNull,
				nil,
				bsontype.Null,
			},
			{
				"ReadUndefined/incorrect type",
				(*valueReader).ReadUndefined,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Undefined),
				bsontype.EmbeddedDocument,
			},
			{
				"ReadUndefined/success",
				(*valueReader).ReadUndefined,
				nil,
				bsontype.Undefined,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				err := tc.fn(vr)
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
			})
		}
	})
	t.Run("ReadObjectID", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			oid    primitive.ObjectID
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				primitive.ObjectID{},
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.ObjectID),
				bsontype.EmbeddedDocument,
			},
			{
				"not enough bytes for objectID",
				[]byte{},
				0,
				primitive.ObjectID{},
				io.EOF,
				bsontype.ObjectID,
			},
			{
				"success",
				[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C},
				0,
				primitive.ObjectID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C},
				nil,
				bsontype.ObjectID,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				oid, err := vr.ReadObjectID()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if oid != tc.oid {
					t.Errorf("ObjectIDs did not match. got %v; want %v", oid, tc.oid)
				}
			})
		}
	})
	t.Run("ReadRegex", func(t *testing.T) {
		testCases := []struct {
			name    string
			data    []byte
			offset  int64
			pattern string
			options string
			err     error
			vType   bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				"",
				"",
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Regex),
				bsontype.EmbeddedDocument,
			},
			{
				"length too short",
				[]byte{},
				0,
				"",
				"",
				io.EOF,
				bsontype.Regex,
			},
			{
				"not enough bytes for options",
				[]byte{'f', 'o', 'o', 0x00},
				0,
				"",
				"",
				io.EOF,
				bsontype.Regex,
			},
			{
				"success",
				[]byte{'f', 'o', 'o', 0x00, 'b', 'a', 'r', 0x00},
				0,
				"foo",
				"bar",
				nil,
				bsontype.Regex,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				pattern, options, err := vr.ReadRegex()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if pattern != tc.pattern {
					t.Errorf("Incorrect pattern returned. got %s; want %s", pattern, tc.pattern)
				}
				if options != tc.options {
					t.Errorf("Incorrect options returned. got %s; want %s", options, tc.options)
				}
			})
		}
	})
	t.Run("ReadTimestamp", func(t *testing.T) {
		testCases := []struct {
			name   string
			data   []byte
			offset int64
			ts     uint32
			incr   uint32
			err    error
			vType  bsontype.Type
		}{
			{
				"incorrect type",
				[]byte{},
				0,
				0,
				0,
				(&valueReader{stack: []vrState{{vType: bsontype.EmbeddedDocument}}, frame: 0}).typeError(bsontype.Timestamp),
				bsontype.EmbeddedDocument,
			},
			{
				"not enough bytes for increment",
				[]byte{},
				0,
				0,
				0,
				io.EOF,
				bsontype.Timestamp,
			},
			{
				"not enough bytes for timestamp",
				[]byte{0x01, 0x02, 0x03, 0x04},
				0,
				0,
				0,
				io.EOF,
				bsontype.Timestamp,
			},
			{
				"success",
				[]byte{0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00},
				0,
				256,
				255,
				nil,
				bsontype.Timestamp,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				vr := &valueReader{
					offset: tc.offset,
					d:      tc.data,
					stack: []vrState{
						{mode: mTopLevel},
						{
							mode:  mElement,
							vType: tc.vType,
						},
					},
					frame: 1,
				}

				ts, incr, err := vr.ReadTimestamp()
				if !errequal(t, err, tc.err) {
					t.Errorf("Returned errors do not match. got %v; want %v", err, tc.err)
				}
				if ts != tc.ts {
					t.Errorf("Incorrect timestamp returned. got %d; want %d", ts, tc.ts)
				}
				if incr != tc.incr {
					t.Errorf("Incorrect increment returned. got %d; want %d", incr, tc.incr)
				}
			})
		}
	})

	t.Run("ReadBytes & Skip", func(t *testing.T) {
		index, docb := bsoncore.ReserveLength(nil)
		docb = bsoncore.AppendNullElement(docb, "foobar")
		docb = append(docb, 0x00)
		docb = bsoncore.UpdateLength(docb, index, int32(len(docb)))
		cwsbytes := bsoncore.AppendCodeWithScope(nil, "var hellow = world;", docb)
		strbytes := []byte{0x04, 0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00}
		testCases := []struct {
			name           string
			t              bsontype.Type
			data           []byte
			err            error
			offset         int64
			startingOffset int64
		}{
			{
				"Array/invalid length",
				bsontype.Array,
				[]byte{0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"Array/not enough bytes",
				bsontype.Array,
				[]byte{0x0F, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"Array/success",
				bsontype.Array,
				[]byte{0x08, 0x00, 0x00, 0x00, 0x0A, '1', 0x00, 0x00},
				nil, 8, 0,
			},
			{
				"EmbeddedDocument/invalid length",
				bsontype.EmbeddedDocument,
				[]byte{0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"EmbeddedDocument/not enough bytes",
				bsontype.EmbeddedDocument,
				[]byte{0x0F, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"EmbeddedDocument/success",
				bsontype.EmbeddedDocument,
				[]byte{0x08, 0x00, 0x00, 0x00, 0x0A, 'A', 0x00, 0x00},
				nil, 8, 0,
			},
			{
				"CodeWithScope/invalid length",
				bsontype.CodeWithScope,
				[]byte{0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"CodeWithScope/not enough bytes",
				bsontype.CodeWithScope,
				[]byte{0x0F, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"CodeWithScope/success",
				bsontype.CodeWithScope,
				cwsbytes,
				nil, 41, 0,
			},
			{
				"Binary/invalid length",
				bsontype.Binary,
				[]byte{0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"Binary/not enough bytes",
				bsontype.Binary,
				[]byte{0x0F, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"Binary/success",
				bsontype.Binary,
				[]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03},
				nil, 8, 0,
			},
			{
				"Boolean/invalid length",
				bsontype.Boolean,
				[]byte{},
				io.EOF, 0, 0,
			},
			{
				"Boolean/success",
				bsontype.Boolean,
				[]byte{0x01},
				nil, 1, 0,
			},
			{
				"DBPointer/invalid length",
				bsontype.DBPointer,
				[]byte{0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"DBPointer/not enough bytes",
				bsontype.DBPointer,
				[]byte{0x0F, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03},
				io.EOF, 0, 0,
			},
			{
				"DBPointer/success",
				bsontype.DBPointer,
				[]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C},
				nil, 17, 0,
			},
			{"DBPointer/not enough bytes", bsontype.DateTime, []byte{0x01, 0x02, 0x03, 0x04}, io.EOF, 0, 0},
			{"DBPointer/success", bsontype.DateTime, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, nil, 8, 0},
			{"Double/not enough bytes", bsontype.Double, []byte{0x01, 0x02, 0x03, 0x04}, io.EOF, 0, 0},
			{"Double/success", bsontype.Double, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, nil, 8, 0},
			{"Int64/not enough bytes", bsontype.Int64, []byte{0x01, 0x02, 0x03, 0x04}, io.EOF, 0, 0},
			{"Int64/success", bsontype.Int64, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, nil, 8, 0},
			{"Timestamp/not enough bytes", bsontype.Timestamp, []byte{0x01, 0x02, 0x03, 0x04}, io.EOF, 0, 0},
			{"Timestamp/success", bsontype.Timestamp, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, nil, 8, 0},
			{
				"Decimal128/not enough bytes",
				bsontype.Decimal128,
				[]byte{0x01, 0x02, 0x03, 0x04},
				io.EOF, 0, 0,
			},
			{
				"Decimal128/success",
				bsontype.Decimal128,
				[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10},
				nil, 16, 0,
			},
			{"Int32/not enough bytes", bsontype.Int32, []byte{0x01, 0x02}, io.EOF, 0, 0},
			{"Int32/success", bsontype.Int32, []byte{0x01, 0x02, 0x03, 0x04}, nil, 4, 0},
			{"Javascript/invalid length", bsontype.JavaScript, strbytes[:2], io.EOF, 0, 0},
			{"Javascript/not enough bytes", bsontype.JavaScript, strbytes[:5], io.EOF, 0, 0},
			{"Javascript/success", bsontype.JavaScript, strbytes, nil, 8, 0},
			{"String/invalid length", bsontype.String, strbytes[:2], io.EOF, 0, 0},
			{"String/not enough bytes", bsontype.String, strbytes[:5], io.EOF, 0, 0},
			{"String/success", bsontype.String, strbytes, nil, 8, 0},
			{"Symbol/invalid length", bsontype.Symbol, strbytes[:2], io.EOF, 0, 0},
			{"Symbol/not enough bytes", bsontype.Symbol, strbytes[:5], io.EOF, 0, 0},
			{"Symbol/success", bsontype.Symbol, strbytes, nil, 8, 0},
			{"MaxKey/success", bsontype.MaxKey, []byte{}, nil, 0, 0},
			{"MinKey/success", bsontype.MinKey, []byte{}, nil, 0, 0},
			{"Null/success", bsontype.Null, []byte{}, nil, 0, 0},
			{"Undefined/success", bsontype.Undefined, []byte{}, nil, 0, 0},
			{
				"ObjectID/not enough bytes",
				bsontype.ObjectID,
				[]byte{0x01, 0x02, 0x03, 0x04},
				io.EOF, 0, 0,
			},
			{
				"ObjectID/success",
				bsontype.ObjectID,
				[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C},
				nil, 12, 0,
			},
			{
				"Regex/not enough bytes (first string)",
				bsontype.Regex,
				[]byte{'f', 'o', 'o'},
				io.EOF, 0, 0,
			},
			{
				"Regex/not enough bytes (second string)",
				bsontype.Regex,
				[]byte{'f', 'o', 'o', 0x00, 'b', 'a', 'r'},
				io.EOF, 0, 0,
			},
			{
				"Regex/success",
				bsontype.Regex,
				[]byte{0x00, 0x00, 0x00, 'f', 'o', 'o', 0x00, 'i', 0x00},
				nil, 9, 3,
			},
			{
				"Unknown Type",
				bsontype.Type(0),
				nil,
				fmt.Errorf("attempted to read bytes of unknown BSON type %v", bsontype.Type(0)), 0, 0,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				t.Run("Skip", func(t *testing.T) {
					vr := &valueReader{
						d: tc.data,
						stack: []vrState{
							{mode: mTopLevel},
							{mode: mElement, vType: tc.t},
						},
						frame:  1,
						offset: tc.startingOffset,
					}

					err := vr.Skip()
					if !errequal(t, err, tc.err) {
						t.Errorf("Did not receive expected error; got %v; want %v", err, tc.err)
					}
					if tc.err == nil && vr.offset != tc.offset {
						t.Errorf("Offset not set at correct position; got %d; want %d", vr.offset, tc.offset)
					}
				})
				t.Run("ReadBytes", func(t *testing.T) {
					vr := &valueReader{
						d: tc.data,
						stack: []vrState{
							{mode: mTopLevel},
							{mode: mElement, vType: tc.t},
						},
						frame:  1,
						offset: tc.startingOffset,
					}

					_, got, err := vr.ReadValueBytes(nil)
					if !errequal(t, err, tc.err) {
						t.Errorf("Did not receive expected error; got %v; want %v", err, tc.err)
					}
					if tc.err == nil && vr.offset != tc.offset {
						t.Errorf("Offset not set at correct position; got %d; want %d", vr.offset, tc.offset)
					}
					if tc.err == nil && !bytes.Equal(got, tc.data[tc.startingOffset:]) {
						t.Errorf("Did not receive expected bytes. got %v; want %v", got, tc.data[tc.startingOffset:])
					}
				})
			})
		}
		t.Run("ReadValueBytes/Top Level Doc", func(t *testing.T) {
			testCases := []struct {
				name     string
				want     []byte
				wantType bsontype.Type
				wantErr  error
			}{
				{
					"success",
					bsoncore.BuildDocument(nil, bsoncore.AppendDoubleElement(nil, "pi", 3.14159)),
					bsontype.Type(0),
					nil,
				},
				{
					"wrong length",
					[]byte{0x01, 0x02, 0x03},
					bsontype.Type(0),
					io.EOF,
				},
				{
					"append bytes",
					[]byte{0x01, 0x02, 0x03, 0x04},
					bsontype.Type(0),
					io.EOF,
				},
			}

			for _, tc := range testCases {
				tc := tc
				t.Run(tc.name, func(t *testing.T) {
					t.Parallel()
					vr := &valueReader{
						d: tc.want,
						stack: []vrState{
							{mode: mTopLevel},
						},
						frame: 0,
					}
					gotType, got, gotErr := vr.ReadValueBytes(nil)
					if gotErr != tc.wantErr {
						t.Errorf("Did not receive expected error. got %v; want %v", gotErr, tc.wantErr)
					}
					if tc.wantErr == nil && gotType != tc.wantType {
						t.Errorf("Did not receive expected type. got %v; want %v", gotType, tc.wantType)
					}
					if tc.wantErr == nil && !bytes.Equal(got, tc.want) {
						t.Errorf("Did not receive expected bytes. got %v; want %v", got, tc.want)
					}
				})
			}
		})
	})

	t.Run("invalid transition", func(t *testing.T) {
		t.Run("Skip", func(t *testing.T) {
			vr := &valueReader{stack: []vrState{{mode: mTopLevel}}}
			wanterr := (&valueReader{stack: []vrState{{mode: mTopLevel}}}).invalidTransitionErr(0, "Skip", []mode{mElement, mValue})
			goterr := vr.Skip()
			if !cmp.Equal(goterr, wanterr, cmp.Comparer(compareErrors)) {
				t.Errorf("Expected correct invalid transition error. got %v; want %v", goterr, wanterr)
			}
		})
	})
	t.Run("ReadBytes", func(t *testing.T) {
		vr := &valueReader{stack: []vrState{{mode: mTopLevel}, {mode: mDocument}}, frame: 1}
		wanterr := (&valueReader{stack: []vrState{{mode: mTopLevel}, {mode: mDocument}}, frame: 1}).
			invalidTransitionErr(0, "ReadValueBytes", []mode{mElement, mValue})
		_, _, goterr := vr.ReadValueBytes(nil)
		if !cmp.Equal(goterr, wanterr, cmp.Comparer(compareErrors)) {
			t.Errorf("Expected correct invalid transition error. got %v; want %v", goterr, wanterr)
		}
	})
}

func errequal(t *testing.T, err1, err2 error) bool {
	t.Helper()
	if err1 == nil && err2 == nil { // If they are both nil, they are equal
		return true
	}
	if err1 == nil || err2 == nil { // If only one is nil, they are not equal
		return false
	}

	if err1 == err2 { // They are the same error, they are equal
		return true
	}

	if err1.Error() == err2.Error() { // They string formats match, they are equal
		return true
	}

	return false
}
