package mapping

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

	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/externaleffectv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	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 TestExternalEffectMappingRoundTrip(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	minimum := uint64(7)
	claim := semantic.ClaimRequest{WorkerID: "integration-1", Limit: 1, MinCatalogRevision: &minimum}
	wireClaim, err := ToWireClaimExternalEffectsRequest(ctx, claim)
	if err != nil {
		t.Fatal(err)
	}
	roundClaim, err := FromWireClaimExternalEffectsRequest(ctx, wireClaim)
	if err != nil || !reflect.DeepEqual(roundClaim, claim) {
		t.Fatalf("claim round trip = %#v, %v", roundClaim, err)
	}

	operation := semantic.Operation{
		OperationID: "op-1", Method: "PATCH", RelativePath: "/objects/1",
		Query:   []semantic.QueryParameter{{Name: "archived", Value: "false"}},
		Headers: []semantic.Header{{Name: "content-type", Values: []string{"application/json"}}},
		Body:    []byte(`{"name":"Ada"}`), CredentialRef: "credential-1", EndpointClass: "objects.write",
		RetryPolicy: semantic.RetryPolicy{MaxAttempts: 3, BaseDelayMillis: 10, MultiplierMilli: 2000, MaxDelayMillis: 100, RetryableStatusCodes: []uint32{429, 503}},
	}
	operation.RequestDigest = semantic.ComputeRequestDigest(operation)
	claimResponse := semantic.ClaimResponse{
		Effects:  []semantic.Effect{{EffectID: "effect-1", ClaimToken: "claim-1", LeaseExpiresAtUnixMillis: 1234, Operations: []semantic.Operation{operation}}},
		Receipts: externalEffectMappingReceipts(7),
	}
	wireClaimResponse, err := ToWireClaimExternalEffectsResponse(claim, claimResponse)
	if err != nil {
		t.Fatal(err)
	}
	roundClaimResponse, err := FromWireClaimExternalEffectsResponse(claim, wireClaimResponse)
	if err != nil || !reflect.DeepEqual(roundClaimResponse, claimResponse) {
		t.Fatalf("claim response round trip = %#v, %v", roundClaimResponse, err)
	}

	complete := semantic.CompleteRequest{
		EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: minimum,
		Results: []semantic.OperationResult{{
			OperationID: "op-1", RequestDigest: operation.RequestDigest,
			Outcome: semantic.OutcomeResponseReceived, StatusCode: 200,
			Body: []byte(`{"id":"1"}`),
		}},
	}
	wireComplete, err := ToWireCompleteExternalEffectRequest(ctx, complete)
	if err != nil {
		t.Fatal(err)
	}
	roundComplete, err := FromWireCompleteExternalEffectRequest(ctx, wireComplete)
	if err != nil || !reflect.DeepEqual(roundComplete, complete) {
		t.Fatalf("complete round trip = %#v, %v", roundComplete, err)
	}
	completeResponse := semantic.CompleteResponse{EffectID: "effect-1", State: semantic.CompletionFinalized, Receipts: externalEffectMappingReceipts(7)}
	wireCompleteResponse, err := ToWireCompleteExternalEffectResponse(complete, completeResponse)
	if err != nil {
		t.Fatal(err)
	}
	roundCompleteResponse, err := FromWireCompleteExternalEffectResponse(complete, wireCompleteResponse)
	if err != nil || !reflect.DeepEqual(roundCompleteResponse, completeResponse) {
		t.Fatalf("complete response round trip = %#v, %v", roundCompleteResponse, err)
	}
}

func TestExternalEffectMappingFailsClosed(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if _, err := FromWireClaimExternalEffectsRequest(ctx, nil); err == nil {
		t.Fatal("accepted nil claim request")
	}
	if _, err := FromWireClaimExternalEffectsResponse(semantic.ClaimRequest{WorkerID: "worker", Limit: 1}, nil); err == nil {
		t.Fatal("accepted nil claim response")
	}
	if _, err := FromWireCompleteExternalEffectRequest(ctx, &runtimev1.CompleteExternalEffectRequest{
		EffectId: "effect-1", ClaimToken: "claim-1",
		Results: []*runtimev1.ExternalEffectOperationResult{{Outcome: runtimev1.ExternalEffectOperationOutcome_EXTERNAL_EFFECT_OPERATION_OUTCOME_UNSPECIFIED}},
	}); err == nil {
		t.Fatal("accepted unknown completion outcome")
	}
	if _, err := FromWireCompleteExternalEffectResponse(semantic.CompleteRequest{}, &runtimev1.CompleteExternalEffectResponse{
		State:    runtimev1.ExternalEffectCompletionState_EXTERNAL_EFFECT_COMPLETION_STATE_UNSPECIFIED,
		Receipts: toWireReceipts(externalEffectMappingReceipts(1), platformCapability(semantic.CapabilityName)),
	}); err == nil {
		t.Fatal("accepted unknown completion state")
	}
}

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

	claim := semantic.ClaimRequest{WorkerID: "integration-1", Limit: 1}
	operations := make([]semantic.Operation, 9)
	for index := range operations {
		bodyBytes := semantic.MaxRequestBodyBytes
		if index == len(operations)-1 {
			bodyBytes = 200 * 1024
		}
		operations[index] = semantic.Operation{
			OperationID: "op-" + string(rune('a'+index)), Method: "POST",
			RelativePath: "/objects", Body: make([]byte, bodyBytes),
			CredentialRef: "credential-1", EndpointClass: "objects.write",
			RetryPolicy: semantic.RetryPolicy{MaxAttempts: 1, MultiplierMilli: 1000},
		}
		operations[index].RequestDigest = semantic.ComputeRequestDigest(operations[index])
	}
	claimResponse := semantic.ClaimResponse{
		Effects: []semantic.Effect{{
			EffectID: "effect-1", ClaimToken: "claim-1",
			LeaseExpiresAtUnixMillis: 1, Operations: operations,
		}},
		Receipts: externalEffectMappingReceipts(1),
	}
	claimSemantic, err := json.Marshal(claimResponse)
	if err != nil {
		t.Fatalf("encode maximum-body claim semantics: %v", err)
	}
	if size := len(claimSemantic); size < semantic.MaxSemanticResponseBytes-(256*1024) {
		t.Fatalf("claim response semantic size=%d does not exercise the 3 MiB boundary", size)
	}
	wireClaim, err := ToWireClaimExternalEffectsResponse(claim, claimResponse)
	if err != nil {
		t.Fatalf("map maximum-body claim response: %v", err)
	}
	if size := proto.Size(wireClaim); size > envelope.MaxResponseWireBytes {
		t.Fatalf("claim response wire size=%d cap=%d", size, envelope.MaxResponseWireBytes)
	}

	complete := semantic.CompleteRequest{
		EffectID: "effect-1", ClaimToken: "claim-1", CatalogRevision: 1,
		Results: []semantic.OperationResult{
			{
				OperationID:   operations[0].OperationID,
				RequestDigest: operations[0].RequestDigest,
				Outcome:       semantic.OutcomeResponseReceived, StatusCode: 200,
				Body: make([]byte, semantic.MaxResponseBodyBytes),
			},
			{
				OperationID:   operations[1].OperationID,
				RequestDigest: operations[1].RequestDigest,
				Outcome:       semantic.OutcomeResponseReceived, StatusCode: 200,
				Body: make([]byte, 500*1024),
			},
		},
	}
	completeSemantic, err := json.Marshal(complete)
	if err != nil {
		t.Fatalf("encode maximum-body completion semantics: %v", err)
	}
	if size := len(completeSemantic); size < semantic.MaxSemanticRequestBytes-(32*1024) {
		t.Fatalf("completion request semantic size=%d does not exercise the 2 MiB boundary", size)
	}
	wireComplete, err := ToWireCompleteExternalEffectRequest(ctx, complete)
	if err != nil {
		t.Fatalf("map maximum-body completion request: %v", err)
	}
	if size := proto.Size(wireComplete); size > envelope.MaxRequestWireBytes {
		t.Fatalf("completion request wire size=%d cap=%d", size, envelope.MaxRequestWireBytes)
	}
}

func TestExternalEffectStatusDisclosureIsCapabilityBound(t *testing.T) {
	source := &semantic.Failure{Category: commonsemantic.FailureMissing}
	decoded := FromPlatformStatusError(ToPlatformStatusError(source, semantic.CapabilityName), semantic.CapabilityName)
	var failure *commonsemantic.Failure
	if !errors.As(decoded, &failure) || failure.Category != commonsemantic.FailureMissing {
		t.Fatalf("decoded failure = %#v", decoded)
	}
	invalid := &semantic.Failure{Category: commonsemantic.FailureInvalidRequest, InvalidComponent: commonsemantic.InvalidComponentObjectID}
	if got := ToPlatformStatusError(invalid, semantic.CapabilityName); status.Code(got) != codes.Internal {
		t.Fatalf("unsafe detail status = %v", got)
	}
}

func externalEffectMappingReceipts(revision uint64) semantic.Receipts {
	return semantic.Receipts{
		ProtocolVersion: "1.0", CatalogRevision: revision, SchemaGeneration: 2,
		EngineGeneration: 3, SecurityEpoch: 4, CapabilityVersion: semantic.CapabilityName,
	}
}
