// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package protorange

import (
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protopath"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
	"google.golang.org/protobuf/testing/protocmp"

	newspb "google.golang.org/protobuf/internal/testprotos/news"
	anypb "google.golang.org/protobuf/types/known/anypb"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)

func mustMarshal(m proto.Message) []byte {
	b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
	if err != nil {
		panic(err)
	}
	return b
}

var transformReflectValue = cmp.Transformer("", func(v protoreflect.Value) any {
	switch v := v.Interface().(type) {
	case protoreflect.Message:
		return v.Interface()
	case protoreflect.Map:
		ms := map[any]protoreflect.Value{}
		v.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
			ms[k.Interface()] = v
			return true
		})
		return ms
	case protoreflect.List:
		ls := []protoreflect.Value{}
		for i := 0; i < v.Len(); i++ {
			ls = append(ls, v.Get(i))
		}
		return ls
	default:
		return v
	}
})

func TestRange(t *testing.T) {
	m2 := (&newspb.KeyValueAttachment{
		Name: "checksums.txt",
		Data: map[string]string{
			"go1.10.src.tar.gz":         "07cbb9d0091b846c6aea40bf5bc0cea7",
			"go1.10.darwin-amd64.pkg":   "cbb38bb6ff6ea86279e01745984445bf",
			"go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566",
			"go1.10.windows-amd64.msi":  "57bda02030f58f5d2bf71943e1390123",
		},
	}).ProtoReflect()
	m := (&newspb.Article{
		Author:  "Brad Fitzpatrick",
		Date:    timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)),
		Title:   "Go 1.10 is released",
		Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...",
		Status:  newspb.Article_PUBLISHED,
		Tags:    []string{"go1.10", "release"},
		Attachments: []*anypb.Any{{
			TypeUrl: "google.golang.org.KeyValueAttachment",
			Value:   mustMarshal(m2.Interface()),
		}},
	}).ProtoReflect()

	// Nil push and pop functions should not panic.
	noop := func(protopath.Values) error { return nil }
	Options{}.Range(m, nil, nil)
	Options{}.Range(m, noop, nil)
	Options{}.Range(m, nil, noop)

	getByName := func(m protoreflect.Message, s protoreflect.Name) protoreflect.Value {
		fds := m.Descriptor().Fields()
		return m.Get(fds.ByName(s))
	}

	wantPaths := []string{
		``,
		`.author`,
		`.date`,
		`.date.seconds`,
		`.title`,
		`.content`,
		`.attachments`,
		`.attachments[0]`,
		`.attachments[0].(google.golang.org.KeyValueAttachment)`,
		`.attachments[0].(google.golang.org.KeyValueAttachment).name`,
		`.attachments[0].(google.golang.org.KeyValueAttachment).data`,
		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.darwin-amd64.pkg"]`,
		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.linux-amd64.tar.gz"]`,
		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.src.tar.gz"]`,
		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.windows-amd64.msi"]`,
		`.tags`,
		`.tags[0]`,
		`.tags[1]`,
		`.status`,
	}
	wantValues := []protoreflect.Value{
		protoreflect.ValueOfMessage(m),
		getByName(m, "author"),
		getByName(m, "date"),
		getByName(getByName(m, "date").Message(), "seconds"),
		getByName(m, `title`),
		getByName(m, `content`),
		getByName(m, `attachments`),
		getByName(m, `attachments`).List().Get(0),
		protoreflect.ValueOfMessage(m2),
		getByName(m2, `name`),
		getByName(m2, `data`),
		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.darwin-amd64.pkg").MapKey()),
		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.linux-amd64.tar.gz").MapKey()),
		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.src.tar.gz").MapKey()),
		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.windows-amd64.msi").MapKey()),
		getByName(m, `tags`),
		getByName(m, `tags`).List().Get(0),
		getByName(m, `tags`).List().Get(1),
		getByName(m, `status`),
	}

	tests := []struct {
		resolver interface {
			protoregistry.ExtensionTypeResolver
			protoregistry.MessageTypeResolver
		}

		errorAt     int
		breakAt     int
		terminateAt int

		wantPaths  []string
		wantValues []protoreflect.Value
		wantError  error
	}{{
		wantPaths:  wantPaths,
		wantValues: wantValues,
	}, {
		resolver: (*protoregistry.Types)(nil),
		wantPaths: append(append(wantPaths[:8:8],
			`.attachments[0].type_url`,
			`.attachments[0].value`,
		), wantPaths[15:]...),
		wantValues: append(append(wantValues[:8:8],
			protoreflect.ValueOfString("google.golang.org.KeyValueAttachment"),
			protoreflect.ValueOfBytes(mustMarshal(m2.Interface())),
		), wantValues[15:]...),
	}, {
		errorAt:    5, // return error within newspb.Article
		wantPaths:  wantPaths[:5],
		wantValues: wantValues[:5],
		wantError:  cmpopts.AnyError,
	}, {
		terminateAt: 11, // terminate within newspb.KeyValueAttachment
		wantPaths:   wantPaths[:11],
		wantValues:  wantValues[:11],
	}, {
		breakAt:    11, // break within newspb.KeyValueAttachment
		wantPaths:  append(wantPaths[:11:11], wantPaths[15:]...),
		wantValues: append(wantValues[:11:11], wantValues[15:]...),
	}, {
		errorAt:    17, // return error within newspb.Article.Tags
		wantPaths:  wantPaths[:17],
		wantValues: wantValues[:17],
		wantError:  cmpopts.AnyError,
	}, {
		breakAt:    17, // break within newspb.Article.Tags
		wantPaths:  append(wantPaths[:17:17], wantPaths[18:]...),
		wantValues: append(wantValues[:17:17], wantValues[18:]...),
	}, {
		terminateAt: 17, // terminate within newspb.Article.Tags
		wantPaths:   wantPaths[:17],
		wantValues:  wantValues[:17],
	}, {
		errorAt:    13, // return error within newspb.KeyValueAttachment.Data
		wantPaths:  wantPaths[:13],
		wantValues: wantValues[:13],
		wantError:  cmpopts.AnyError,
	}, {
		breakAt:    13, // break within newspb.KeyValueAttachment.Data
		wantPaths:  append(wantPaths[:13:13], wantPaths[15:]...),
		wantValues: append(wantValues[:13:13], wantValues[15:]...),
	}, {
		terminateAt: 13, // terminate within newspb.KeyValueAttachment.Data
		wantPaths:   wantPaths[:13],
		wantValues:  wantValues[:13],
	}}
	for _, tt := range tests {
		t.Run("", func(t *testing.T) {
			var gotPaths []string
			var gotValues []protoreflect.Value
			var stackPaths []string
			var stackValues []protoreflect.Value
			gotError := Options{
				Stable:   true,
				Resolver: tt.resolver,
			}.Range(m,
				func(p protopath.Values) error {
					gotPaths = append(gotPaths, p.Path[1:].String())
					stackPaths = append(stackPaths, p.Path[1:].String())
					gotValues = append(gotValues, p.Index(-1).Value)
					stackValues = append(stackValues, p.Index(-1).Value)
					switch {
					case tt.errorAt > 0 && tt.errorAt == len(gotPaths):
						return cmpopts.AnyError
					case tt.breakAt > 0 && tt.breakAt == len(gotPaths):
						return Break
					case tt.terminateAt > 0 && tt.terminateAt == len(gotPaths):
						return Terminate
					default:
						return nil
					}
				},
				func(p protopath.Values) error {
					gotPath := p.Path[1:].String()
					wantPath := stackPaths[len(stackPaths)-1]
					if wantPath != gotPath {
						t.Errorf("%d: pop path mismatch: got %v, want %v", len(gotPaths), gotPath, wantPath)
					}
					gotValue := p.Index(-1).Value
					wantValue := stackValues[len(stackValues)-1]
					if diff := cmp.Diff(wantValue, gotValue, transformReflectValue, protocmp.Transform()); diff != "" {
						t.Errorf("%d: pop value mismatch (-want +got):\n%v", len(gotValues), diff)
					}
					stackPaths = stackPaths[:len(stackPaths)-1]
					stackValues = stackValues[:len(stackValues)-1]
					return nil
				},
			)
			if n := len(stackPaths) + len(stackValues); n > 0 {
				t.Errorf("stack mismatch: got %d unpopped items", n)
			}
			if diff := cmp.Diff(tt.wantPaths, gotPaths); diff != "" {
				t.Errorf("paths mismatch (-want +got):\n%s", diff)
			}
			if diff := cmp.Diff(tt.wantValues, gotValues, transformReflectValue, protocmp.Transform()); diff != "" {
				t.Errorf("values mismatch (-want +got):\n%s", diff)
			}
			if !cmp.Equal(gotError, tt.wantError, cmpopts.EquateErrors()) {
				t.Errorf("error mismatch: got %v, want %v", gotError, tt.wantError)
			}
		})
	}
}
