package mapping

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

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	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"
	"google.golang.org/protobuf/encoding/protowire"
	"google.golang.org/protobuf/proto"
)

func TestTypedRequestAndResponseRoundTrip(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	revision := uint64(42)
	request := semantic.Request{ObjectID: "Issue", RecordID: "I-1", SelectedRootFields: []string{"Subject", "Archived"}, MinCatalogRevision: &revision}
	wireRequest, err := ToWireRequest(ctx, request)
	if err != nil {
		t.Fatalf("ToWireRequest() = %v", err)
	}
	roundRequest, err := FromWireRequest(ctx, wireRequest)
	if err != nil || !reflect.DeepEqual(roundRequest, request) {
		t.Fatalf("request round trip = %#v, %v", roundRequest, err)
	}

	empty := ""
	falseValue := false
	zero := int64(0)
	date := "2026-07-12"
	datetime := "2026-07-12T10:11:12.123Z"
	reference := "P-1"
	response := semantic.Response{
		RecordID: "I-1",
		Fields: []semantic.Field{
			{Name: "Archived", Kind: semantic.ValueKindBoolean, Boolean: &falseValue},
			{Name: "DueDate", Kind: semantic.ValueKindDate, Date: &date},
			{Name: "Empty", Kind: semantic.ValueKindString, String: &empty},
			{Name: "Estimate", Kind: semantic.ValueKindDecimal, Decimal: &semantic.Decimal{Coefficient: "0", Scale: 2}},
			{Name: "Optional", Kind: semantic.ValueKindString, Null: true},
			{Name: "ProjectId", Kind: semantic.ValueKindReference, Reference: &reference},
			{Name: "RetryCount", Kind: semantic.ValueKindInteger, Integer: &zero},
			{Name: "UpdatedAt", Kind: semantic.ValueKindDateTime, DateTime: &datetime},
		},
		Receipts: semantic.Receipts{ProtocolVersion: "1.0", CatalogRevision: 42, SchemaGeneration: 7, EngineGeneration: 9, SecurityEpoch: 11, CapabilityVersion: semantic.CapabilityName},
	}
	wireResponse, err := ToWireResponse(response)
	if err != nil {
		t.Fatalf("ToWireResponse() = %v", err)
	}
	roundResponse, err := FromWireResponse(wireResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("response round trip = %#v, %v", roundResponse, err)
	}
}

func TestUnknownOptionalFieldsRemainCompatible(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	base := &runtimev1.ReadSelectedRootRequest{ObjectId: "Issue", RecordId: "I-1", SelectedRootFields: []string{"Subject"}}
	encoded, err := proto.Marshal(base)
	if err != nil {
		t.Fatal(err)
	}
	encoded = protowire.AppendTag(encoded, 100, protowire.BytesType)
	encoded = protowire.AppendString(encoded, "future-optional")
	var decoded runtimev1.ReadSelectedRootRequest
	if err := proto.Unmarshal(encoded, &decoded); err != nil {
		t.Fatalf("unmarshal additive optional field: %v", err)
	}
	if _, err := FromWireRequest(ctx, &decoded); err != nil {
		t.Fatalf("mapping rejected unknown optional field: %v", err)
	}

	responseBytes, err := proto.Marshal(validWireResponse())
	if err != nil {
		t.Fatal(err)
	}
	responseBytes = protowire.AppendTag(responseBytes, 100, protowire.BytesType)
	responseBytes = protowire.AppendString(responseBytes, "future-optional")
	var decodedResponse runtimev1.ReadSelectedRootResponse
	if err := proto.Unmarshal(responseBytes, &decodedResponse); err != nil {
		t.Fatalf("unmarshal additive response field: %v", err)
	}
	if _, err := FromWireResponse(&decodedResponse); err != nil {
		t.Fatalf("mapping rejected unknown optional response field: %v", err)
	}
}

func TestUnknownOrMismatchedValueKindFailsClosed(t *testing.T) {
	base := validWireResponse()
	base.Fields = []*runtimev1.SelectedRootField{{
		Name: "Subject", DeclaredKind: runtimev1.ValueKind(99),
		Value: &runtimev1.SelectedRootField_StringValue{StringValue: "value"},
	}}
	var failure *semantic.Failure
	if _, err := FromWireResponse(base); !errors.As(err, &failure) || failure.Category != semantic.FailureInternal {
		t.Fatalf("unknown kind = %#v, want INTERNAL_FAILURE", err)
	}

	base = validWireResponse()
	base.Fields = []*runtimev1.SelectedRootField{{
		Name: "Subject", DeclaredKind: runtimev1.ValueKind_VALUE_KIND_BOOLEAN,
		Value: &runtimev1.SelectedRootField_StringValue{StringValue: "value"},
	}}
	if _, err := FromWireResponse(base); !errors.As(err, &failure) || failure.Category != semantic.FailureInternal {
		t.Fatalf("mismatched kind = %#v, want fail closed", err)
	}

	if wireField, err := toWireField(semantic.Field{
		Name: "Future", Kind: semantic.ValueKind("future-kind"), Null: true,
	}); err == nil || wireField != nil {
		t.Fatalf("future null semantic kind = %#v, %v; want fail closed", wireField, err)
	}
}

func TestStableStatusMappingsAndSafeDetails(t *testing.T) {
	tests := []struct {
		failure *semantic.Failure
		code    codes.Code
	}{
		{&semantic.Failure{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentField, CanonicalField: "Subject"}, codes.InvalidArgument},
		{&semantic.Failure{Category: semantic.FailureUnauthenticated}, codes.Unauthenticated},
		{&semantic.Failure{Category: semantic.FailureForbidden}, codes.PermissionDenied},
		{&semantic.Failure{Category: semantic.FailureMissing, Receipts: &semantic.Receipts{ProtocolVersion: "1.0", CatalogRevision: 12, SchemaGeneration: 3, EngineGeneration: 4, SecurityEpoch: 5, CapabilityVersion: semantic.CapabilityName}}, codes.NotFound},
		{&semantic.Failure{Category: semantic.FailureCanceled}, codes.Canceled},
		{&semantic.Failure{Category: semantic.FailureDeadlineExceeded}, codes.DeadlineExceeded},
		{&semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitSelectedFields, Maximum: 512}, codes.ResourceExhausted},
		{&semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitAdmission, Maximum: 16, RetryAfterMillis: 50}, codes.ResourceExhausted},
		{incompatibleFailure(), codes.FailedPrecondition},
		{&semantic.Failure{Category: semantic.FailureStaleState, StaleAxis: semantic.StaleAxisCatalog, RequiredRevision: 10, ServedRevision: 9}, codes.FailedPrecondition},
		{&semantic.Failure{Category: semantic.FailureUnavailable, RetryAfterMillis: 250}, codes.Unavailable},
		{&semantic.Failure{Category: semantic.FailureInternal, RequestID: "req-safe"}, codes.Internal},
	}
	for _, tc := range tests {
		t.Run(string(tc.failure.Category), func(t *testing.T) {
			wireErr := ToStatusError(tc.failure)
			if got := status.Code(wireErr); got != tc.code {
				t.Fatalf("status code = %s, want %s", got, tc.code)
			}
			round := FromStatusError(wireErr)
			var failure *semantic.Failure
			if !errors.As(round, &failure) || failure.Category != tc.failure.Category {
				t.Fatalf("round status = %#v", round)
			}
			if !reflect.DeepEqual(failure.Receipts, tc.failure.Receipts) {
				t.Fatalf("round receipts = %#v, want %#v", failure.Receipts, tc.failure.Receipts)
			}
			if failure.RetryAfterMillis != tc.failure.RetryAfterMillis {
				t.Fatalf("round retry-after = %d, want %d", failure.RetryAfterMillis, tc.failure.RetryAfterMillis)
			}
			for _, detail := range status.Convert(wireErr).Details() {
				if _, ok := detail.(*commonv1.RuntimeErrorDetail); !ok {
					t.Fatalf("unexpected public error detail %T", detail)
				}
			}
		})
	}
}

func TestRetryAfterOnNonAdmissionLimitFailsClosed(t *testing.T) {
	failure := &semantic.Failure{
		Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitSemanticRequest,
		Maximum: 1024, RetryAfterMillis: 50,
	}
	if got := status.Code(ToStatusError(failure)); got != codes.Internal {
		t.Fatalf("outbound non-admission retry-after status = %s, want INTERNAL", got)
	}

	detail := &commonv1.RuntimeErrorDetail{
		Category: commonv1.ErrorCategory_ERROR_CATEGORY_RESOURCE_EXHAUSTED,
		Detail: &commonv1.RuntimeErrorDetail_LimitViolation{LimitViolation: &commonv1.LimitViolationDetail{
			Kind:                   commonv1.LimitKind_LIMIT_KIND_SEMANTIC_REQUEST_BYTES,
			RetryAfterMilliseconds: 50,
		}},
	}
	wireErr, err := status.New(codes.ResourceExhausted, "runtime request failed").WithDetails(detail)
	if err != nil {
		t.Fatal(err)
	}
	var decoded *semantic.Failure
	if err := FromStatusError(wireErr.Err()); !errors.As(err, &decoded) || decoded.Category != semantic.FailureInternal {
		t.Fatalf("inbound non-admission retry-after decoded as %#v, want INTERNAL_FAILURE", err)
	}
}

func TestMissingAndRLSMaskedFailuresHaveIdenticalWireProjection(t *testing.T) {
	receipts := &semantic.Receipts{ProtocolVersion: "1.0", CatalogRevision: 12, SchemaGeneration: 3, EngineGeneration: 4, SecurityEpoch: 5, CapabilityVersion: semantic.CapabilityName}
	missing := status.Convert(ToStatusError(&semantic.Failure{Category: semantic.FailureMissing, Receipts: receipts})).Proto()
	rlsMasked := status.Convert(ToStatusError(&semantic.Failure{Category: semantic.FailureMissing, Receipts: receipts})).Proto()
	missingBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(missing)
	if err != nil {
		t.Fatal(err)
	}
	rlsBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(rlsMasked)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(missingBytes, rlsBytes) {
		t.Fatal("missing and RLS-masked failures differ on the wire")
	}
}

func TestMalformedFailureReceiptsFailClosed(t *testing.T) {
	err := ToStatusError(&semantic.Failure{
		Category: semantic.FailureMissing,
		Receipts: &semantic.Receipts{ProtocolVersion: "2.0", CapabilityVersion: "other"},
	})
	if status.Code(err) != codes.Internal {
		t.Fatalf("malformed receipt status = %s, want INTERNAL", status.Code(err))
	}
	var failure *semantic.Failure
	if decoded := FromStatusError(err); !errors.As(decoded, &failure) || failure.Category != semantic.FailureInternal {
		t.Fatalf("malformed receipt projection = %#v", decoded)
	}
}

func TestMalformedInboundSafeDetailsFailClosed(t *testing.T) {
	tests := []struct {
		name   string
		code   codes.Code
		detail *commonv1.RuntimeErrorDetail
	}{
		{
			name: "detail kind does not match category", code: codes.PermissionDenied,
			detail: &commonv1.RuntimeErrorDetail{
				Category: commonv1.ErrorCategory_ERROR_CATEGORY_FORBIDDEN,
				Detail: &commonv1.RuntimeErrorDetail_LimitViolation{LimitViolation: &commonv1.LimitViolationDetail{
					Kind: commonv1.LimitKind_LIMIT_KIND_SELECTED_FIELDS, ApprovedMaximum: 512,
				}},
			},
		},
		{
			name: "unspecified invalid component", code: codes.InvalidArgument,
			detail: &commonv1.RuntimeErrorDetail{
				Category: commonv1.ErrorCategory_ERROR_CATEGORY_INVALID_REQUEST,
				Detail:   &commonv1.RuntimeErrorDetail_InvalidRequest{InvalidRequest: &commonv1.InvalidRequestDetail{}},
			},
		},
		{
			name: "unsafe canonical field", code: codes.InvalidArgument,
			detail: &commonv1.RuntimeErrorDetail{
				Category: commonv1.ErrorCategory_ERROR_CATEGORY_INVALID_REQUEST,
				Detail: &commonv1.RuntimeErrorDetail_InvalidRequest{InvalidRequest: &commonv1.InvalidRequestDetail{
					Component:          commonv1.InvalidComponent_INVALID_COMPONENT_SELECTED_ROOT_FIELD,
					CanonicalFieldName: "field with spaces",
				}},
			},
		},
		{
			name: "malformed compatibility range", code: codes.FailedPrecondition,
			detail: &commonv1.RuntimeErrorDetail{
				Category: commonv1.ErrorCategory_ERROR_CATEGORY_INCOMPATIBLE,
				Detail: &commonv1.RuntimeErrorDetail_Compatibility{Compatibility: &commonv1.CompatibilityDetail{
					RequiredCapability: "unsafe capability", SupportedProtocolMajor: 2,
					SupportedProtocolMinorMin: 2, SupportedProtocolMinorMax: 1,
				}},
			},
		},
		{
			name: "unspecified stale axis", code: codes.FailedPrecondition,
			detail: &commonv1.RuntimeErrorDetail{
				Category: commonv1.ErrorCategory_ERROR_CATEGORY_STALE_STATE,
				Detail:   &commonv1.RuntimeErrorDetail_StaleState{StaleState: &commonv1.StaleStateDetail{}},
			},
		},
		{
			name: "unsafe internal request id", code: codes.Internal,
			detail: &commonv1.RuntimeErrorDetail{
				Category: commonv1.ErrorCategory_ERROR_CATEGORY_INTERNAL_FAILURE,
				Detail: &commonv1.RuntimeErrorDetail_InternalFailure{InternalFailure: &commonv1.InternalFailureDetail{
					RequestId: "request id with spaces",
				}},
			},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			withDetail, err := status.New(tc.code, "untrusted peer detail").WithDetails(tc.detail)
			if err != nil {
				t.Fatal(err)
			}
			var failure *semantic.Failure
			if decoded := FromStatusError(withDetail.Err()); !errors.As(decoded, &failure) || failure.Category != semantic.FailureInternal {
				t.Fatalf("malformed detail projection = %#v", decoded)
			}
		})
	}
}

func TestWellFormedForeignCompatibilityDetailRemainsIncompatible(t *testing.T) {
	detail := &commonv1.RuntimeErrorDetail{
		Category: commonv1.ErrorCategory_ERROR_CATEGORY_INCOMPATIBLE,
		Detail: &commonv1.RuntimeErrorDetail_Compatibility{Compatibility: &commonv1.CompatibilityDetail{
			RequiredCapability:     "runtime.records.read-selected-root.v2",
			SupportedProtocolMajor: 2, SupportedProtocolMinorMin: 0, SupportedProtocolMinorMax: 1,
		}},
	}
	withDetail, err := status.New(codes.FailedPrecondition, "incompatible peer").WithDetails(detail)
	if err != nil {
		t.Fatal(err)
	}
	var failure *semantic.Failure
	decoded := FromStatusError(withDetail.Err())
	if !errors.As(decoded, &failure) || failure.Category != semantic.FailureIncompatible ||
		failure.RequiredCapability != "runtime.records.read-selected-root.v2" || failure.SupportedMajor != 2 {
		t.Fatalf("foreign compatibility projection = %#v", decoded)
	}
}

func validWireResponse() *runtimev1.ReadSelectedRootResponse {
	return &runtimev1.ReadSelectedRootResponse{
		RecordId: "I-1",
		Receipts: &commonv1.ConsistencyReceipts{
			ProtocolVersion:   &commonv1.ProtocolVersion{Major: 1},
			CapabilityVersion: &commonv1.CapabilityVersion{Name: semantic.CapabilityName, Major: 1},
		},
	}
}
