package mapping

import (
	"errors"
	"fmt"
	"reflect"
	"testing"

	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/protobuf/encoding/protowire"
	"google.golang.org/protobuf/proto"
)

func layoutAuthoringStoredWithCreateContext(context *semantic.CreateContext) semantic.StoredLayout {
	return semantic.StoredLayout{
		Document: layoutAuthoringTestDocument(), OrderedListFields: []string{"Name"},
		Lifecycle:       semantic.LayoutLifecycleState{ProvenanceKind: "admin"},
		ResourceVersion: "resource-v1", AggregateVersion: "aggregate-v1", WorkspaceVersion: "workspace-v1",
		CreateContext: context, ViewRef: layoutAuthoringTestViewRef(50), Receipts: layoutAuthoringTestReceipts(50),
	}
}

func TestLayoutAuthoringCreateContextRoundTripsPresenceAndCopies(t *testing.T) {
	for _, tc := range []struct {
		name    string
		context *semantic.CreateContext
	}{
		{"legacy unknown", nil}, {"known empty", &semantic.CreateContext{RequiredFields: []string{}}},
		{"required", &semantic.CreateContext{RequiredFields: []string{"Name", "OwnerId"}}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			source := layoutAuthoringStoredWithCreateContext(tc.context)
			request := semantic.StoredLayoutRequest{LayoutID: "layout-main", ViewRef: &source.ViewRef}
			wire, err := ToWireLayoutAuthoringStoredLayout(request, source)
			if err != nil {
				t.Fatal(err)
			}
			encoded, err := proto.Marshal(wire)
			if err != nil {
				t.Fatal(err)
			}
			if err = PreflightPlatformWirePayload(encoded, &runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse{}); err != nil {
				t.Fatal(err)
			}
			var decoded runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse
			if err = proto.Unmarshal(encoded, &decoded); err != nil {
				t.Fatal(err)
			}
			roundtrip, err := FromWireLayoutAuthoringStoredLayout(request, &decoded)
			if err != nil || !reflect.DeepEqual(roundtrip, source) {
				t.Fatalf("roundtrip=%#v, %v; source=%#v", roundtrip, err, source)
			}
			if tc.context != nil && len(tc.context.RequiredFields) > 0 {
				wire.CreateContext.RequiredFields[0] = "ChangedWire"
				if source.CreateContext.RequiredFields[0] != "Name" {
					t.Fatal("ToWire leaked source array")
				}
				decoded.CreateContext.RequiredFields[0] = "ChangedDecoded"
				if roundtrip.CreateContext.RequiredFields[0] != "Name" {
					t.Fatal("FromWire leaked decoded array")
				}
			}
		})
	}
}

func TestLayoutAuthoringCreateContextSupportsFieldsAcrossSections(t *testing.T) {
	fields := make([]string, semantic.MaxFieldsPerSection+100)
	for i := range fields {
		fields[i] = fmt.Sprintf("Field%04d", i)
	}
	source := layoutAuthoringStoredWithCreateContext(&semantic.CreateContext{RequiredFields: fields})
	first, second := source.Document.Sections[0], source.Document.Sections[0]
	first.Fields = fields[:semantic.MaxFieldsPerSection]
	second.SectionID = "second"
	second.Fields = fields[semantic.MaxFieldsPerSection:]
	source.Document.Sections = []semantic.LayoutSection{first, second}
	request := semantic.StoredLayoutRequest{LayoutID: "layout-main", ViewRef: &source.ViewRef}
	wire, err := ToWireLayoutAuthoringStoredLayout(request, source)
	if err != nil {
		t.Fatalf("multi-section response rejected: %v", err)
	}
	encoded, err := proto.Marshal(wire)
	if err != nil {
		t.Fatal(err)
	}
	if err = PreflightPlatformWirePayload(encoded, &runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse{}); err != nil {
		t.Fatal(err)
	}
	var decoded runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse
	if err = proto.Unmarshal(encoded, &decoded); err != nil {
		t.Fatal(err)
	}
	result, err := FromWireLayoutAuthoringStoredLayout(request, &decoded)
	if err != nil || !reflect.DeepEqual(result, source) {
		t.Fatalf("multi-section roundtrip failed: %v", err)
	}
}

func layoutAuthoringOverBudgetCreateContext() *runtimev1.LayoutAuthoringCreateContext {
	count := semantic.MaxSemanticResponseBytes/(commonsemantic.MaxFieldIdentifierBytes+2) + 1
	fields := make([]string, count)
	for i := range fields {
		fields[i] = fmt.Sprintf("F%063d", i)
	}
	return &runtimev1.LayoutAuthoringCreateContext{RequiredFields: fields}
}

func TestLayoutAuthoringCreateContextPreflightChargesNameBytesBeforeMapping(t *testing.T) {
	source := layoutAuthoringStoredWithCreateContext(&semantic.CreateContext{RequiredFields: []string{}})
	request := semantic.StoredLayoutRequest{LayoutID: "layout-main", ViewRef: &source.ViewRef}
	wire, err := ToWireLayoutAuthoringStoredLayout(request, source)
	if err != nil {
		t.Fatal(err)
	}
	wire.CreateContext = layoutAuthoringOverBudgetCreateContext()
	encoded, err := proto.Marshal(wire)
	if err != nil {
		t.Fatal(err)
	}
	if len(encoded) >= envelope.MaxResponseWireBytes {
		t.Fatal("fixture only tests outer wire ceiling, not semantic budget")
	}
	_, mappingErr := FromWireLayoutAuthoringStoredLayout(request, wire)
	for name, err := range map[string]error{
		"raw":   PreflightPlatformWirePayload(encoded, &runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse{}),
		"typed": preflightWireLayoutAuthoringStoredLayout(wire), "mapping": mappingErr,
	} {
		var failure *semantic.Failure
		if !errors.As(err, &failure) || failure.Category != semantic.FailureInternal {
			t.Errorf("%s over-budget error=%v; want internal response failure", name, err)
		}
	}
}

func BenchmarkLayoutAuthoringCreateContextRawAdmission(b *testing.B) {
	encoded, err := proto.Marshal(&runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse{CreateContext: layoutAuthoringOverBudgetCreateContext()})
	if err != nil {
		b.Fatal(err)
	}
	b.ReportAllocs()
	b.SetBytes(int64(len(encoded)))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if err := PreflightPlatformWirePayload(encoded, &runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse{}); err == nil {
			b.Fatal("over-budget context admitted")
		}
	}
}

func TestLayoutAuthoringCreateContextRejectsSplitSingularMessage(t *testing.T) {
	source := layoutAuthoringStoredWithCreateContext(&semantic.CreateContext{RequiredFields: []string{"Name"}})
	request := semantic.StoredLayoutRequest{LayoutID: "layout-main"}
	wire, err := ToWireLayoutAuthoringStoredLayout(request, source)
	if err != nil {
		t.Fatal(err)
	}
	encoded, err := proto.Marshal(wire)
	if err != nil {
		t.Fatal(err)
	}
	// Protobuf would merge two occurrences of this singular message. Reject them
	// before per-message field counting can be bypassed by splitting a list.
	second, err := proto.Marshal(&runtimev1.LayoutAuthoringCreateContext{RequiredFields: []string{"OwnerId"}})
	if err != nil {
		t.Fatal(err)
	}
	encoded = protowire.AppendTag(encoded, 9, protowire.BytesType)
	encoded = protowire.AppendBytes(encoded, second)
	if err = PreflightPlatformWirePayload(encoded, &runtimev1.LayoutAuthoringServiceReadStoredLayoutResponse{}); !errors.Is(err, ErrMalformedPlatformWire) {
		t.Fatalf("split context accepted: %v", err)
	}
}

func TestLayoutAuthoringCreateContextRejectsUncanonicalWireFields(t *testing.T) {
	source := layoutAuthoringStoredWithCreateContext(&semantic.CreateContext{RequiredFields: []string{}})
	request := semantic.StoredLayoutRequest{LayoutID: "layout-main"}
	for _, fields := range [][]string{{"OwnerId", "Name"}, {"Name", "name"}, {"Owner.Name"}} {
		wire, err := ToWireLayoutAuthoringStoredLayout(request, source)
		if err != nil {
			t.Fatal(err)
		}
		wire.CreateContext.RequiredFields = fields
		if _, err = FromWireLayoutAuthoringStoredLayout(request, wire); err == nil {
			t.Fatalf("invalid fields mapped: %v", fields)
		}
	}
}
