package metadataadminreadv1

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"
)

func TestObjectAdminIndexIsBoundedCanonicalAndRevisionBound(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	minimum := uint64(7)
	request := ObjectAdminIndexRequest{MinCatalogRevision: &minimum}
	if err := ValidateObjectAdminIndexRequest(ctx, request); err != nil {
		t.Fatalf("request = %v", err)
	}
	description := "Customer accounts"
	response := ObjectAdminIndexResponse{
		Objects: []ObjectAdminMetadata{
			{ObjectID: "Account", Description: &description, DeleteMode: "RestrictedDelete"},
			{ObjectID: "Contact", DeleteMode: "CascadeDelete"},
		},
		Receipts: testReceipts(minimum),
	}
	if err := ValidateObjectAdminIndexResponse(request, response); err != nil {
		t.Fatalf("response = %v", err)
	}

	unsorted := response
	unsorted.Objects = append([]ObjectAdminMetadata(nil), response.Objects...)
	unsorted.Objects[0], unsorted.Objects[1] = unsorted.Objects[1], unsorted.Objects[0]
	if err := ValidateObjectAdminIndexResponse(request, unsorted); err == nil {
		t.Fatal("expected non-canonical object order rejection")
	}

	duplicate := response
	duplicate.Objects = append(duplicate.Objects, response.Objects[0])
	if err := ValidateObjectAdminIndexResponse(request, duplicate); err == nil {
		t.Fatal("expected duplicate object rejection")
	}

	stale := response
	stale.Receipts.CatalogRevision = minimum - 1
	if err := ValidateObjectAdminIndexResponse(request, stale); err == nil {
		t.Fatal("expected stale response rejection")
	}

	invalidRequest := request
	invalidRequest.MinCatalogRevision = new(uint64)
	if err := ValidateObjectAdminIndexResponse(invalidRequest, response); err == nil {
		t.Fatal("expected response validator to reject zero minimum revision")
	}

	largeDescription := strings.Repeat("x", MaxDescriptionBytes)
	tooLarge := ObjectAdminIndexResponse{Receipts: testReceipts(minimum)}
	for index := 0; index < MaxSemanticResponseBytes/MaxDescriptionBytes+1; index++ {
		tooLarge.Objects = append(tooLarge.Objects, ObjectAdminMetadata{
			ObjectID: fmt.Sprintf("Object_%04d", index), Description: &largeDescription,
		})
	}
	var failure *Failure
	if err := ValidateObjectAdminIndexResponse(request, tooLarge); !errors.As(err, &failure) ||
		failure.Category != FailureResourceExhausted || failure.LimitKind != LimitSemanticResponse {
		t.Fatalf("aggregate raw budget = %#v", err)
	}
}

func TestLayoutAdminWorkspaceKeepsStoredRowsAndAssignmentsTogether(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := LayoutAdminWorkspaceRequest{ObjectID: "Account"}
	if err := ValidateLayoutAdminWorkspaceRequest(ctx, request); err != nil {
		t.Fatalf("request = %v", err)
	}
	response := LayoutAdminWorkspaceResponse{
		ObjectID: "Account",
		Layouts: []LayoutSummary{
			{LayoutID: "layout-default", ObjectID: "Account", Name: "Account Layout", Type: "Detail", IsDefault: true},
			{LayoutID: "layout-sales", ObjectID: "Account", Name: "Sales Layout", Type: "Detail"},
		},
		Assignments: []LayoutAssignment{
			{AssignmentID: "assignment-default", ObjectID: "Account", LayoutID: "layout-default"},
			{AssignmentID: "assignment-sales", ObjectID: "Account", LayoutID: "layout-sales", Dimensions: []LayoutAssignmentDimension{{Key: "audience", Value: "sales"}}, Specificity: 1},
		},
		Receipts: testReceipts(7),
	}
	if err := ValidateLayoutAdminWorkspaceResponse(request, response); err != nil {
		t.Fatalf("response = %v", err)
	}

	wrongObject := response
	wrongObject.Layouts = append([]LayoutSummary(nil), response.Layouts...)
	wrongObject.Layouts[0].ObjectID = "Contact"
	if err := ValidateLayoutAdminWorkspaceResponse(request, wrongObject); err == nil {
		t.Fatal("expected cross-object layout rejection")
	}

	orphan := response
	orphan.Assignments = append([]LayoutAssignment(nil), response.Assignments...)
	orphan.Assignments[0].LayoutID = "missing-layout"
	if err := ValidateLayoutAdminWorkspaceResponse(request, orphan); err == nil {
		t.Fatal("expected orphan assignment rejection")
	}

	badSpecificity := response
	badSpecificity.Assignments = append([]LayoutAssignment(nil), response.Assignments...)
	badSpecificity.Assignments[1].Specificity = 0
	if err := ValidateLayoutAdminWorkspaceResponse(request, badSpecificity); err == nil {
		t.Fatal("expected assignment specificity mismatch rejection")
	}

	unsortedDimensions := response
	unsortedDimensions.Assignments = append([]LayoutAssignment(nil), response.Assignments...)
	unsortedDimensions.Assignments[1].Dimensions = []LayoutAssignmentDimension{
		{Key: "zone", Value: "west"}, {Key: "audience", Value: "sales"},
	}
	unsortedDimensions.Assignments[1].Specificity = 2
	if err := ValidateLayoutAdminWorkspaceResponse(request, unsortedDimensions); err == nil {
		t.Fatal("expected non-canonical routing dimension rejection")
	}
}

func TestStoredLayoutResponseIsTheBoundedStoredDocument(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := StoredLayoutRequest{LayoutID: "layout-default"}
	if err := ValidateStoredLayoutRequest(ctx, request); err != nil {
		t.Fatalf("request = %v", err)
	}
	document := []byte(`dialect-owned stored authoring document`)
	response := StoredLayoutResponse{
		LayoutID: "layout-default", ObjectID: "Account",
		StoredDocument: document, Receipts: testReceipts(7),
	}
	if err := ValidateStoredLayoutResponse(request, response); err != nil {
		t.Fatalf("response = %v", err)
	}
	if string(response.StoredDocument) != string(document) {
		t.Fatal("stored document was transformed")
	}

	mismatched := response
	mismatched.LayoutID = "layout-other"
	if err := ValidateStoredLayoutResponse(request, mismatched); err == nil {
		t.Fatal("expected request/document identity mismatch rejection")
	}

	invalidUTF8 := response
	invalidUTF8.StoredDocument = []byte{0xff}
	if err := ValidateStoredLayoutResponse(request, invalidUTF8); err == nil {
		t.Fatal("expected invalid UTF-8 stored document rejection")
	}

	invalidRequest := request
	invalidRequest.LayoutID = ""
	if err := ValidateStoredLayoutResponse(invalidRequest, response); err == nil {
		t.Fatal("expected response validator to reject invalid request identity")
	}

	oversized := response
	oversized.StoredDocument = make([]byte, MaxStoredDocumentBytes+1)
	if err := ValidateStoredLayoutResponse(request, oversized); err == nil {
		t.Fatal("expected stored document bound rejection")
	}
}

func TestValidationRuleAdminIndexIsBoundedCanonicalAndObjectBound(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := ValidationRuleAdminIndexRequest{ObjectID: "WorkItem"}
	if err := ValidateValidationRuleAdminIndexRequest(ctx, request); err != nil {
		t.Fatalf("request = %v", err)
	}
	description := "Priority must be positive"
	field := "Priority"
	ownerPack := "pack-a"
	response := ValidationRuleAdminIndexResponse{
		ObjectID: "WorkItem",
		Rules: []ValidationRuleMetadata{
			{RuleID: "rule-a", ObjectID: "WorkItem", Name: "PositivePriority", Description: &description,
				Active: true, Condition: "Priority < 0", ErrorMessage: "Priority must be positive",
				ErrorDisplayField: &field, IsCustomized: true, OwnerPack: &ownerPack,
				RecordSetBindings: []ValidationRuleRecordSetBinding{{
					Name: "hasOpenDependency", Mode: "exists", ObjectID: "Dependency", FilterLogic: "1 AND 2",
					Filters: []ValidationRuleRecordSetFilter{
						{Field: "ParentId", Operator: "eq", ValueSource: "record", Value: json.RawMessage(`"Id"`)},
						{Field: "Status", Operator: "eq", ValueSource: "literal", Value: json.RawMessage(`"Open"`)},
					},
				}},
			},
			{RuleID: "rule-b", ObjectID: "WorkItem", Name: "RequiresStatus", Condition: "Status = null", ErrorMessage: "Status is required"},
		},
		Receipts: testReceipts(7),
	}
	if err := ValidateValidationRuleAdminIndexResponse(request, response); err != nil {
		t.Fatalf("response = %v", err)
	}

	wrongObject := response
	wrongObject.Rules = append([]ValidationRuleMetadata(nil), response.Rules...)
	wrongObject.Rules[0].ObjectID = "Account"
	if err := ValidateValidationRuleAdminIndexResponse(request, wrongObject); err == nil {
		t.Fatal("expected cross-object validation rule rejection")
	}

	wrongCase := response
	wrongCase.ObjectID = "workitem"
	if err := ValidateValidationRuleAdminIndexResponse(request, wrongCase); err == nil {
		t.Fatal("expected case-mismatched object identity rejection")
	}

	unsorted := response
	unsorted.Rules = append([]ValidationRuleMetadata(nil), response.Rules...)
	unsorted.Rules[0], unsorted.Rules[1] = unsorted.Rules[1], unsorted.Rules[0]
	if err := ValidateValidationRuleAdminIndexResponse(request, unsorted); err == nil {
		t.Fatal("expected non-canonical rule order rejection")
	}

	invalidValue := response
	invalidValue.Rules = append([]ValidationRuleMetadata(nil), response.Rules...)
	invalidValue.Rules[0].RecordSetBindings = append([]ValidationRuleRecordSetBinding(nil), response.Rules[0].RecordSetBindings...)
	invalidValue.Rules[0].RecordSetBindings[0].Filters = append([]ValidationRuleRecordSetFilter(nil), response.Rules[0].RecordSetBindings[0].Filters...)
	invalidValue.Rules[0].RecordSetBindings[0].Filters[0].Value = json.RawMessage(`not-json`)
	if err := ValidateValidationRuleAdminIndexResponse(request, invalidValue); err == nil {
		t.Fatal("expected invalid record-set filter value rejection")
	}
	invalidUTF8Value := json.RawMessage{'"', 0xff, '"'}
	if !json.Valid(invalidUTF8Value) {
		t.Fatal("test fixture must demonstrate that json.Valid accepts invalid UTF-8")
	}
	invalidValue.Rules[0].RecordSetBindings[0].Filters[0].Value = invalidUTF8Value
	if err := ValidateValidationRuleAdminIndexResponse(request, invalidValue); err == nil {
		t.Fatal("expected invalid UTF-8 record-set filter value rejection")
	}

	missingRequiredText := response
	missingRequiredText.Rules = append([]ValidationRuleMetadata(nil), response.Rules...)
	missingRequiredText.Rules[0].Condition = ""
	if err := ValidateValidationRuleAdminIndexResponse(request, missingRequiredText); err == nil {
		t.Fatal("expected blank validation-rule condition rejection")
	}
	missingRequiredText = response
	missingRequiredText.Rules = append([]ValidationRuleMetadata(nil), response.Rules...)
	missingRequiredText.Rules[0].ErrorMessage = ""
	if err := ValidateValidationRuleAdminIndexResponse(request, missingRequiredText); err == nil {
		t.Fatal("expected blank validation-rule error message rejection")
	}
}

func TestValidationRuleAdminIndexRejectsEveryCollectionOverflow(t *testing.T) {
	request := ValidationRuleAdminIndexRequest{ObjectID: "WorkItem"}
	base := ValidationRuleAdminIndexResponse{
		ObjectID: "WorkItem",
		Rules: []ValidationRuleMetadata{{
			RuleID: "rule-a", ObjectID: "WorkItem", Name: "RequiredStatus",
			Condition: "Status = null", ErrorMessage: "Status is required",
		}},
		Receipts: testReceipts(7),
	}
	tests := []struct {
		name     string
		response ValidationRuleAdminIndexResponse
		maximum  uint64
	}{
		{name: "rules", response: func() ValidationRuleAdminIndexResponse {
			response := base
			response.Rules = make([]ValidationRuleMetadata, MaxValidationRulesPerObject+1)
			return response
		}(), maximum: MaxValidationRulesPerObject},
		{name: "bindings", response: func() ValidationRuleAdminIndexResponse {
			response := base
			response.Rules = append([]ValidationRuleMetadata(nil), base.Rules...)
			response.Rules[0].RecordSetBindings = make([]ValidationRuleRecordSetBinding, MaxRecordSetBindingsPerRule+1)
			response.Rules[0].RecordSetBindings[0].Name = strings.Repeat("x", MaxSemanticResponseBytes+1)
			return response
		}(), maximum: MaxRecordSetBindingsPerRule},
		{name: "filters", response: func() ValidationRuleAdminIndexResponse {
			response := base
			response.Rules = append([]ValidationRuleMetadata(nil), base.Rules...)
			response.Rules[0].RecordSetBindings = []ValidationRuleRecordSetBinding{{
				Name: "related", Mode: "exists", ObjectID: "Dependency",
				Filters: make([]ValidationRuleRecordSetFilter, MaxRecordSetFiltersPerBinding+1),
			}}
			response.Rules[0].RecordSetBindings[0].Filters[0].Value = json.RawMessage(
				`"` + strings.Repeat("x", MaxSemanticResponseBytes) + `"`,
			)
			return response
		}(), maximum: MaxRecordSetFiltersPerBinding},
		{name: "aggregate projection entries", response: func() ValidationRuleAdminIndexResponse {
			response := base
			response.Rules = append([]ValidationRuleMetadata(nil), base.Rules...)
			response.Rules[0].RecordSetBindings = make([]ValidationRuleRecordSetBinding, 128)
			for index := range response.Rules[0].RecordSetBindings {
				response.Rules[0].RecordSetBindings[index].Filters = make(
					[]ValidationRuleRecordSetFilter,
					MaxRecordSetFiltersPerBinding,
				)
			}
			return response
		}(), maximum: MaxValidationRuleProjectionEntries},
		{name: "filter value", response: func() ValidationRuleAdminIndexResponse {
			response := base
			response.Rules = append([]ValidationRuleMetadata(nil), base.Rules...)
			response.Rules[0].RecordSetBindings = []ValidationRuleRecordSetBinding{{
				Name: "related", Mode: "exists", ObjectID: "Dependency",
				Filters: []ValidationRuleRecordSetFilter{{
					Field: "Status", Operator: "eq", ValueSource: "literal",
					Value: json.RawMessage(`"` + strings.Repeat("x", MaxFilterValueBytes) + `"`),
				}},
			}}
			return response
		}(), maximum: MaxFilterValueBytes},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var failure *Failure
			err := ValidateValidationRuleAdminIndexResponse(request, test.response)
			if !errors.As(err, &failure) || failure.Category != FailureResourceExhausted || failure.Maximum != test.maximum {
				t.Fatalf("failure = %#v, error = %v", failure, err)
			}
		})
	}
}

func TestMetadataAdminReadRequestsRequireDeadlineAndIdentifiers(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	for _, test := range []struct {
		name string
		err  error
	}{
		{"index without deadline", ValidateObjectAdminIndexRequest(context.Background(), ObjectAdminIndexRequest{})},
		{"workspace without object", ValidateLayoutAdminWorkspaceRequest(ctx, LayoutAdminWorkspaceRequest{})},
		{"stored without layout", ValidateStoredLayoutRequest(ctx, StoredLayoutRequest{})},
		{"validation rules without object", ValidateValidationRuleAdminIndexRequest(ctx, ValidationRuleAdminIndexRequest{})},
		{"zero minimum", ValidateObjectAdminIndexRequest(ctx, ObjectAdminIndexRequest{MinCatalogRevision: new(uint64)})},
	} {
		var failure *Failure
		if !errors.As(test.err, &failure) || failure.Category != FailureInvalidRequest {
			t.Fatalf("%s = %#v", test.name, test.err)
		}
	}
}

func testReceipts(revision uint64) Receipts {
	return Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: revision}
}
