package mapping

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

	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordviewv1"
	commonv1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/common/v1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func recordViewContext(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func TestRecordViewWireRoundTripsBothBoundedOperations(t *testing.T) {
	ctx := recordViewContext(t)
	request := semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "record-1"}, Projection: semantic.ProjectionEnrichedView,
		Authority: &semantic.AuthoritySelection{Sections: []semantic.AuthoritySection{
			semantic.AuthoritySystemPermissions,
			semantic.AuthorityObjectPermissions,
			semantic.AuthorityFieldPermissions,
		}},
	}
	wireRequest, err := ToWireReadRecordViewRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	roundRequest, err := FromWireReadRecordViewRequest(ctx, wireRequest)
	if err != nil || !reflect.DeepEqual(roundRequest, request) {
		t.Fatalf("read request = %#v, %v", roundRequest, err)
	}
	recordID, ownerID := "record-1", "principal-1"
	response := semantic.ReadResponse{
		RecordID: recordID,
		Root: semantic.Record{ObjectID: "WorkItem", Fields: []semantic.Field{
			{Name: "Id", Value: semantic.Value{Kind: semantic.ValueString, String: &recordID}},
			{Name: "OwnerId", Value: semantic.Value{Kind: semantic.ValueReference, Reference: &ownerID}},
		}},
		ReferenceDisplays:      []semantic.ReferenceDisplay{{SourceFieldID: "OwnerId", RelationshipID: "Owner", TargetObjectID: "Principal", TargetRecordID: ownerID, DisplayName: "Ada Example"}},
		AttachmentSourceFields: []semantic.AttachmentSourceField{{SourceFieldID: "DocumentLink", ResolverID: "legacy_attachment"}},
		Access:                 &semantic.RecordAccess{Read: true, Update: true, Delete: false, Locked: false},
		Authority: &semantic.SelectedAuthority{
			SystemPermissions: []semantic.PermissionGrant{{PermissionID: "ApiEnabled", Granted: true}},
			ObjectPermissions: []semantic.ObjectPermission{{ResourceType: "WorkItem", CanRead: true, CanEdit: true}},
			FieldPermissions: []semantic.FieldPermission{
				{ResourceType: "WorkItem", FieldName: "Id", CanRead: true},
				{ResourceType: "WorkItem", FieldName: "OwnerId", CanRead: true, CanEdit: true, CanCreate: true},
			},
		},
		Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName},
	}
	documentLink := "opaque-source-value"
	response.Root.Fields = append(response.Root.Fields, semantic.Field{Name: "DocumentLink", Value: semantic.Value{Kind: semantic.ValueString, String: &documentLink}})
	slices.SortFunc(response.Root.Fields, func(a, b semantic.Field) int { return strings.Compare(a.Name, b.Name) })
	wireResponse, err := ToWireReadRecordViewResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	roundResponse, err := FromWireReadRecordViewResponse(request, wireResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("read response = %#v, %v", roundResponse, err)
	}

	resolveRequest := semantic.ResolveAttachmentSourcesRequest{ParentObjectID: "WorkItem", ParentRecordID: recordID, ResolverID: "legacy_attachment", SourceValues: []string{"source-1"}}
	wireResolveRequest, err := ToWireResolveRecordAttachmentSourcesRequest(ctx, resolveRequest)
	if err != nil {
		t.Fatal(err)
	}
	roundResolveRequest, err := FromWireResolveRecordAttachmentSourcesRequest(ctx, wireResolveRequest)
	if err != nil || !reflect.DeepEqual(roundResolveRequest, resolveRequest) {
		t.Fatalf("resolve request = %#v, %v", roundResolveRequest, err)
	}
	resolveResponse := semantic.ResolveAttachmentSourcesResponse{Results: []semantic.AttachmentSourceResult{{Index: 0, Outcome: semantic.AttachmentSourceResolved, FileID: "file-1", FileName: "example.txt"}}, Receipts: response.Receipts}
	wireResolveResponse, err := ToWireResolveRecordAttachmentSourcesResponse(resolveRequest, resolveResponse)
	if err != nil {
		t.Fatal(err)
	}
	roundResolveResponse, err := FromWireResolveRecordAttachmentSourcesResponse(resolveRequest, wireResolveResponse)
	if err != nil || !reflect.DeepEqual(roundResolveResponse, resolveResponse) {
		t.Fatalf("resolve response = %#v, %v", roundResolveResponse, err)
	}
}

func TestRecordViewWireRejectsOpenOrMalformedShapes(t *testing.T) {
	ctx := recordViewContext(t)
	for name, request := range map[string]*runtimev1.ReadRecordViewRequest{
		"missing selector":   {ObjectId: "WorkItem", Projection: runtimev1.RecordViewProjection_RECORD_VIEW_PROJECTION_ROOT},
		"unknown selector":   {ObjectId: "WorkItem", Selector: &runtimev1.RecordViewSelector{Kind: runtimev1.RecordViewSelectorKind(99)}, Projection: runtimev1.RecordViewProjection_RECORD_VIEW_PROJECTION_ROOT},
		"unknown projection": {ObjectId: "WorkItem", Selector: &runtimev1.RecordViewSelector{Kind: runtimev1.RecordViewSelectorKind_RECORD_VIEW_SELECTOR_KIND_RECORD_ID, RecordId: "record-1"}, Projection: runtimev1.RecordViewProjection(99)},
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := FromWireReadRecordViewRequest(ctx, request); err == nil {
				t.Fatal("expected rejection")
			}
		})
	}
	tooMany := &runtimev1.ResolveRecordAttachmentSourcesRequest{SourceValues: make([]string, semantic.MaxAttachmentSources+1)}
	if _, err := FromWireResolveRecordAttachmentSourcesRequest(ctx, tooMany); err == nil {
		t.Fatal("one-over attachment source batch accepted")
	}
}

func TestRecordViewStatusUsesItsOwnCapabilityAndMissingSemantics(t *testing.T) {
	for _, category := range []semantic.FailureCategory{semantic.FailureMissing, semantic.FailureForbidden, semantic.FailureUnavailable} {
		wireErr := ToRecordViewStatusError(&semantic.Failure{Category: category})
		var round *semantic.Failure
		if decoded := FromRecordViewStatusError(wireErr); !errors.As(decoded, &round) || round.Category != category {
			t.Fatalf("%s round trip = %#v", category, decoded)
		}
	}
	wireErr := ToRecordViewStatusError(&semantic.Failure{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentOperation})
	if status.Code(wireErr) != codes.InvalidArgument {
		t.Fatalf("status = %s", status.Code(wireErr))
	}
	details := status.Convert(wireErr).Details()
	if len(details) != 1 {
		t.Fatalf("details = %#v", details)
	}
	detail, ok := details[0].(*commonv1.RuntimeErrorDetail)
	if !ok || detail.GetInvalidRequest().GetComponent() != commonv1.InvalidComponent_INVALID_COMPONENT_OPERATION {
		t.Fatalf("detail = %#v", detail)
	}
	for _, limit := range []struct {
		kind    semantic.LimitKind
		maximum uint64
	}{
		{kind: querysemantic.LimitSelectedFields, maximum: querysemantic.MaxSelectedFields},
		{kind: querysemantic.LimitQueryDepth, maximum: querysemantic.MaxNestingDepth},
	} {
		wireErr := ToRecordViewStatusError(&semantic.Failure{
			Category: semantic.FailureResourceExhausted, LimitKind: limit.kind, Maximum: limit.maximum,
		})
		if status.Code(wireErr) != codes.ResourceExhausted {
			t.Fatalf("shared query limit %q status = %s", limit.kind, status.Code(wireErr))
		}
		var round *semantic.Failure
		if decoded := FromRecordViewStatusError(wireErr); !errors.As(decoded, &round) ||
			round.Category != semantic.FailureResourceExhausted || round.LimitKind != limit.kind || round.Maximum != limit.maximum {
			t.Fatalf("shared query limit %q round trip = %#v", limit.kind, decoded)
		}
	}
}

func TestRecordViewDecoderPreservesSharedQueryDepthLimit(t *testing.T) {
	root := &runtimev1.QueryRecord{ObjectId: "WorkItem"}
	current := root
	for depth := 0; depth < querysemantic.MaxNestingDepth; depth++ {
		nested := &runtimev1.QueryRecord{ObjectId: "Nested"}
		current.Fields = []*runtimev1.QueryField{{
			Name: "Nested", Value: &runtimev1.QueryValue{
				DeclaredKind: runtimev1.QueryValueKind_QUERY_VALUE_KIND_OBJECT,
				Value:        &runtimev1.QueryValue_ObjectValue{ObjectValue: nested},
			},
		}}
		current = nested
	}
	request := semantic.ReadRequest{
		ObjectID: "WorkItem", Selector: semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: "record-1"}, Projection: semantic.ProjectionRoot,
	}
	_, err := FromWireReadRecordViewResponse(request, &runtimev1.ReadRecordViewResponse{
		RecordId: "record-1", Root: root,
		Receipts: toWireReceipts(semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}, recordViewCapability),
	})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureResourceExhausted ||
		failure.LimitKind != querysemantic.LimitQueryDepth || failure.Maximum != querysemantic.MaxNestingDepth {
		t.Fatalf("deep query carrier error = %#v", err)
	}
}
