package pagelayoutresolutionv1

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func TestRequestCannotCarryCallerSecurityIdentity(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := Request{ObjectID: "Account", RecordTypeIDs: []string{"012000000000001AAA"}}
	if err := ValidateRequest(ctx, request); err != nil {
		t.Fatalf("ValidateRequest() = %v", err)
	}
	// The compile-time request shape is the security assertion: there is no
	// ProfileID, RoleID, principal, or session field a caller can populate.
}

func TestResponseBindsRequestDocumentRelatedResolutionAndReceipts(t *testing.T) {
	revision := uint64(17)
	request := Request{
		ObjectID:           "Account",
		RecordTypeIDs:      []string{"012000000000001AAA", "012000000000002AAA"},
		MinCatalogRevision: &revision,
	}
	response := validResponse(request)
	if err := ValidateResponseForRequest(request, response); err != nil {
		t.Fatalf("ValidateResponseForRequest() = %v", err)
	}

	wrongRecordType := "012000000000003AAA"
	response.Selections[0].RecordTypeID = &wrongRecordType
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureInternal)
	response = validResponse(request)
	response.Layouts[0].RelatedLists[0].Create.PrefillMappings = append(response.Layouts[0].RelatedLists[0].Create.PrefillMappings,
		FieldMapping{TargetFieldID: "AField", SourceFieldID: "Id"})
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureInternal)
	response = validResponse(request)
	response.Receipts.CatalogRevision = 16
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureInternal)
}

func TestResponseRejectsInvalidResolvedFieldTargets(t *testing.T) {
	request := Request{ObjectID: "Account"}
	for name, mutate := range map[string]func(*RelatedListResolution){
		"display lookup": func(value *RelatedListResolution) { value.DisplayLookupField = "Parent\nId" },
		"query lookup":   func(value *RelatedListResolution) { value.Query.LookupField = "Parent.Id" },
		"query parent": func(value *RelatedListResolution) {
			value.Query.ParentFieldID = strings.Repeat("F", MaxOpaqueIdentifierBytes)
		},
		"list filter":    func(value *RelatedListResolution) { value.List.FilterField = "Parent Id" },
		"list parent":    func(value *RelatedListResolution) { value.List.ParentFieldID = " ParentId" },
		"mapping target": func(value *RelatedListResolution) { value.Create.PrefillMappings[0].TargetFieldID = "Target.Field" },
		"mapping source": func(value *RelatedListResolution) { value.Create.PrefillMappings[0].SourceFieldID = "Source/Field" },
	} {
		t.Run(name, func(t *testing.T) {
			response := validResponse(request)
			mutate(&response.Layouts[0].RelatedLists[0])
			assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureInternal)
		})
	}
}

func TestResponseRequiresJSONObjectAndOwnedDocumentVersion(t *testing.T) {
	request := Request{ObjectID: "Account"}
	for name, mutate := range map[string]func(*Response){
		"array document":  func(response *Response) { response.Layouts[0].LayoutDocument = []byte(`[]`) },
		"scalar document": func(response *Response) { response.Layouts[0].LayoutDocument = []byte(`"layout"`) },
		"wrong kind":      func(response *Response) { response.Layouts[0].DocumentKind = "other" },
		"wrong version":   func(response *Response) { response.Layouts[0].DocumentFormatVersion++ },
	} {
		t.Run(name, func(t *testing.T) {
			response := validResponse(request)
			mutate(&response)
			assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureInternal)
		})
	}
}

func TestLayoutDocumentRemainsOpaqueToNeutralContract(t *testing.T) {
	request := Request{ObjectID: "Account"}
	response := validResponse(request)
	response.Layouts[0].LayoutDocument = []byte(`{"dialectOwnedShape":{"anything":true}}`)
	if err := ValidateResponseForRequest(request, response); err != nil {
		t.Fatalf("opaque layout document = %v", err)
	}
}

func TestAggregateDocumentBudgetRunsBeforeDocumentParsing(t *testing.T) {
	request := Request{ObjectID: "Account", RecordTypeIDs: []string{"rt-a", "rt-b"}}
	response := validResponse(request)
	response.Selections[1].LayoutID = "layout-other"
	response.Layouts = append(response.Layouts, ResolvedLayout{
		LayoutID: "layout-other", DocumentKind: PageLayoutDocumentKind,
		DocumentFormatVersion: PageLayoutDocumentFormatVersion,
	})
	response.Layouts[0].LayoutDocument = []byte(strings.Repeat("x", MaxLayoutDocumentBytes))
	response.Layouts[1].LayoutDocument = []byte(strings.Repeat("x", MaxLayoutDocumentBytes))
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureResourceExhausted)
}

func TestPrefillMappingLimitRunsBeforeMappingValidation(t *testing.T) {
	request := Request{ObjectID: "Account"}
	response := validResponse(request)
	response.Layouts[0].RelatedLists[0].Create.PrefillMappings = make([]FieldMapping, MaxPrefillMappingsPerResolution+1)
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureResourceExhausted)
}

func TestProjectionPayloadBudgetRequiresNoSerialization(t *testing.T) {
	request := Request{ObjectID: "Account", RecordTypeIDs: []string{"rt-a", "rt-b", "rt-c"}}
	response := validResponse(request)
	response.Layouts = make([]ResolvedLayout, 0, len(request.RecordTypeIDs))
	for index, recordTypeID := range request.RecordTypeIDs {
		layoutID := "layout-" + recordTypeID
		response.Selections[index].LayoutID = layoutID
		layout := ResolvedLayout{
			LayoutID: layoutID, DocumentKind: PageLayoutDocumentKind,
			DocumentFormatVersion: PageLayoutDocumentFormatVersion,
			LayoutDocument:        []byte(`{"id":"layout"}`),
			RelatedLists:          make([]RelatedListResolution, MaxRelatedListResolutions),
		}
		for relatedIndex := range layout.RelatedLists {
			layout.RelatedLists[relatedIndex] = maximumTextResolution()
		}
		response.Layouts = append(response.Layouts, layout)
	}
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureResourceExhausted)
}

func TestResponsePayloadBudgetPrecedesReceiptComparison(t *testing.T) {
	request := Request{ObjectID: "Account"}
	response := validResponse(request)
	response.Receipts.CapabilityVersion = strings.Repeat("x", MaxSemanticResponseBytes+1)
	assertFailureCategory(t, ValidateResponseForRequest(request, response), FailureResourceExhausted)
}

func maximumTextResolution() RelatedListResolution {
	objectID := "O" + strings.Repeat("b", common.MaxObjectIdentifierBytes-1)
	fieldID := "F" + strings.Repeat("b", common.MaxFieldIdentifierBytes-1)
	return RelatedListResolution{
		DisplayObjectID: objectID, DisplayLookupField: fieldID,
		Query:  ResolvedQueryTarget{ObjectID: objectID, LookupField: fieldID, ParentFieldID: fieldID},
		Detail: ResolvedDetailTarget{ObjectID: objectID},
		List:   ResolvedListTarget{ObjectID: objectID, FilterField: fieldID, ParentFieldID: fieldID},
		Create: ResolvedCreateTarget{Kind: "standard", ObjectID: objectID},
	}
}

func validResponse(request Request) Response {
	selections := []Selection{{LayoutID: "layout-account"}}
	if len(request.RecordTypeIDs) != 0 {
		selections = make([]Selection, 0, len(request.RecordTypeIDs))
		for index := range request.RecordTypeIDs {
			recordTypeID := request.RecordTypeIDs[index]
			selections = append(selections, Selection{RecordTypeID: &recordTypeID, LayoutID: "layout-account"})
		}
	}
	return Response{
		ObjectID: request.ObjectID, Selections: selections,
		Layouts: []ResolvedLayout{{
			LayoutID: "layout-account", DocumentKind: PageLayoutDocumentKind,
			DocumentFormatVersion: PageLayoutDocumentFormatVersion,
			LayoutDocument:        []byte(`{"id":"layout-account","objectApiName":"Account"}`),
			RelatedLists: []RelatedListResolution{{
				DisplayObjectID: "Attachment", DisplayLookupField: "ParentId",
				Query:  ResolvedQueryTarget{ObjectID: "ContentDocumentLink", LookupField: "LinkedEntityId"},
				Detail: ResolvedDetailTarget{ObjectID: "ContentDocument"},
				List:   ResolvedListTarget{ObjectID: "ContentDocumentLink", FilterField: "LinkedEntityId"},
				Create: ResolvedCreateTarget{Kind: "upload", PrefillMappings: []FieldMapping{{TargetFieldID: "LinkedEntityId", SourceFieldID: "Id"}}},
			}},
		}},
		Receipts: Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: 17, SecurityEpoch: 3},
	}
}

func assertFailureCategory(t *testing.T, err error, want FailureCategory) {
	t.Helper()
	var failure *Failure
	if !errors.As(err, &failure) || failure.Category != want {
		t.Fatalf("error = %#v, want %s", err, want)
	}
}
