////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation                                             //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file.                                                              //
////////////////////////////////////////////////////////////////////////////////

//go:build js && wasm

package utils

import (
	"encoding/base64"
	"encoding/json"
	"sort"
	"syscall/js"
	"testing"
)

import (
	"bytes"
	"fmt"
	"strings"
)

// Tests that CopyBytesToGo returns a byte slice that matches the Uint8Array.
func TestCopyBytesToGo(t *testing.T) {
	for i, val := range testBytes {
		// Create Uint8Array and set each element individually
		jsBytes := Uint8Array.New(len(val))
		for j, v := range val {
			jsBytes.SetIndex(j, v)
		}

		goBytes := CopyBytesToGo(jsBytes)

		if !bytes.Equal(val, goBytes) {
			t.Errorf("Failed to recevie expected bytes from Uint8Array (%d)."+
				"\nexpected: %d\nreceived: %d",
				i, val, goBytes)
		}
	}
}

// Tests that CopyBytesToJS returns a Javascript Uint8Array with values matching
// the original byte slice.
func TestCopyBytesToJS(t *testing.T) {
	for i, val := range testBytes {
		jsBytes := CopyBytesToJS(val)

		// Generate the expected string to match the output of toString() on a
		// Uint8Array
		expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:]
		expected = expected[:len(expected)-1]

		// Get the string value of the Uint8Array
		jsString := jsBytes.Call("toString").String()

		if expected != jsString {
			t.Errorf("Failed to recevie expected string representation of "+
				"the Uint8Array (%d).\nexpected: %s\nreceived: %s",
				i, expected, jsString)
		}
	}
}

// Tests that a byte slice converted to Javascript via CopyBytesToJS and
// converted back to Go via CopyBytesToGo matches the original.
func TestCopyBytesToJSCopyBytesToGo(t *testing.T) {
	for i, val := range testBytes {
		jsBytes := CopyBytesToJS(val)
		goBytes := CopyBytesToGo(jsBytes)

		if !bytes.Equal(val, goBytes) {
			t.Errorf("Failed to recevie expected bytes from Uint8Array (%d)."+
				"\nexpected: %d\nreceived: %d",
				i, val, goBytes)
		}
	}

}

// Tests that JsToJson can convert a Javascript object to JSON that matches the
// output of json.Marshal on the Go version of the same object.
func TestJsToJson(t *testing.T) {
	testObj := map[string]any{
		"nil":    nil,
		"bool":   true,
		"int":    1,
		"float":  1.5,
		"string": "I am string",
		"array":  []any{1, 2, 3},
		"object": map[string]any{"int": 5},
	}

	expected, err := json.Marshal(testObj)
	if err != nil {
		t.Errorf("Failed to JSON marshal test object: %+v", err)
	}

	jsJson := JsToJson(js.ValueOf(testObj))

	// Javascript does not return the JSON object fields sorted so the letters
	// of each Javascript string are sorted and compared
	er := []rune(string(expected))
	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
	jj := []rune(jsJson)
	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })

	if string(er) != string(jj) {
		t.Errorf("Recieved incorrect JSON from Javascript object."+
			"\nexpected: %s\nreceived: %s", expected, jsJson)
	}
}

// Tests that JsToJson return a null object when the Javascript object is
// undefined.
func TestJsToJson_Undefined(t *testing.T) {
	expected, err := json.Marshal(nil)
	if err != nil {
		t.Errorf("Failed to JSON marshal test object: %+v", err)
	}

	jsJson := JsToJson(js.Undefined())

	if string(expected) != jsJson {
		t.Errorf("Recieved incorrect JSON from Javascript object."+
			"\nexpected: %s\nreceived: %s", expected, jsJson)
	}
}

// Tests that JsonToJS can convert a JSON object with multiple types to a
// Javascript object and that all values match.
func TestJsonToJS(t *testing.T) {
	testObj := map[string]any{
		"nil":    nil,
		"bool":   true,
		"int":    1,
		"float":  1.5,
		"string": "I am string",
		"bytes":  []byte{1, 2, 3},
		"array":  []any{1, 2, 3},
		"object": map[string]any{"int": 5},
	}
	jsonData, err := json.Marshal(testObj)
	if err != nil {
		t.Errorf("Failed to JSON marshal test object: %+v", err)
	}

	jsObj, err := JsonToJS(jsonData)
	if err != nil {
		t.Errorf("Failed to convert JSON to Javascript object: %+v", err)
	}

	for key, val := range testObj {
		jsVal := jsObj.Get(key)
		switch key {
		case "nil":
			if !jsVal.IsNull() {
				t.Errorf("Key %s is not null.", key)
			}
		case "bool":
			if jsVal.Bool() != val {
				t.Errorf("Incorrect value for key %s."+
					"\nexpected: %t\nreceived: %t", key, val, jsVal.Bool())
			}
		case "int":
			if jsVal.Int() != val {
				t.Errorf("Incorrect value for key %s."+
					"\nexpected: %d\nreceived: %d", key, val, jsVal.Int())
			}
		case "float":
			if jsVal.Float() != val {
				t.Errorf("Incorrect value for key %s."+
					"\nexpected: %f\nreceived: %f", key, val, jsVal.Float())
			}
		case "string":
			if jsVal.String() != val {
				t.Errorf("Incorrect value for key %s."+
					"\nexpected: %s\nreceived: %s", key, val, jsVal.String())
			}
		case "bytes":
			if jsVal.String() != base64.StdEncoding.EncodeToString(val.([]byte)) {
				t.Errorf("Incorrect value for key %s."+
					"\nexpected: %s\nreceived: %s", key,
					base64.StdEncoding.EncodeToString(val.([]byte)),
					jsVal.String())
			}
		case "array":
			for i, v := range val.([]any) {
				if jsVal.Index(i).Int() != v {
					t.Errorf("Incorrect value for key %s index %d."+
						"\nexpected: %d\nreceived: %d",
						key, i, v, jsVal.Index(i).Int())
				}
			}
		case "object":
			if jsVal.Get("int").Int() != val.(map[string]any)["int"] {
				t.Errorf("Incorrect value for key %s."+
					"\nexpected: %d\nreceived: %d", key,
					val.(map[string]any)["int"], jsVal.Get("int").Int())
			}
		}
	}
}

// Tests that JSON can be converted to a Javascript object via JsonToJS and back
// to JSON using JsToJson and matches the original.
func TestJsonToJSJsToJson(t *testing.T) {
	testObj := map[string]any{
		"nil":    nil,
		"bool":   true,
		"int":    1,
		"float":  1.5,
		"string": "I am string",
		"bytes":  []byte{1, 2, 3},
		"array":  []any{1, 2, 3},
		"object": map[string]any{"int": 5},
	}
	jsonData, err := json.Marshal(testObj)
	if err != nil {
		t.Errorf("Failed to JSON marshal test object: %+v", err)
	}

	jsObj, err := JsonToJS(jsonData)
	if err != nil {
		t.Errorf("Failed to convert the Javascript object to JSON: %+v", err)
	}

	jsJson := JsToJson(jsObj)

	// Javascript does not return the JSON object fields sorted so the letters
	// of each Javascript string are sorted and compared
	er := []rune(string(jsonData))
	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
	jj := []rune(jsJson)
	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })

	if string(er) != string(jj) {
		t.Errorf("JSON from Javascript does not match original."+
			"\nexpected: %s\nreceived: %s", jsonData, jsJson)
	}
}

// Tests that JsErrorToJson can convert a Javascript object to JSON that matches
// the output of json.Marshal on the Go version of the same object.
func TestJsErrorToJson(t *testing.T) {
	testObj := map[string]any{
		"nil":    nil,
		"bool":   true,
		"int":    1,
		"float":  1.5,
		"string": "I am string",
		"array":  []any{1, 2, 3},
		"object": map[string]any{"int": 5},
	}

	expected, err := json.Marshal(testObj)
	if err != nil {
		t.Errorf("Failed to JSON marshal test object: %+v", err)
	}

	jsJson := JsErrorToJson(js.ValueOf(testObj))

	// Javascript does not return the JSON object fields sorted so the letters
	// of each Javascript string are sorted and compared
	er := []rune(string(expected))
	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
	jj := []rune(jsJson)
	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })

	if string(er) != string(jj) {
		t.Errorf("Recieved incorrect JSON from Javascript object."+
			"\nexpected: %s\nreceived: %s", expected, jsJson)
	}
}

// Tests that JsErrorToJson return a null object when the Javascript object is
// undefined.
func TestJsErrorToJson_Undefined(t *testing.T) {
	expected, err := json.Marshal(nil)
	if err != nil {
		t.Errorf("Failed to JSON marshal test object: %+v", err)
	}

	jsJson := JsErrorToJson(js.Undefined())

	if string(expected) != jsJson {
		t.Errorf("Recieved incorrect JSON from Javascript object."+
			"\nexpected: %s\nreceived: %s", expected, jsJson)
	}
}

// Tests that JsErrorToJson returns a JSON object containing the original error
// string.
func TestJsErrorToJson_ErrorObject(t *testing.T) {
	expected := "An error"
	jsErr := Error.New(expected)
	jsJson := JsErrorToJson(jsErr)

	if !strings.Contains(jsJson, expected) {
		t.Errorf("Recieved incorrect JSON from Javascript error."+
			"\nexpected: %s\nreceived: %s", expected, jsJson)
	}
}