/*
 * Copyright 2023 CloudWeGo 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 proto

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"testing"

	"github.com/cloudwego/dynamicgo/conv"
	"github.com/cloudwego/dynamicgo/proto"
	"github.com/jhump/protoreflect/desc/protoparse"
	"github.com/jhump/protoreflect/dynamic"

	"github.com/cloudwego/kitex/internal/test"
)

var (
	example2IDLPath   = "../../../pkg/generic/jsonpb_test/idl/example2.proto"
	example2ProtoPath = "../../../pkg/generic/jsonpb_test/data/example2_pb.bin"
)

func TestRun(t *testing.T) {
	t.Run("TestWrite", TestWrite)
	t.Run("TestRead", TestRead)
}

func makePBDynamicStruct(idl, message string, includes ...string) (*dynamic.Message, error) {
	parser := &protoparse.Parser{
		ImportPaths: includes,
	}

	fds, err := parser.ParseFiles(idl)
	if err != nil {
		panic(fmt.Errorf("failed to parse proto file: %v", err))
	}

	msgDesc := fds[0].FindMessage(message)

	return dynamic.NewMessage(msgDesc), nil
}

// Check NewWriteJSON converting JSON to protobuf wire format
func TestWrite(t *testing.T) {
	method := "ExampleMethod"

	opts := proto.Options{}
	svc, err := opts.NewDescriptorFromPath(context.Background(), example2IDLPath)
	test.Assert(t, err == nil)

	msg := getExampleReq()

	// get expected json struct
	exp, err := makePBDynamicStruct(example2IDLPath, "test.ExampleReq")
	test.Assert(t, err == nil)
	err = exp.UnmarshalJSON([]byte(msg))
	test.Assert(t, err == nil)

	// marshal json string into protobuf wire format using Write
	wm := NewWriteJSON(svc, &conv.Options{})
	out, err := wm.Write(context.Background(), msg, method, true)
	test.Assert(t, err == nil)
	buf, ok := out.([]byte)
	test.Assert(t, ok)

	// read into struct using fastread
	act, err := makePBDynamicStruct(example2IDLPath, "test.ExampleReq")
	test.Assert(t, err == nil)
	err = act.Unmarshal(buf)
	test.Assert(t, err == nil)
	// compare exp and act struct
	test.Assert(t, deepEqual(t, exp, act))
}

func deepEqual(t *testing.T, a, b interface{}) bool {
	ajs, err := json.Marshal(a)
	if err != nil {
		t.Error(err)
	}
	bjs, err := json.Marshal(b)
	if err != nil {
		t.Error(err)
	}
	return bytes.Equal(ajs, bjs)
}

// Check NewReadJSON converting protobuf wire format to JSON
func TestRead(t *testing.T) {
	// get protobuf wire format data
	in := readExampleReqProtoBufData()
	method := "ExampleMethod"

	opts := proto.Options{}
	svc, err := opts.NewDescriptorFromPath(context.Background(), example2IDLPath)
	test.Assert(t, err == nil)

	// unmarshal protobuf wire format into json string using Read
	rm := NewReadJSON(svc, &conv.Options{})
	out, err := rm.Read(context.Background(), method, false, in)
	test.Assert(t, err == nil)

	// get expected json struct
	exp, err := makePBDynamicStruct(example2IDLPath, "test.ExampleReq")
	test.Assert(t, err == nil)
	err = exp.Unmarshal(in)
	test.Assert(t, err == nil)

	act, err := makePBDynamicStruct(example2IDLPath, "test.ExampleReq")
	test.Assert(t, err == nil)
	err = act.UnmarshalJSON([]byte(out.(string)))
	test.Assert(t, err == nil)
	// compare exp and act struct
	test.Assert(t, deepEqual(t, exp, act))
}

// helper methods
// comes from https://github.com/cloudwego/dynamicgo/blob/main/testdata/data/example2req.json
func getExampleReq() string {
	return `{"Msg":"hello","A":25,"InnerBase2":{"Bool":true,"Uint32":123,"Uint64":123,"Int32":123,"SInt64":123,"Double":22.3,"String":"hello_inner","ListInt32":[12,13,14,15,16,17],"MapStringString":{"m1":"aaa","m2":"bbb"},"ListSInt64":[200,201,202,203,204,205],"MapInt32String":{"1":"aaa","2":"bbb","3":"ccc","4":"ddd"},"Binary":"AQIDBA==","MapUint32String":{"1":"u32aa","2":"u32bb","3":"u32cc","4":"u32dd"},"MapUint64String":{"1":"u64aa","2":"u64bb","3":"u64cc","4":"u64dd"},"MapInt64String":{"1":"64aaa","2":"64bbb","3":"64ccc","4":"64ddd"},"MapInt64Base":{"1":{"LogID":"logId","Caller":"caller","Addr":"addr","Client":"client","TrafficEnv":{"Env":"env"},"Extra":{"1a":"aaa","2a":"bbb","3a":"ccc","4a":"ddd"}},"2":{"LogID":"logId2","Caller":"caller2","Addr":"addr2","Client":"client2","TrafficEnv":{"Open":true,"Env":"env2"},"Extra":{"1a":"aaa2","2a":"bbb2","3a":"ccc2","4a":"ddd2"}}},"MapStringBase":{"1":{"LogID":"logId","Caller":"caller","Addr":"addr","Client":"client","TrafficEnv":{"Env":"env"},"Extra":{"1a":"aaa","2a":"bbb","3a":"ccc","4a":"ddd"}},"2":{"LogID":"logId2","Caller":"caller2","Addr":"addr2","Client":"client2","TrafficEnv":{"Open":true,"Env":"env2"},"Extra":{"1a":"aaa2","2a":"bbb2","3a":"ccc2","4a":"ddd2"}}},"ListBase":[{"LogID":"logId","Caller":"caller","Addr":"addr","Client":"client","TrafficEnv":{"Env":"env"},"Extra":{"1a":"aaa","2a":"bbb","3a":"ccc","4a":"ddd"}},{"LogID":"logId2","Caller":"caller2","Addr":"addr2","Client":"client2","TrafficEnv":{"Open":true,"Env":"env2"},"Extra":{"1a":"aaa2","2a":"bbb2","3a":"ccc2","4a":"ddd2"}}],"ListString":["111","222","333","44","51","6"],"Base":{"LogID":"logId","Caller":"caller","Addr":"addr","Client":"client","TrafficEnv":{"Env":"env"},"Extra":{"1b":"aaa","2b":"bbb","3b":"ccc","4b":"ddd"}}}}`
}

// read ProtoBuf's data in binary format from exampleProtoPath
func readExampleReqProtoBufData() []byte {
	out, err := os.ReadFile(example2ProtoPath)
	if err != nil {
		panic(err)
	}
	return out
}
