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

import (
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"testing"
)

type encodetest struct {
	Field1String  string
	Field1Int64   int64
	Field1Float64 float64
	Field2String  string
	Field2Int64   int64
	Field2Float64 float64
	Field3String  string
	Field3Int64   int64
	Field3Float64 float64
	Field4String  string
	Field4Int64   int64
	Field4Float64 float64
}

type nestedtest1 struct {
	Nested nestedtest2
}

type nestedtest2 struct {
	Nested nestedtest3
}

type nestedtest3 struct {
	Nested nestedtest4
}

type nestedtest4 struct {
	Nested nestedtest5
}

type nestedtest5 struct {
	Nested nestedtest6
}

type nestedtest6 struct {
	Nested nestedtest7
}

type nestedtest7 struct {
	Nested nestedtest8
}

type nestedtest8 struct {
	Nested nestedtest9
}

type nestedtest9 struct {
	Nested nestedtest10
}

type nestedtest10 struct {
	Nested nestedtest11
}

type nestedtest11 struct {
	Nested encodetest
}

var encodetestInstance = encodetest{
	Field1String:  "foo",
	Field1Int64:   1,
	Field1Float64: 3.0,
	Field2String:  "bar",
	Field2Int64:   2,
	Field2Float64: 3.1,
	Field3String:  "baz",
	Field3Int64:   3,
	Field3Float64: 3.14,
	Field4String:  "qux",
	Field4Int64:   4,
	Field4Float64: 3.141,
}

var nestedInstance = nestedtest1{
	nestedtest2{
		nestedtest3{
			nestedtest4{
				nestedtest5{
					nestedtest6{
						nestedtest7{
							nestedtest8{
								nestedtest9{
									nestedtest10{
										nestedtest11{
											encodetest{
												Field1String:  "foo",
												Field1Int64:   1,
												Field1Float64: 3.0,
												Field2String:  "bar",
												Field2Int64:   2,
												Field2Float64: 3.1,
												Field3String:  "baz",
												Field3Int64:   3,
												Field3Float64: 3.14,
												Field4String:  "qux",
												Field4Int64:   4,
												Field4Float64: 3.141,
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	},
}

const extendedBSONDir = "../testdata/extended_bson"

// readExtJSONFile reads the GZIP-compressed extended JSON document from the given filename in the
// "extended BSON" test data directory (../testdata/extended_bson) and returns it as a
// map[string]interface{}. It panics on any errors.
func readExtJSONFile(filename string) map[string]interface{} {
	filePath := path.Join(extendedBSONDir, filename)
	file, err := os.Open(filePath)
	if err != nil {
		panic(fmt.Sprintf("error opening file %q: %s", filePath, err))
	}
	defer func() {
		_ = file.Close()
	}()

	gz, err := gzip.NewReader(file)
	if err != nil {
		panic(fmt.Sprintf("error creating GZIP reader: %s", err))
	}
	defer func() {
		_ = gz.Close()
	}()

	data, err := ioutil.ReadAll(gz)
	if err != nil {
		panic(fmt.Sprintf("error reading GZIP contents of file: %s", err))
	}

	var v map[string]interface{}
	err = UnmarshalExtJSON(data, false, &v)
	if err != nil {
		panic(fmt.Sprintf("error unmarshalling extended JSON: %s", err))
	}

	return v
}

func BenchmarkMarshal(b *testing.B) {
	cases := []struct {
		desc  string
		value interface{}
	}{
		{
			desc:  "simple struct",
			value: encodetestInstance,
		},
		{
			desc:  "nested struct",
			value: nestedInstance,
		},
		{
			desc:  "deep_bson.json.gz",
			value: readExtJSONFile("deep_bson.json.gz"),
		},
		{
			desc:  "flat_bson.json.gz",
			value: readExtJSONFile("flat_bson.json.gz"),
		},
		{
			desc:  "full_bson.json.gz",
			value: readExtJSONFile("full_bson.json.gz"),
		},
	}

	for _, tc := range cases {
		b.Run(tc.desc, func(b *testing.B) {
			b.Run("BSON", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					_, err := Marshal(tc.value)
					if err != nil {
						b.Errorf("error marshalling BSON: %s", err)
					}
				}
			})

			b.Run("extJSON", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					_, err := MarshalExtJSON(tc.value, true, false)
					if err != nil {
						b.Errorf("error marshalling extended JSON: %s", err)
					}
				}
			})

			b.Run("JSON", func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					_, err := json.Marshal(tc.value)
					if err != nil {
						b.Errorf("error marshalling JSON: %s", err)
					}
				}
			})
		})
	}
}

func BenchmarkUnmarshal(b *testing.B) {
	cases := []struct {
		desc  string
		value interface{}
	}{
		{
			desc:  "simple struct",
			value: encodetestInstance,
		},
		{
			desc:  "nested struct",
			value: nestedInstance,
		},
		{
			desc:  "deep_bson.json.gz",
			value: readExtJSONFile("deep_bson.json.gz"),
		},
		{
			desc:  "flat_bson.json.gz",
			value: readExtJSONFile("flat_bson.json.gz"),
		},
		{
			desc:  "full_bson.json.gz",
			value: readExtJSONFile("full_bson.json.gz"),
		},
	}

	for _, tc := range cases {
		b.Run(tc.desc, func(b *testing.B) {
			b.Run("BSON", func(b *testing.B) {
				data, err := Marshal(tc.value)
				if err != nil {
					b.Errorf("error marshalling BSON: %s", err)
					return
				}

				b.ResetTimer()
				var v2 map[string]interface{}
				for i := 0; i < b.N; i++ {
					err := Unmarshal(data, &v2)
					if err != nil {
						b.Errorf("error unmarshalling BSON: %s", err)
					}
				}
			})

			b.Run("extJSON", func(b *testing.B) {
				data, err := MarshalExtJSON(tc.value, true, false)
				if err != nil {
					b.Errorf("error marshalling extended JSON: %s", err)
					return
				}

				b.ResetTimer()
				var v2 map[string]interface{}
				for i := 0; i < b.N; i++ {
					err := UnmarshalExtJSON(data, true, &v2)
					if err != nil {
						b.Errorf("error unmarshalling extended JSON: %s", err)
					}
				}
			})

			b.Run("JSON", func(b *testing.B) {
				data, err := json.Marshal(tc.value)
				if err != nil {
					b.Errorf("error marshalling JSON: %s", err)
					return
				}

				b.ResetTimer()
				var v2 map[string]interface{}
				for i := 0; i < b.N; i++ {
					err := json.Unmarshal(data, &v2)
					if err != nil {
						b.Errorf("error unmarshalling JSON: %s", err)
					}
				}
			})
		})
	}
}
