package layoutauthoringv1

import (
	"encoding/binary"
	"fmt"
	"strings"
	"testing"
)

func TestLayoutDocumentSemanticSizeExactBoundAndOneOver(t *testing.T) {
	document := validDocument()
	filler := strings.Repeat("x", 16*1024)
	document.HeaderActions.Actions[0].Configuration = append(
		document.HeaderActions.Actions[0].Configuration,
		NamedValue{Name: "zz_size_filler", Value: stringValue(filler)},
	)
	current := LayoutDocumentSemanticSize(document)
	if current >= MaxLayoutDocumentSemanticBytes {
		t.Fatalf("fixture size = %d, want below bound", current)
	}
	needed := MaxLayoutDocumentSemanticBytes - current
	filler += strings.Repeat("x", needed)
	document.HeaderActions.Actions[0].Configuration[1].Value = stringValue(filler)
	if got := LayoutDocumentSemanticSize(document); got != MaxLayoutDocumentSemanticBytes {
		t.Fatalf("exact document size = %d, want %d", got, MaxLayoutDocumentSemanticBytes)
	}
	if err := ValidateLayoutDocument(document); err != nil {
		t.Fatalf("exact-bound document rejected: %v", err)
	}

	filler += "x"
	document.HeaderActions.Actions[0].Configuration[1].Value = stringValue(filler)
	if got := LayoutDocumentSemanticSize(document); got != MaxLayoutDocumentSemanticBytes+1 {
		t.Fatalf("one-over document size = %d, want %d", got, MaxLayoutDocumentSemanticBytes+1)
	}
	assertResourceExhausted(t, ValidateLayoutDocument(document), LimitSemanticRequest, MaxLayoutDocumentSemanticBytes)
}

func TestSemanticRequestSizeExactBoundAndOneOver(t *testing.T) {
	headerFiller := strings.Repeat("h", 16*1024)
	quickFiller := strings.Repeat("q", 16*1024)
	changes := preserveChanges()
	changes.HeaderActions = Change[ActionPlacement]{Mode: ChangeSet, Value: &ActionPlacement{
		Authored: true,
		Actions: []LayoutAction{{
			Name: "header", Label: "Header", Kind: "Custom",
			Configuration: []NamedValue{{Name: "payload", Value: stringValue(headerFiller)}}, Extensions: []NamedValue{},
		}},
	}}
	changes.QuickActions = Change[ActionPlacement]{Mode: ChangeSet, Value: &ActionPlacement{
		Authored: true,
		Actions: []LayoutAction{{
			Name: "quick", Label: "Quick", Kind: "Custom",
			Configuration: []NamedValue{{Name: "payload", Value: stringValue(quickFiller)}}, Extensions: []NamedValue{},
		}},
	}}
	request := validSubmit(CommandOperation{Update: &UpdateLayout{
		LayoutID: "layout-main", ExpectedAggregateVersion: "aggregate-v1", Changes: changes,
		ListFields: FieldIDListChange{Mode: ChangePreserve},
	}})
	current := SemanticRequestSize(request)
	remaining := MaxSemanticRequestBytes - current
	if remaining <= 0 {
		t.Fatalf("fixture size = %d, want below bound", current)
	}
	headerFiller, used := growASCIIString(headerFiller, MaxValueStringBytes, remaining)
	remaining -= used
	request.Operation.Update.Changes.HeaderActions.Value.Actions[0].Configuration[0].Value = stringValue(headerFiller)
	quickFiller, used = growASCIIString(quickFiller, MaxValueStringBytes, remaining)
	remaining -= used
	request.Operation.Update.Changes.QuickActions.Value.Actions[0].Configuration[0].Value = stringValue(quickFiller)
	if remaining != 0 {
		t.Fatalf("could not construct exact request boundary; %d bytes remain", remaining)
	}
	if got := SemanticRequestSize(request); got != MaxSemanticRequestBytes {
		t.Fatalf("exact request size = %d, want %d", got, MaxSemanticRequestBytes)
	}
	if err := ValidateSubmitRequest(deadlineContext(t), request); err != nil {
		t.Fatalf("exact-bound request rejected: %v", err)
	}

	quickFiller += "q"
	request.Operation.Update.Changes.QuickActions.Value.Actions[0].Configuration[0].Value = stringValue(quickFiller)
	if got := SemanticRequestSize(request); got != MaxSemanticRequestBytes+1 {
		t.Fatalf("one-over request size = %d, want %d", got, MaxSemanticRequestBytes+1)
	}
	assertResourceExhausted(t, ValidateSubmitRequest(deadlineContext(t), request), LimitSemanticRequest, MaxSemanticRequestBytes)
}

func TestSemanticResponseSizeExactBoundAndOneOver(t *testing.T) {
	exact := workspaceWithSemanticSize(t, MaxSemanticResponseBytes)
	request := WorkspaceRequest{ObjectID: "Account", ViewRef: &exact.ViewRef}
	if got := SemanticResponseSize(exact); got != MaxSemanticResponseBytes {
		t.Fatalf("exact response size = %d, want %d", got, MaxSemanticResponseBytes)
	}
	if err := ValidateWorkspaceResponse(request, exact); err != nil {
		t.Fatalf("exact-bound response rejected: %v", err)
	}

	over := workspaceWithSemanticSize(t, MaxSemanticResponseBytes+1)
	if got := SemanticResponseSize(over); got != MaxSemanticResponseBytes+1 {
		t.Fatalf("one-over response size = %d, want %d", got, MaxSemanticResponseBytes+1)
	}
	assertResourceExhausted(t, ValidateWorkspaceResponse(
		WorkspaceRequest{ObjectID: "Account", ViewRef: &over.ViewRef}, over,
	), LimitSemanticResponse, MaxSemanticResponseBytes)
}

func TestSectionMapFactCollectionBounds(t *testing.T) {
	labels := make([]FieldLabel, MaxFieldLabelsPerSection)
	for index := range labels {
		labels[index] = FieldLabel{FieldReference: fmt.Sprintf("F%04d", index), Label: "label"}
	}
	if err := validateFieldLabels(labels); err != nil {
		t.Fatalf("exact field-label bound rejected: %v", err)
	}
	labels = append(labels, FieldLabel{FieldReference: "F1000", Label: "label"})
	assertResourceExhausted(t, validateFieldLabels(labels), LimitFieldCount, MaxFieldLabelsPerSection)

	perField := make([]FieldBehavior, MaxFieldBehaviorsPerField)
	for index := range perField {
		perField[index] = FieldBehavior{Mode: fmt.Sprintf("M%02d", index), Behavior: "exact"}
	}
	groups := []FieldBehaviorGroup{{FieldReference: "Name", Behaviors: &perField}}
	if err := validateFieldBehaviors(groups); err != nil {
		t.Fatalf("exact per-field behavior bound rejected: %v", err)
	}
	perField = append(perField, FieldBehavior{Mode: "M64", Behavior: "exact"})
	assertResourceExhausted(t, validateFieldBehaviors(groups), LimitFieldCount, MaxFieldBehaviorsPerField)

	perField = perField[:MaxFieldBehaviorsPerField]
	groups = make([]FieldBehaviorGroup, MaxFieldBehaviorGroupsPerSection)
	for field := 0; field < MaxFieldsPerSection; field++ {
		groups[field] = FieldBehaviorGroup{FieldReference: fmt.Sprintf("F%04d", field), Behaviors: &perField}
	}
	if err := validateFieldBehaviors(groups); err != nil {
		t.Fatalf("exact section behavior bound rejected: %v", err)
	}
	groups = append(groups, FieldBehaviorGroup{})
	assertResourceExhausted(t, validateFieldBehaviors(groups), LimitFieldCount, MaxFieldBehaviorGroupsPerSection)

	exactSections := []LayoutSection{{FieldBehaviors: groups[:MaxFieldBehaviorGroupsPerSection]}}
	if err := validateFieldBehaviorBudget(exactSections); err != nil {
		t.Fatalf("exact document behavior-node bound rejected: %v", err)
	}
	overSections := append(exactSections, LayoutSection{FieldBehaviors: []FieldBehaviorGroup{{FieldReference: "extra"}}})
	assertResourceExhausted(t, validateFieldBehaviorBudget(overSections), LimitFieldCount, MaxFieldBehaviorNodesPerDocument)

	document := validDocument()
	document.Tabs = []string{}
	document.Sections = make([]LayoutSection, 2)
	maxGroups := groups[:MaxFieldBehaviorGroupsPerSection]
	for index := range document.Sections {
		count := MaxFieldBehaviorGroupsPerSection
		if index == 1 {
			count = 1
		}
		document.Sections[index] = LayoutSection{
			SectionID: fmt.Sprintf("section-%03d", index), Label: "section",
			ComponentConfiguration: []NamedValue{}, Fields: []string{}, FieldLabels: []FieldLabel{},
			FieldBehaviors: maxGroups[:count],
		}
	}
	assertResourceExhausted(t, ValidateLayoutDocument(document), LimitFieldCount, MaxFieldBehaviorNodesPerDocument)
}

func workspaceWithSemanticSize(t *testing.T, target int) Workspace {
	t.Helper()
	ref := validViewRef(50)
	workspace := Workspace{
		ObjectID: "Account", WorkspaceVersion: "v", ViewRef: ref, Receipts: validReceipts(50),
		Layouts: make([]LayoutSummary, MaxWorkspaceLayouts), Assignments: []LayoutAssignmentSummary{{
			AssignmentID: "assignment-default", ObjectID: "Account", LayoutID: "layout-0000",
			Selectors: AssignmentSelectors{}, Specificity: 0, ResourceVersion: "v",
		}},
	}
	for index := range workspace.Layouts {
		workspace.Layouts[index] = LayoutSummary{
			LayoutID: fmt.Sprintf("layout-%04d", index), ObjectID: "Account", Name: "n", Kind: "k",
			ResourceVersion: "v",
		}
	}
	workspace.Layouts[0].IsDefault = true
	current := SemanticResponseSize(workspace)
	if current > target {
		t.Fatalf("minimum workspace size = %d, exceeds target %d", current, target)
	}
	remaining := target - current
	for index := range workspace.Layouts {
		fields := []struct {
			value   *string
			maximum int
		}{
			{&workspace.Layouts[index].Name, MaxNameBytes},
			{&workspace.Layouts[index].Kind, MaxKindBytes},
			{&workspace.Layouts[index].ResourceVersion, MaxVersionBytes},
		}
		for _, field := range fields {
			if remaining == 0 {
				return workspace
			}
			grown, used := growASCIIString(*field.value, field.maximum, remaining)
			*field.value = grown
			remaining -= used
		}
	}
	if remaining != 0 {
		remaining = addTimestampSize(workspace.Layouts, remaining)
	}
	if remaining != 0 {
		t.Fatalf("workspace capacity exhausted with %d bytes remaining", remaining)
	}
	return workspace
}

func addTimestampSize(layouts []LayoutSummary, remaining int) int {
	lengths := []int{20, 22, 23, 24, 25, 26, 27, 28, 29, 30}
	previous := make([]int, remaining+1)
	for index := range previous {
		previous[index] = -1
	}
	previous[0] = 0
	for amount := 1; amount <= remaining; amount++ {
		for _, length := range lengths {
			if amount >= length && previous[amount-length] >= 0 {
				previous[amount] = length
				break
			}
		}
	}
	if previous[remaining] < 0 {
		return remaining
	}
	usedLayouts := 0
	for remaining > 0 && usedLayouts < len(layouts) {
		length := previous[remaining]
		if length == 20 {
			layouts[usedLayouts].CreatedAt = "2026-01-01T00:00:00Z"
		} else {
			layouts[usedLayouts].CreatedAt = "2026-01-01T00:00:00." + strings.Repeat("1", length-21) + "Z"
		}
		remaining -= length
		usedLayouts++
	}
	return remaining
}

// growASCIIString consumes as much semantic-size growth as possible without
// exceeding remaining. It accounts for unsigned-varint length-prefix steps;
// another field consumes a one-byte gap at a prefix transition.
func growASCIIString(value string, maximum, remaining int) (string, int) {
	base := encodedSemanticStringSize(len(value))
	bestLength := len(value)
	bestGrowth := 0
	low, high := len(value), maximum
	for low <= high {
		middle := low + (high-low)/2
		growth := encodedSemanticStringSize(middle) - base
		if growth <= remaining {
			bestLength, bestGrowth = middle, growth
			low = middle + 1
		} else {
			high = middle - 1
		}
	}
	return value + strings.Repeat("x", bestLength-len(value)), bestGrowth
}

func encodedSemanticStringSize(length int) int {
	var encoded [binary.MaxVarintLen64]byte
	return 1 + binary.PutUvarint(encoded[:], uint64(length)) + length
}

func assertResourceExhausted(t *testing.T, err error, kind LimitKind, maximum int) {
	t.Helper()
	failure, ok := err.(*Failure)
	if !ok || failure.Category != FailureResourceExhausted || failure.LimitKind != kind || failure.Maximum != uint64(maximum) {
		t.Fatalf("error = %#v, want RESOURCE_EXHAUSTED %s max %d", err, kind, maximum)
	}
}
