package mapping

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

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinv1"
	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 recycleBinMappingContext(t *testing.T) context.Context {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	return ctx
}

func recycleBinMappingReceipts() semantic.Receipts {
	return semantic.Receipts{
		ProtocolVersion: "1.0", CatalogRevision: 11, SchemaGeneration: 12,
		EngineGeneration: 13, SecurityEpoch: 14, CapabilityVersion: semantic.CapabilityName,
	}
}

func TestRecycleBinWireRoundTripPreservesNeutralProjection(t *testing.T) {
	revision := uint64(10)
	request := semantic.Request{Scope: semantic.ScopeAll, MinCatalogRevision: &revision}
	wireRequest, err := ToWireListRecycleBinRequest(recycleBinMappingContext(t), request)
	if err != nil {
		t.Fatal(err)
	}
	roundRequest, err := FromWireListRecycleBinRequest(recycleBinMappingContext(t), wireRequest)
	if err != nil || roundRequest.Scope != semantic.ScopeAll || roundRequest.MinCatalogRevision == nil || *roundRequest.MinCatalogRevision != revision {
		t.Fatalf("request = %#v, %v", roundRequest, err)
	}

	response := semantic.Response{
		Items: []semantic.Item{{
			RecordID: "row-1", RecordName: "Visible", ObjectID: "WorkItem", DeletedBy: "Test User",
			DeletedDate: "2026-07-22T04:00:00Z", CreatedDate: "2026-07-21T04:00:00Z",
		}},
		FailedObjects: []semantic.ObjectFailure{{ObjectID: "RestrictedItem", Code: semantic.ObjectFailureForbidden, Message: "record read is forbidden"}},
		Receipts:      recycleBinMappingReceipts(),
	}
	wireResponse, err := ToWireListRecycleBinResponse(response)
	if err != nil {
		t.Fatal(err)
	}
	roundResponse, err := FromWireListRecycleBinResponse(wireResponse)
	if err != nil || len(roundResponse.Items) != 1 || roundResponse.Items[0].RecordID != "row-1" ||
		len(roundResponse.FailedObjects) != 1 || roundResponse.FailedObjects[0].Code != semantic.ObjectFailureForbidden ||
		roundResponse.Receipts.CatalogRevision != 11 {
		t.Fatalf("response = %#v, %v", roundResponse, err)
	}
}

func TestRecycleBinWireRejectsMalformedClosedShapes(t *testing.T) {
	ctx := recycleBinMappingContext(t)
	for name, request := range map[string]*runtimev1.ListRecycleBinRequest{
		"nil":         nil,
		"unspecified": {},
		"unknown":     {Scope: runtimev1.RecycleBinScope(99)},
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := FromWireListRecycleBinRequest(ctx, request); err == nil {
				t.Fatal("expected rejection")
			}
		})
	}
	receipts := toWireReceipts(recycleBinMappingReceipts(), recycleBinCapability)
	for name, response := range map[string]*runtimev1.ListRecycleBinResponse{
		"nil response": nil,
		"nil item":     {Items: []*runtimev1.RecycleBinItem{nil}, Receipts: receipts},
		"nil failure":  {FailedObjects: []*runtimev1.RecycleBinObjectFailure{nil}, Receipts: receipts},
		"unknown code": {FailedObjects: []*runtimev1.RecycleBinObjectFailure{{ObjectId: "WorkItem", Code: runtimev1.RecycleBinObjectFailureCode(99)}}, Receipts: receipts},
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := FromWireListRecycleBinResponse(response); err == nil {
				t.Fatal("expected rejection")
			}
		})
	}
}

func TestRecycleBinWireIgnoresUnknownOptionalFields(t *testing.T) {
	request := &runtimev1.ListRecycleBinRequest{Scope: runtimev1.RecycleBinScope_RECYCLE_BIN_SCOPE_MINE}
	request.ProtoReflect().SetUnknown([]byte{0x98, 0x06, 0x01})
	if decoded, err := FromWireListRecycleBinRequest(recycleBinMappingContext(t), request); err != nil || decoded.Scope != semantic.ScopeMine {
		t.Fatalf("request = %#v, %v", decoded, err)
	}

	response := &runtimev1.ListRecycleBinResponse{Receipts: toWireReceipts(recycleBinMappingReceipts(), recycleBinCapability)}
	response.ProtoReflect().SetUnknown([]byte{0x98, 0x06, 0x01})
	if decoded, err := FromWireListRecycleBinResponse(response); err != nil || decoded.Receipts.CatalogRevision != 11 {
		t.Fatalf("response = %#v, %v", decoded, err)
	}
}

func TestRecycleBinStatusIsCapabilityBoundAndNonDisclosing(t *testing.T) {
	wireErr := ToRecycleBinStatusError(&semantic.Failure{
		Category: semantic.FailureStaleState, StaleAxis: semantic.StaleAxisCatalog,
		RequiredRevision: 12, ServedRevision: 11, Receipts: ptrRecycleBinReceipts(recycleBinMappingReceipts()),
	})
	var round *semantic.Failure
	if err := FromRecycleBinStatusError(wireErr); !errors.As(err, &round) || round.StaleAxis != semantic.StaleAxisCatalog || round.RequiredRevision != 12 {
		t.Fatalf("status round trip = %#v", err)
	}

	unsafe := &semantic.Failure{Category: semantic.FailureInvalidRequest, CanonicalField: "SecretField", InvalidComponent: semantic.InvalidComponentOperation}
	if got := status.Code(ToRecycleBinStatusError(unsafe)); got != codes.Internal {
		t.Fatalf("unsafe detail code = %s", got)
	}

	wrong := &commonv1.RuntimeErrorDetail{Category: commonv1.ErrorCategory_ERROR_CATEGORY_STALE_STATE,
		Receipts: toWireReceipts(recycleBinMappingReceipts(), relatedCountCapability)}
	withDetail, err := status.New(codes.Aborted, "stale").WithDetails(wrong)
	if err != nil {
		t.Fatal(err)
	}
	if err := FromRecycleBinStatusError(withDetail.Err()); !errors.As(err, &round) || round.Category != semantic.FailureInternal {
		t.Fatalf("foreign receipt = %#v", err)
	}
}

func ptrRecycleBinReceipts(receipts semantic.Receipts) *semantic.Receipts { return &receipts }
