package mapping

import (
	"context"
	"encoding/json"
	"errors"
	"reflect"
	"testing"
	"time"

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

func TestMetadataAdminReadWireRoundTripsPreserveThreeReadVariants(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	minimum := uint64(7)
	description := "Customer accounts"
	receipts := semantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
		CatalogRevision: 7, SchemaGeneration: 2, EngineGeneration: 3, SecurityEpoch: 4,
	}

	indexRequest := semantic.ObjectAdminIndexRequest{MinCatalogRevision: &minimum}
	wireIndexRequest, err := ToWireReadObjectAdminIndexRequest(ctx, indexRequest)
	if err != nil {
		t.Fatalf("index request to wire = %v", err)
	}
	roundIndexRequest, err := FromWireReadObjectAdminIndexRequest(ctx, wireIndexRequest)
	if err != nil || !reflect.DeepEqual(roundIndexRequest, indexRequest) {
		t.Fatalf("index request round trip = %#v, %v", roundIndexRequest, err)
	}
	indexResponse := semantic.ObjectAdminIndexResponse{
		Objects:  []semantic.ObjectAdminMetadata{{ObjectID: "Account", Description: &description, DeleteMode: "RestrictedDelete"}},
		Receipts: receipts,
	}
	wireIndexResponse, err := ToWireReadObjectAdminIndexResponse(indexRequest, indexResponse)
	if err != nil {
		t.Fatalf("index response to wire = %v", err)
	}
	roundIndexResponse, err := FromWireReadObjectAdminIndexResponse(indexRequest, wireIndexResponse)
	if err != nil || !reflect.DeepEqual(roundIndexResponse, indexResponse) {
		t.Fatalf("index response round trip = %#v, %v", roundIndexResponse, err)
	}

	workspaceRequest := semantic.LayoutAdminWorkspaceRequest{ObjectID: "Account", MinCatalogRevision: &minimum}
	wireWorkspaceRequest, err := ToWireReadLayoutAdminWorkspaceRequest(ctx, workspaceRequest)
	if err != nil {
		t.Fatalf("workspace request to wire = %v", err)
	}
	roundWorkspaceRequest, err := FromWireReadLayoutAdminWorkspaceRequest(ctx, wireWorkspaceRequest)
	if err != nil || !reflect.DeepEqual(roundWorkspaceRequest, workspaceRequest) {
		t.Fatalf("workspace request round trip = %#v, %v", roundWorkspaceRequest, err)
	}
	workspaceResponse := semantic.LayoutAdminWorkspaceResponse{
		ObjectID:    "Account",
		Layouts:     []semantic.LayoutSummary{{LayoutID: "layout-default", ObjectID: "Account", Name: "Account Layout", Type: "Detail", IsDefault: true}},
		Assignments: []semantic.LayoutAssignment{{AssignmentID: "assignment-sales", ObjectID: "Account", LayoutID: "layout-default", Dimensions: []semantic.LayoutAssignmentDimension{{Key: "audience", Value: "sales"}}, Specificity: 1}},
		Receipts:    receipts,
	}
	wireWorkspaceResponse, err := ToWireReadLayoutAdminWorkspaceResponse(workspaceRequest, workspaceResponse)
	if err != nil {
		t.Fatalf("workspace response to wire = %v", err)
	}
	roundWorkspaceResponse, err := FromWireReadLayoutAdminWorkspaceResponse(workspaceRequest, wireWorkspaceResponse)
	if err != nil || !reflect.DeepEqual(roundWorkspaceResponse, workspaceResponse) {
		t.Fatalf("workspace response round trip = %#v, %v", roundWorkspaceResponse, err)
	}

	storedRequest := semantic.StoredLayoutRequest{LayoutID: "layout-default", MinCatalogRevision: &minimum}
	wireStoredRequest, err := ToWireReadStoredLayoutRequest(ctx, storedRequest)
	if err != nil {
		t.Fatalf("stored request to wire = %v", err)
	}
	roundStoredRequest, err := FromWireReadStoredLayoutRequest(ctx, wireStoredRequest)
	if err != nil || !reflect.DeepEqual(roundStoredRequest, storedRequest) {
		t.Fatalf("stored request round trip = %#v, %v", roundStoredRequest, err)
	}
	storedResponse := semantic.StoredLayoutResponse{
		LayoutID: "layout-default", ObjectID: "Account",
		StoredDocument: []byte(`{"id":"layout-default","objectApiName":"Account","layoutName":"Account Layout"}`),
		Receipts:       receipts,
	}
	wireStoredResponse, err := ToWireReadStoredLayoutResponse(storedRequest, storedResponse)
	if err != nil {
		t.Fatalf("stored response to wire = %v", err)
	}
	roundStoredResponse, err := FromWireReadStoredLayoutResponse(storedRequest, wireStoredResponse)
	if err != nil || !reflect.DeepEqual(roundStoredResponse, storedResponse) {
		t.Fatalf("stored response round trip = %#v, %v", roundStoredResponse, err)
	}
}

func TestMetadataAdminReadWireRoundTripPreservesValidationRuleIndex(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	minimum := uint64(7)
	request := semantic.ValidationRuleAdminIndexRequest{ObjectID: "WorkItem", MinCatalogRevision: &minimum}
	wireRequest, err := ToWireReadValidationRuleAdminIndexRequest(ctx, request)
	if err != nil {
		t.Fatalf("request to wire = %v", err)
	}
	roundRequest, err := FromWireReadValidationRuleAdminIndexRequest(ctx, wireRequest)
	if err != nil || !reflect.DeepEqual(roundRequest, request) {
		t.Fatalf("request round trip = %#v, %v", roundRequest, err)
	}
	description := "Priority must be positive"
	field := "Priority"
	ownerPack := "pack-a"
	response := semantic.ValidationRuleAdminIndexResponse{
		ObjectID: "WorkItem",
		Rules: []semantic.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: []semantic.ValidationRuleRecordSetBinding{{
				Name: "hasOpenDependency", Mode: "exists", ObjectID: "Dependency", FilterLogic: "1",
				Filters: []semantic.ValidationRuleRecordSetFilter{{
					Field: "Status", Operator: "eq", ValueSource: "literal", Value: json.RawMessage(`"Open"`),
				}},
			}},
		}},
		Receipts: semantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
			CatalogRevision: 7, SchemaGeneration: 2, EngineGeneration: 3, SecurityEpoch: 4,
		},
	}
	wireResponse, err := ToWireReadValidationRuleAdminIndexResponse(request, response)
	if err != nil {
		t.Fatalf("response to wire = %v", err)
	}
	roundResponse, err := FromWireReadValidationRuleAdminIndexResponse(request, wireResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("response round trip = %#v, %v", roundResponse, err)
	}
}

func TestMetadataAdminReadWireRejectsMissingReceiptsAndNilEntries(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	indexRequest := semantic.ObjectAdminIndexRequest{}
	wireRequest, err := ToWireReadObjectAdminIndexRequest(ctx, indexRequest)
	if err != nil {
		t.Fatalf("request = %v", err)
	}
	if _, err := FromWireReadObjectAdminIndexRequest(ctx, wireRequest); err != nil {
		t.Fatalf("round request = %v", err)
	}
	if _, err := FromWireReadObjectAdminIndexResponse(indexRequest, nil); err == nil {
		t.Fatal("expected nil response rejection")
	}
}

func TestMetadataAdminReadWirePreflightsValidationRuleCollectionsBeforeAllocation(t *testing.T) {
	request := semantic.ValidationRuleAdminIndexRequest{ObjectID: "WorkItem"}
	baseResponse := semantic.ValidationRuleAdminIndexResponse{
		ObjectID: "WorkItem",
		Rules: []semantic.ValidationRuleMetadata{{
			RuleID: "rule-a", ObjectID: "WorkItem", Name: "RequiredPriority",
			Condition: "Priority == null", ErrorMessage: "Priority is required",
			RecordSetBindings: []semantic.ValidationRuleRecordSetBinding{{
				Name: "dependency", Mode: "exists", ObjectID: "Dependency",
				Filters: []semantic.ValidationRuleRecordSetFilter{{
					Field: "State", Operator: "eq", ValueSource: "literal", Value: json.RawMessage(`"open"`),
				}},
			}},
		}},
		Receipts: semantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
			CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1,
		},
	}
	newWireResponse := func(t *testing.T) *runtimev1.ReadValidationRuleAdminIndexResponse {
		t.Helper()
		response, err := ToWireReadValidationRuleAdminIndexResponse(request, baseResponse)
		if err != nil {
			t.Fatalf("valid response to wire = %v", err)
		}
		return response
	}

	for _, test := range []struct {
		name      string
		limitKind semantic.LimitKind
		maximum   uint64
		mutate    func(*runtimev1.ReadValidationRuleAdminIndexResponse)
	}{
		{
			name: "rules", limitKind: semantic.LimitRecordCount, maximum: semantic.MaxValidationRulesPerObject,
			mutate: func(response *runtimev1.ReadValidationRuleAdminIndexResponse) {
				response.Rules = make([]*runtimev1.ValidationRuleAdminMetadata, semantic.MaxValidationRulesPerObject+1)
			},
		},
		{
			name: "bindings", limitKind: semantic.LimitRecordCount, maximum: semantic.MaxRecordSetBindingsPerRule,
			mutate: func(response *runtimev1.ReadValidationRuleAdminIndexResponse) {
				response.Rules[0].RecordSetBindings = make(
					[]*runtimev1.ValidationRuleRecordSetBinding,
					semantic.MaxRecordSetBindingsPerRule+1,
				)
			},
		},
		{
			name: "filters", limitKind: semantic.LimitRecordCount, maximum: semantic.MaxRecordSetFiltersPerBinding,
			mutate: func(response *runtimev1.ReadValidationRuleAdminIndexResponse) {
				response.Rules[0].RecordSetBindings[0].Filters = make(
					[]*runtimev1.ValidationRuleRecordSetFilter,
					semantic.MaxRecordSetFiltersPerBinding+1,
				)
			},
		},
		{
			name: "aggregate projection entries", limitKind: semantic.LimitRecordCount,
			maximum: semantic.MaxValidationRuleProjectionEntries,
			mutate: func(response *runtimev1.ReadValidationRuleAdminIndexResponse) {
				bindings := make([]*runtimev1.ValidationRuleRecordSetBinding, 128)
				filter := &runtimev1.ValidationRuleRecordSetFilter{}
				for bindingIndex := range bindings {
					filters := make([]*runtimev1.ValidationRuleRecordSetFilter, semantic.MaxRecordSetFiltersPerBinding)
					for filterIndex := range filters {
						filters[filterIndex] = filter
					}
					bindings[bindingIndex] = &runtimev1.ValidationRuleRecordSetBinding{Filters: filters}
				}
				response.Rules[0].RecordSetBindings = bindings
			},
		},
		{
			name: "filter value", limitKind: semantic.LimitSemanticResponse, maximum: semantic.MaxFilterValueBytes,
			mutate: func(response *runtimev1.ReadValidationRuleAdminIndexResponse) {
				response.Rules[0].RecordSetBindings[0].Filters[0].ValueJson = make(
					[]byte,
					semantic.MaxFilterValueBytes+1,
				)
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			response := newWireResponse(t)
			test.mutate(response)
			_, err := FromWireReadValidationRuleAdminIndexResponse(request, response)
			var failure *semantic.Failure
			if !errors.As(err, &failure) || failure.Category != semantic.FailureResourceExhausted ||
				failure.LimitKind != test.limitKind || failure.Maximum != test.maximum {
				t.Fatalf("failure = %#v, want %s maximum %d", err, test.limitKind, test.maximum)
			}
		})
	}
}

func TestMetadataAdminReadStatusPreservesClosedFailureGrammar(t *testing.T) {
	for _, test := range []struct {
		name     string
		failure  *semantic.Failure
		wantCode codes.Code
	}{
		{name: "forbidden", failure: &semantic.Failure{Category: semantic.FailureForbidden}, wantCode: codes.PermissionDenied},
		{name: "missing", failure: &semantic.Failure{Category: semantic.FailureMissing}, wantCode: codes.NotFound},
		{name: "operation", failure: &semantic.Failure{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentOperation}, wantCode: codes.InvalidArgument},
		{name: "objects", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxObjects}, wantCode: codes.ResourceExhausted},
		{name: "validation rules", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxValidationRulesPerObject}, wantCode: codes.ResourceExhausted},
		{name: "record-set bindings", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxRecordSetBindingsPerRule}, wantCode: codes.ResourceExhausted},
		{name: "record-set filters", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxRecordSetFiltersPerBinding}, wantCode: codes.ResourceExhausted},
		{name: "validation-rule projection entries", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxValidationRuleProjectionEntries}, wantCode: codes.ResourceExhausted},
		{name: "filter value", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitSemanticResponse, Maximum: semantic.MaxFilterValueBytes}, wantCode: codes.ResourceExhausted},
		{name: "stored document", failure: &semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitSemanticResponse, Maximum: semantic.MaxStoredDocumentBytes}, wantCode: codes.ResourceExhausted},
	} {
		t.Run(test.name, func(t *testing.T) {
			wireError := ToPlatformStatusError(test.failure, semantic.CapabilityName)
			if status.Code(wireError) != test.wantCode {
				t.Fatalf("wire status = %v, want %v", status.Code(wireError), test.wantCode)
			}
			var decoded *semantic.Failure
			if err := FromPlatformStatusError(wireError, semantic.CapabilityName); !errors.As(err, &decoded) ||
				decoded.Category != test.failure.Category || decoded.InvalidComponent != test.failure.InvalidComponent ||
				decoded.LimitKind != test.failure.LimitKind || decoded.Maximum != test.failure.Maximum {
				t.Fatalf("decoded = %#v, want %#v", err, test.failure)
			}
		})
	}
}
