package compatibility

import (
	"testing"

	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protodesc"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
	"google.golang.org/protobuf/types/dynamicpb"
)

func TestEffectivePermissionRecordTypesAreAdditiveAcrossNAndNMinusOne(t *testing.T) {
	response := permissionsemantic.Response{
		SystemPermissions: []permissionsemantic.PermissionGrant{{
			PermissionID: "ApiEnabled", Granted: true,
		}},
		RecordTypePermissions: []permissionsemantic.RecordTypePermission{{
			ResourceType: "Order", RecordTypeID: "rt-business",
			Available: true, IsDefault: true,
		}},
		Receipts: commonsemantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: permissionsemantic.CapabilityName,
			CatalogRevision: 3, SchemaGeneration: 4, EngineGeneration: 5,
			SecurityEpoch: 6,
		},
	}
	wireResponse, err := mapping.ToWireReadEffectivePermissionsResponse(response)
	if err != nil {
		t.Fatal(err)
	}
	encoded, err := proto.Marshal(wireResponse)
	if err != nil {
		t.Fatal(err)
	}

	// Model an N-1 client from the immutable schema immediately before field
	// 8 existed. It must accept N bytes, preserve old fields, and safely ignore
	// the additive record-type projection.
	oldDescriptor := nMinusOneEffectivePermissionResponseDescriptor(t)
	oldResponse := dynamicpb.NewMessage(oldDescriptor)
	if err := (proto.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(encoded, oldResponse); err != nil {
		t.Fatalf("N-1 decode of N response: %v", err)
	}
	systemField := oldDescriptor.Fields().ByName("system_permissions")
	if systemField == nil || oldResponse.Get(systemField).List().Len() != 1 {
		t.Fatal("N-1 client lost an existing permission field")
	}

	// The N client must likewise accept an N-1 response. The absent additive
	// field has exactly one meaning: no record-type facts were published.
	oldEncoded, err := proto.Marshal(oldResponse)
	if err != nil {
		t.Fatal(err)
	}
	roundWire := &runtimev1.ReadEffectivePermissionsResponse{}
	if err := proto.Unmarshal(oldEncoded, roundWire); err != nil {
		t.Fatalf("N decode of N-1 response: %v", err)
	}
	roundSemantic, err := mapping.FromWireReadEffectivePermissionsResponse(roundWire)
	if err != nil {
		t.Fatalf("N mapping of N-1 response: %v", err)
	}
	if len(roundSemantic.RecordTypePermissions) != 0 {
		t.Fatalf("N-1 response produced record-type facts: %#v", roundSemantic.RecordTypePermissions)
	}
	if len(roundSemantic.SystemPermissions) != 1 || !roundSemantic.SystemPermissions[0].Granted {
		t.Fatalf("existing permission changed across N/N-1: %#v", roundSemantic.SystemPermissions)
	}
}

func nMinusOneEffectivePermissionResponseDescriptor(t *testing.T) protoreflect.MessageDescriptor {
	t.Helper()
	file := protodesc.ToFileDescriptorProto(runtimev1.File_runtime_v1_effective_permissions_proto)
	for _, message := range file.GetMessageType() {
		if message.GetName() != "ReadEffectivePermissionsResponse" {
			continue
		}
		fields := message.Field[:0]
		for _, field := range message.GetField() {
			if field.GetNumber() != 8 {
				fields = append(fields, field)
			}
		}
		message.Field = fields
	}
	oldFile, err := protodesc.NewFile(file, protoregistry.GlobalFiles)
	if err != nil {
		t.Fatal(err)
	}
	descriptor := oldFile.Messages().ByName("ReadEffectivePermissionsResponse")
	if descriptor == nil || descriptor.Fields().ByName("record_type_permissions") != nil {
		t.Fatal("failed to construct N-1 effective-permission descriptor")
	}
	return descriptor
}
