package mapping

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

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/relatedcountsv1"
	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 TestRelatedCountsWireRoundTripPreservesParentAuthorizationAndZero(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := semantic.Request{Counts: []semantic.CountSpec{
		{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId"},
		{ParentRecordID: "P-1", ChildObjectID: "Comment", RelationshipField: "OwnerKey", MatchValue: "opaque-parent-key"},
	}}
	wireRequest, err := ToWireCountRelatedRecordsRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	roundRequest, err := FromWireCountRelatedRecordsRequest(ctx, wireRequest)
	if err != nil || !reflect.DeepEqual(roundRequest, request) {
		t.Fatalf("request round trip = %#v, %v", roundRequest, err)
	}
	response := semantic.Response{Results: []semantic.Result{
		{Index: 0, Outcome: semantic.CountOutcomeCounted, Count: 0},
		{Index: 1, Outcome: semantic.CountOutcomeNotAvailable},
	}, Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}}
	wireResponse, err := ToWireCountRelatedRecordsResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	roundResponse, err := FromWireCountRelatedRecordsResponse(request, wireResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("response round trip = %#v, %v", roundResponse, err)
	}
}

func TestRelatedCountsWireRejectsMalformedCorrelation(t *testing.T) {
	request := semantic.Request{Counts: []semantic.CountSpec{{ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId"}}}
	receipts := &commonv1.ConsistencyReceipts{ProtocolVersion: &commonv1.ProtocolVersion{Major: 1}, CapabilityVersion: &commonv1.CapabilityVersion{Name: semantic.CapabilityName, Major: 1}}
	for name, response := range map[string]*runtimev1.CountRelatedRecordsResponse{
		"unspecified outcome": {Results: []*runtimev1.RelatedRecordCountResult{{Index: 0}}, Receipts: receipts},
		"wrong index":         {Results: []*runtimev1.RelatedRecordCountResult{{Index: 1, Outcome: runtimev1.RelatedRecordCountOutcome_RELATED_RECORD_COUNT_OUTCOME_COUNTED}}, Receipts: receipts},
		"opaque with count":   {Results: []*runtimev1.RelatedRecordCountResult{{Index: 0, Outcome: runtimev1.RelatedRecordCountOutcome_RELATED_RECORD_COUNT_OUTCOME_NOT_AVAILABLE, Count: 1}}, Receipts: receipts},
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := FromWireCountRelatedRecordsResponse(request, response); err == nil {
				t.Fatal("expected rejection")
			}
		})
	}
}

func TestRelatedCountsWireIgnoresUnknownOptionalFieldsAndBoundsDecode(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := semantic.Request{Counts: []semantic.CountSpec{{
		ParentRecordID: "P-1", ChildObjectID: "WorkItem", RelationshipField: "ParentId",
	}}}
	wireRequest, err := ToWireCountRelatedRecordsRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	encoded, err := proto.Marshal(wireRequest)
	if err != nil {
		t.Fatal(err)
	}
	encoded = protowire.AppendTag(encoded, 100, protowire.BytesType)
	encoded = protowire.AppendString(encoded, "future-optional")
	decoded := &runtimev1.CountRelatedRecordsRequest{}
	if err := proto.Unmarshal(encoded, decoded); err != nil {
		t.Fatal(err)
	}
	round, err := FromWireCountRelatedRecordsRequest(ctx, decoded)
	if err != nil || !reflect.DeepEqual(round, request) {
		t.Fatalf("unknown optional request field: %#v, %v", round, err)
	}

	tooMany := &runtimev1.CountRelatedRecordsRequest{Counts: make([]*runtimev1.RelatedRecordCountSpec, semantic.MaxCounts+1)}
	if _, err := FromWireCountRelatedRecordsRequest(ctx, tooMany); err == nil {
		t.Fatal("one-over count batch was accepted")
	}
	if _, err := FromWireCountRelatedRecordsRequest(ctx, &runtimev1.CountRelatedRecordsRequest{Counts: []*runtimev1.RelatedRecordCountSpec{nil}}); err == nil {
		t.Fatal("nil count item was silently omitted")
	}

	response := semantic.Response{
		Results:  []semantic.Result{{Index: 0, Outcome: semantic.CountOutcomeCounted}},
		Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName},
	}
	wireResponse, err := ToWireCountRelatedRecordsResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	responseBytes, err := proto.Marshal(wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	responseBytes = protowire.AppendTag(responseBytes, 100, protowire.VarintType)
	responseBytes = protowire.AppendVarint(responseBytes, 1)
	decodedResponse := &runtimev1.CountRelatedRecordsResponse{}
	if err := proto.Unmarshal(responseBytes, decodedResponse); err != nil {
		t.Fatal(err)
	}
	roundResponse, err := FromWireCountRelatedRecordsResponse(request, decodedResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("unknown optional response field: %#v, %v", roundResponse, err)
	}
	decodedResponse.Results[0].Outcome = runtimev1.RelatedRecordCountOutcome(999)
	if _, err := FromWireCountRelatedRecordsResponse(request, decodedResponse); err == nil {
		t.Fatal("unknown outcome enum was accepted")
	}
}

func TestRelatedCountsStatusOwnsRelationshipFieldAndRejectsDisclosure(t *testing.T) {
	wireErr := ToRelatedCountStatusError(&semantic.Failure{
		Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentRelationshipField,
	})
	if status.Code(wireErr) != codes.InvalidArgument {
		t.Fatalf("status = %s, want INVALID_ARGUMENT", 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_RELATIONSHIP_FIELD {
		t.Fatalf("related-count invalid component = %#v", detail)
	}
	var round *semantic.Failure
	if err := FromRelatedCountStatusError(wireErr); !errors.As(err, &round) || round.InvalidComponent != semantic.InvalidComponentRelationshipField {
		t.Fatalf("round status = %#v", err)
	}

	for _, unsafe := range []*semantic.Failure{
		{Category: semantic.FailureCategory("MISSING")},
		{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponent("SELECTED_ROOT_FIELD")},
	} {
		if got := status.Code(ToRelatedCountStatusError(unsafe)); got != codes.Internal {
			t.Fatalf("unsafe failure %#v encoded as %s, want INTERNAL", unsafe, got)
		}
	}
}
