package mapping

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

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

func TestResourceAccessRequestMappingCoversEveryOperationAcrossBinaryWire(t *testing.T) {
	revision := uint64(7)
	grant := semantic.GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}
	requests := []semantic.Request{
		{Operation: semantic.ListGrants, ResourceType: "folder", ResourceID: "folder-1", MinCatalogRevision: &revision},
		{Operation: semantic.GetGrant, ResourceType: "folder", GrantID: "grant-1", MinCatalogRevision: &revision},
		{CommandID: "create-1", Operation: semantic.CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant, MinCatalogRevision: &revision},
		{CommandID: "create-batch-1", Operation: semantic.CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grants: []semantic.GrantInput{grant}, MinCatalogRevision: &revision},
		{CommandID: "ensure-1", Operation: semantic.EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant, MinCatalogRevision: &revision},
		{CommandID: "replace-1", Operation: semantic.ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: []semantic.GrantInput{grant}, MinCatalogRevision: &revision},
		{CommandID: "update-1", Operation: semantic.UpdateGrant, ResourceType: "folder", GrantID: "grant-1", AccessLevel: "View", MinCatalogRevision: &revision},
		{CommandID: "delete-1", Operation: semantic.DeleteGrant, ResourceType: "folder", GrantID: "grant-1", MinCatalogRevision: &revision},
		{CommandID: "delete-type-1", Operation: semantic.DeletePrincipalType, ResourceType: "folder", ResourceID: "folder-1", PrincipalType: "Organization", MinCatalogRevision: &revision},
		{CommandID: "transfer-1", Operation: semantic.TransferOwnership, ResourceType: "folder", SourceOwnerID: "user-1", TargetOwnerID: "user-2", MinCatalogRevision: &revision},
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	for _, request := range requests {
		wireRequest, err := ToWireResourceAccessRequest(ctx, request)
		if err != nil {
			t.Fatalf("ToWireResourceAccessRequest(%s): %v", request.Operation, err)
		}
		encoded, err := proto.Marshal(wireRequest)
		if err != nil {
			t.Fatal(err)
		}
		decodedWire := new(runtimev1.AccessResourceRequest)
		if err := proto.Unmarshal(encoded, decodedWire); err != nil {
			t.Fatal(err)
		}
		decoded, err := FromWireResourceAccessRequest(ctx, decodedWire)
		if err != nil || !reflect.DeepEqual(decoded, request) {
			t.Fatalf("operation %s decoded=%#v err=%v, want %#v", request.Operation, decoded, err, request)
		}
	}
}

func TestResourceAccessOperationMappingIsComplete(t *testing.T) {
	for _, operation := range []semantic.Operation{
		semantic.ListGrants,
		semantic.GetGrant,
		semantic.CreateGrant,
		semantic.EnsureGrant,
		semantic.ReplaceGrants,
		semantic.UpdateGrant,
		semantic.DeleteGrant,
		semantic.DeletePrincipalType,
		semantic.TransferOwnership,
	} {
		wireOperation, ok := toWireResourceAccessOperation(operation)
		if !ok {
			t.Fatalf("semantic operation %q has no wire value", operation)
		}
		decoded, ok := fromWireResourceAccessOperation(wireOperation)
		if !ok || decoded != operation {
			t.Fatalf("operation %q decoded as %q, ok=%t", operation, decoded, ok)
		}
	}
	if _, ok := fromWireResourceAccessOperation(runtimev1.ResourceAccessOperation_RESOURCE_ACCESS_OPERATION_UNSPECIFIED); ok {
		t.Fatal("unspecified wire operation mapped to semantic")
	}
	if _, ok := fromWireResourceAccessOperation(runtimev1.ResourceAccessOperation(1)); ok {
		t.Fatal("removed visibility-enumeration operation mapped to semantic")
	}
}

func TestResourceAccessMutationIdentitySurvivesBinaryWire(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	grant := semantic.GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}
	for _, test := range []struct {
		name    string
		request semantic.Request
	}{
		{name: "create singleton", request: semantic.Request{CommandID: "create-single", Operation: semantic.CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant}},
		{name: "create batch", request: semantic.Request{CommandID: "create-batch", Operation: semantic.CreateGrant, ResourceType: "folder", ResourceID: "folder-1", Grants: []semantic.GrantInput{grant}}},
		{name: "ensure", request: semantic.Request{CommandID: "ensure-1", Operation: semantic.EnsureGrant, ResourceType: "folder", ResourceID: "folder-1", Grant: &grant}},
		{name: "replace populated", request: semantic.Request{CommandID: "replace-1", Operation: semantic.ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: []semantic.GrantInput{grant}}},
		{name: "replace empty", request: semantic.Request{CommandID: "replace-empty", Operation: semantic.ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1", Grants: []semantic.GrantInput{}}},
		{name: "update", request: semantic.Request{CommandID: "update-1", Operation: semantic.UpdateGrant, ResourceType: "folder", GrantID: "grant-1", AccessLevel: "View"}},
		{name: "delete", request: semantic.Request{CommandID: "delete-1", Operation: semantic.DeleteGrant, ResourceType: "folder", GrantID: "grant-1"}},
		{name: "delete principal type", request: semantic.Request{CommandID: "delete-type-1", Operation: semantic.DeletePrincipalType, ResourceType: "folder", ResourceID: "folder-1", PrincipalType: "Group"}},
		{name: "transfer ownership", request: semantic.Request{CommandID: "transfer-1", Operation: semantic.TransferOwnership, ResourceType: "folder", SourceOwnerID: "user-1", TargetOwnerID: "user-2"}},
	} {
		t.Run(test.name, func(t *testing.T) {
			wireRequest, err := ToWireResourceAccessRequest(ctx, test.request)
			if err != nil {
				t.Fatal(err)
			}
			encodedRequest, err := proto.Marshal(wireRequest)
			if err != nil {
				t.Fatal(err)
			}
			decodedWireRequest := new(runtimev1.AccessResourceRequest)
			if err := proto.Unmarshal(encodedRequest, decodedWireRequest); err != nil {
				t.Fatal(err)
			}
			decodedRequest, err := FromWireResourceAccessRequest(ctx, decodedWireRequest)
			if err != nil || decodedRequest.CommandID != test.request.CommandID {
				t.Fatalf("request round trip = %#v, %v", decodedRequest, err)
			}
			wantDigest, err := semantic.CommandDigest(test.request)
			if err != nil {
				t.Fatal(err)
			}
			gotDigest, err := semantic.CommandDigest(decodedRequest)
			if err != nil || gotDigest != wantDigest {
				t.Fatalf("digest after round trip = %q, %v; want %q", gotDigest, err, wantDigest)
			}
		})
	}

	request := semantic.Request{CommandID: "replace-1", Operation: semantic.ReplaceGrants, ResourceType: "folder", ResourceID: "folder-1"}
	response := semantic.Response{CommandID: request.CommandID, Receipts: semantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
		CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1,
	}}
	wireResponse, err := ToWireResourceAccessResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	encodedResponse, err := proto.Marshal(wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	decodedWireResponse := new(runtimev1.AccessResourceResponse)
	if err := proto.Unmarshal(encodedResponse, decodedWireResponse); err != nil {
		t.Fatal(err)
	}
	got, err := FromWireResourceAccessResponse(request, decodedWireResponse)
	if err != nil || got.CommandID != request.CommandID {
		t.Fatalf("response round trip = %#v, %v", got, err)
	}
}

func TestResourceAccessRejectsSingletonCreateWithExplicitEmptyBatchBeforeWire(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	grant := semantic.GrantInput{PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "Manage"}
	request := semantic.Request{
		CommandID: "create-1", Operation: semantic.CreateGrant, ResourceType: "folder", ResourceID: "folder-1",
		Grant: &grant, Grants: []semantic.GrantInput{},
	}
	if wireRequest, err := ToWireResourceAccessRequest(ctx, request); err == nil || wireRequest != nil {
		t.Fatalf("non-transportable request = %#v, %v; want rejected", wireRequest, err)
	}

	withExplicitEmpty := &runtimev1.AccessResourceRequest{
		CommandId: "create-1", Operation: runtimev1.ResourceAccessOperation_RESOURCE_ACCESS_OPERATION_CREATE_GRANT,
		ResourceType: "folder", ResourceId: "folder-1", Grant: toWireResourceAccessGrantInput(grant),
		Grants: []*runtimev1.ResourceAccessGrantInput{},
	}
	withNil := proto.Clone(withExplicitEmpty).(*runtimev1.AccessResourceRequest)
	withNil.Grants = nil
	emptyBytes, err := proto.Marshal(withExplicitEmpty)
	if err != nil {
		t.Fatal(err)
	}
	nilBytes, err := proto.Marshal(withNil)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(emptyBytes, nilBytes) {
		t.Fatalf("protobuf unexpectedly distinguishes explicit-empty and nil repeated fields: %x != %x", emptyBytes, nilBytes)
	}
	decodedWire := new(runtimev1.AccessResourceRequest)
	if err := proto.Unmarshal(emptyBytes, decodedWire); err != nil {
		t.Fatal(err)
	}
	if decodedWire.Grants != nil {
		t.Fatalf("decoded repeated grants = %#v, want nil", decodedWire.Grants)
	}
}

func TestResourceAccessOwnershipTransferResultSurvivesBinaryWire(t *testing.T) {
	request := semantic.Request{CommandID: "transfer-1", Operation: semantic.TransferOwnership, ResourceType: "folder", SourceOwnerID: "user-1", TargetOwnerID: "user-2"}
	response := semantic.Response{CommandID: request.CommandID, AffectedResources: 17, Receipts: semantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
		CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1,
	}}
	wireResponse, err := ToWireResourceAccessResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	decoded, err := FromWireResourceAccessResponse(request, wireResponse)
	if err != nil || decoded.AffectedResources != response.AffectedResources {
		t.Fatalf("ownership transfer result = %#v, %v", decoded, err)
	}
}

func TestResourceAccessPreservesConflictStatus(t *testing.T) {
	wireErr := ToPlatformStatusError(&semantic.Failure{Category: commonsemantic.FailureConflict}, semantic.CapabilityName)
	if status.Code(wireErr) != codes.Aborted {
		t.Fatalf("wire conflict code = %v", status.Code(wireErr))
	}
	var failure *semantic.Failure
	if err := FromPlatformStatusError(wireErr, semantic.CapabilityName); !errors.As(err, &failure) || failure.Category != commonsemantic.FailureConflict {
		t.Fatalf("round-trip conflict = %#v", err)
	}
}

func TestResourceAccessOwnershipTransferLimitStatusRoundTrip(t *testing.T) {
	source := &semantic.Failure{
		Category:  commonsemantic.FailureResourceExhausted,
		LimitKind: commonsemantic.LimitOperationCount,
		Maximum:   semantic.MaxOwnershipTransferResources,
	}
	wireErr := ToPlatformStatusError(source, semantic.CapabilityName)
	if got := status.Code(wireErr); got != codes.ResourceExhausted {
		t.Fatalf("wire ownership-transfer limit code = %v, want %v", got, codes.ResourceExhausted)
	}
	var failure *semantic.Failure
	if err := FromPlatformStatusError(wireErr, semantic.CapabilityName); !errors.As(err, &failure) ||
		failure.Category != source.Category || failure.LimitKind != source.LimitKind || failure.Maximum != source.Maximum {
		t.Fatalf("ownership-transfer limit round trip = %#v", err)
	}
}

func TestResourceAccessCommandIDFailureMapsToInvalidArgument(t *testing.T) {
	err := ToPlatformStatusError(&commonsemantic.Failure{
		Category:         commonsemantic.FailureInvalidRequest,
		InvalidComponent: commonsemantic.InvalidComponentCommandID,
	}, semantic.CapabilityName)
	if got := status.Code(err); got != codes.InvalidArgument {
		t.Fatalf("command ID failure code = %v, want %v", got, codes.InvalidArgument)
	}
}

func TestResourceAccessInboundPreflightRejectsCountsAndRawBytesBeforeGrantConversion(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	oversizedCount := &runtimev1.AccessResourceRequest{
		CommandId: "replace-1", Operation: runtimev1.ResourceAccessOperation_RESOURCE_ACCESS_OPERATION_REPLACE_GRANTS,
		ResourceType: "folder", ResourceId: "folder-1",
		// Nil members prove that the count limit wins before any nested grant is visited.
		Grants: make([]*runtimev1.ResourceAccessGrantInput, semantic.MaxGrants+1),
	}
	_, err := FromWireResourceAccessRequest(ctx, oversizedCount)
	var failure *commonsemantic.Failure
	if !errors.As(err, &failure) || failure.Category != commonsemantic.FailureResourceExhausted ||
		failure.LimitKind != commonsemantic.LimitOperationCount || failure.Maximum != semantic.MaxGrants {
		t.Fatalf("oversized grant count = %#v, want operation-count limit %d", err, semantic.MaxGrants)
	}

	oversizedRaw := &runtimev1.AccessResourceRequest{
		CommandId: "replace-1", Operation: runtimev1.ResourceAccessOperation_RESOURCE_ACCESS_OPERATION_REPLACE_GRANTS,
		ResourceType: "folder", ResourceId: "folder-1",
		Grants: []*runtimev1.ResourceAccessGrantInput{{
			PrincipalId: strings.Repeat("p", semantic.MaxSemanticRequestBytes), PrincipalType: "User", AccessLevel: "View",
		}},
	}
	_, err = FromWireResourceAccessRequest(ctx, oversizedRaw)
	if !errors.As(err, &failure) || failure.Category != commonsemantic.FailureResourceExhausted ||
		failure.LimitKind != commonsemantic.LimitSemanticRequest || failure.Maximum != semantic.MaxSemanticRequestBytes {
		t.Fatalf("oversized raw grant bytes = %#v, want semantic-request limit %d", err, semantic.MaxSemanticRequestBytes)
	}

	request := semantic.Request{Operation: semantic.ListGrants, ResourceType: "folder", ResourceID: "folder-1"}
	oversizedResponse := &runtimev1.AccessResourceResponse{
		Grants: make([]*runtimev1.ResourceAccessGrant, semantic.MaxGrants+1),
		Receipts: toWireReceipts(semantic.Receipts{
			ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
			CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1,
		}, platformCapability(semantic.CapabilityName)),
	}
	if got, responseErr := FromWireResourceAccessResponse(request, oversizedResponse); responseErr == nil || len(got.Grants) != 0 {
		t.Fatalf("oversized response = %#v, %v; want rejection before semantic grants", got, responseErr)
	}
}

func TestResourceAccessResponseMappingRejectsCrossResourceGrant(t *testing.T) {
	request := semantic.Request{Operation: semantic.ListGrants, ResourceType: "folder", ResourceID: "folder-1"}
	response := semantic.Response{Grants: []semantic.Grant{{ID: "grant-1", ResourceType: "folder", ResourceID: "folder-2", PrincipalID: "user-1", PrincipalType: "User", AccessLevel: "View"}}, Receipts: semantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName,
		CatalogRevision: 1, SchemaGeneration: 1, EngineGeneration: 1, SecurityEpoch: 1,
	}}
	if wire, err := ToWireResourceAccessResponse(request, response); err == nil || wire != nil {
		t.Fatalf("cross-resource response = %#v, %v; want rejected", wire, err)
	}
}
