package mapping

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

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	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 TestEvaluateRecordAccessRequestAndResponseRoundTrip(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	object := "WorkItem"
	request := semantic.Request{ObjectID: &object, RecordIDs: []string{"W-2", "W-1", "W-2"}}
	wireRequest, err := ToWireEvaluateRecordAccessRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	roundRequest, err := FromWireEvaluateRecordAccessRequest(ctx, wireRequest)
	if err != nil || !reflect.DeepEqual(roundRequest, request) {
		t.Fatalf("request round trip = %#v, %v", roundRequest, err)
	}

	response := semantic.Response{
		Results: []semantic.Result{
			{RecordID: "W-1", Access: semantic.Access{Read: true, Update: true, Delete: true}},
			{RecordID: "W-2", Access: semantic.Access{Read: true}},
		},
		Receipts: semantic.Receipts{
			ProtocolVersion: "1.0", CatalogRevision: 42, SchemaGeneration: 7,
			EngineGeneration: 9, SecurityEpoch: 11, CapabilityVersion: semantic.CapabilityName,
		},
	}
	wireResponse, err := ToWireEvaluateRecordAccessResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	roundResponse, err := FromWireEvaluateRecordAccessResponse(request, wireResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("response round trip = %#v, %v", roundResponse, err)
	}
}

func TestEvaluateRecordAccessOptionalObjectAndUnknownFieldsRemainCompatible(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	base := &runtimev1.EvaluateRecordAccessRequest{RecordIds: []string{"W-1", "A-1"}}
	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.EvaluateRecordAccessRequest
	if err := proto.Unmarshal(encoded, &decoded); err != nil {
		t.Fatal(err)
	}
	request, err := FromWireEvaluateRecordAccessRequest(ctx, &decoded)
	if err != nil || request.ObjectID != nil {
		t.Fatalf("optional object request = %#v, %v", request, err)
	}

	responseBytes, err := proto.Marshal(validRecordAccessWireResponse())
	if err != nil {
		t.Fatal(err)
	}
	responseBytes = protowire.AppendTag(responseBytes, 100, protowire.BytesType)
	responseBytes = protowire.AppendString(responseBytes, "future-optional")
	var decodedResponse runtimev1.EvaluateRecordAccessResponse
	if err := proto.Unmarshal(responseBytes, &decodedResponse); err != nil {
		t.Fatal(err)
	}
	responseRequest := semantic.Request{RecordIDs: []string{"W-1"}}
	if _, err := FromWireEvaluateRecordAccessResponse(responseRequest, &decodedResponse); err != nil {
		t.Fatalf("unknown response field = %v", err)
	}
}

func TestEvaluateRecordAccessCodecBoundsAndAdversarialResponses(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	object := strings.Repeat("O", semantic.MaxObjectIdentifierBytes)
	ids := make([]string, semantic.MaxRecords)
	for i := range ids {
		ids[i] = strings.Repeat("r", semantic.MaxRecordIdentifierBytes)
	}
	if _, err := ToWireEvaluateRecordAccessRequest(ctx, semantic.Request{ObjectID: &object, RecordIDs: ids}); err != nil {
		t.Fatalf("maximum request = %v", err)
	}
	ids = append(ids, "one-over")
	var failure *semantic.Failure
	if _, err := ToWireEvaluateRecordAccessRequest(ctx, semantic.Request{ObjectID: &object, RecordIDs: ids}); !errors.As(err, &failure) || failure.LimitKind != semantic.LimitRecordCount {
		t.Fatalf("one-over batch = %#v", err)
	}

	request := semantic.Request{RecordIDs: []string{"W-1", "W-2"}}
	for _, response := range []*runtimev1.EvaluateRecordAccessResponse{
		nil,
		{Results: []*runtimev1.RecordAccessResult{nil}, Receipts: validRecordAccessWireResponse().Receipts},
		{Results: []*runtimev1.RecordAccessResult{{RecordId: strings.Repeat("r", semantic.MaxRecordIdentifierBytes+1), Read: true}}, Receipts: validRecordAccessWireResponse().Receipts},
		{Results: []*runtimev1.RecordAccessResult{{RecordId: "OTHER", Read: true}}, Receipts: validRecordAccessWireResponse().Receipts},
		{Results: []*runtimev1.RecordAccessResult{{RecordId: "W-1", Read: false}}, Receipts: validRecordAccessWireResponse().Receipts},
		{Results: []*runtimev1.RecordAccessResult{{RecordId: "W-1", Read: true, Update: true, Locked: true}}, Receipts: validRecordAccessWireResponse().Receipts},
		{Results: []*runtimev1.RecordAccessResult{{RecordId: "W-1", Read: true}, {RecordId: "W-1", Read: true}}, Receipts: validRecordAccessWireResponse().Receipts},
		{Results: []*runtimev1.RecordAccessResult{{RecordId: "W-2", Read: true}, {RecordId: "W-1", Read: true}}, Receipts: validRecordAccessWireResponse().Receipts},
	} {
		if _, err := FromWireEvaluateRecordAccessResponse(request, response); err == nil {
			t.Fatalf("adversarial response accepted: %#v", response)
		}
	}

	whitespaceResponse := &runtimev1.EvaluateRecordAccessResponse{
		Results:  []*runtimev1.RecordAccessResult{{RecordId: "   ", Read: true}},
		Receipts: validRecordAccessWireResponse().Receipts,
	}
	if _, err := FromWireEvaluateRecordAccessResponse(
		semantic.Request{RecordIDs: []string{"   "}}, whitespaceResponse,
	); err == nil {
		t.Fatal("whitespace-only missing identifier produced a wire result")
	}
}

func TestRecordAccessStatusUsesItsOwnCapabilityAndSafeDetails(t *testing.T) {
	receipts := &semantic.Receipts{
		ProtocolVersion: "1.0", CatalogRevision: 12, SchemaGeneration: 3,
		EngineGeneration: 4, SecurityEpoch: 5, CapabilityVersion: semantic.CapabilityName,
	}
	tests := []struct {
		failure *semantic.Failure
		code    codes.Code
	}{
		{&semantic.Failure{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentRecordID}, codes.InvalidArgument},
		{&semantic.Failure{Category: semantic.FailureForbidden}, codes.PermissionDenied},
		{&semantic.Failure{Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxRecords}, codes.ResourceExhausted},
		{&semantic.Failure{Category: semantic.FailureStaleState, StaleAxis: semantic.StaleAxisCatalog, RequiredRevision: 9, ServedRevision: 8}, codes.FailedPrecondition},
		{&semantic.Failure{Category: semantic.FailureUnavailable, RetryAfterMillis: 10}, codes.Unavailable},
		{&semantic.Failure{Category: semantic.FailureInternal, RequestID: "req-safe"}, codes.Internal},
		{&semantic.Failure{Category: semantic.FailureForbidden, Receipts: receipts}, codes.PermissionDenied},
	}
	for _, tc := range tests {
		wireErr := ToRecordAccessStatusError(tc.failure)
		if got := status.Code(wireErr); got != tc.code {
			t.Fatalf("%s status = %s", tc.failure.Category, got)
		}
		var round *semantic.Failure
		if err := FromRecordAccessStatusError(wireErr); !errors.As(err, &round) || round.Category != tc.failure.Category {
			t.Fatalf("%s round = %#v", tc.failure.Category, err)
		}
	}

	wrong := ToStatusError(&semantic.Failure{Category: semantic.FailureForbidden, Receipts: receipts})
	if status.Code(wrong) != codes.Internal {
		t.Fatalf("record-access receipts accepted by selected-root mapper: %v", wrong)
	}

	incompatible := ToRecordAccessStatusError(&semantic.Failure{Category: semantic.FailureIncompatible})
	statusValue := status.Convert(incompatible)
	details := statusValue.Details()
	if len(details) != 1 {
		t.Fatalf("compatibility details = %#v", details)
	}
	detail, ok := details[0].(*commonv1.RuntimeErrorDetail)
	if !ok || detail.GetCompatibility().GetRequiredCapability() != semantic.CapabilityName {
		t.Fatalf("compatibility detail = %#v", detail)
	}

	missing := ToRecordAccessStatusError(&semantic.Failure{Category: semantic.FailureMissing})
	if status.Code(missing) != codes.Internal {
		t.Fatalf("top-level MISSING status = %s, want INTERNAL", status.Code(missing))
	}

	for name, foreign := range map[string]error{
		"selected-root invalid component": ToStatusError(&semantic.Failure{
			Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponent("SELECTED_ROOT_FIELD"),
		}),
		"selected-root limit": ToStatusError(&semantic.Failure{
			Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitKind("SELECTED_FIELDS"), Maximum: 1,
		}),
		"selected-root compatibility": ToStatusError(&semantic.Failure{Category: semantic.FailureIncompatible}),
	} {
		t.Run(name, func(t *testing.T) {
			var failure *semantic.Failure
			decoded := FromRecordAccessStatusError(foreign)
			if !errors.As(decoded, &failure) || failure.Category != semantic.FailureInternal {
				t.Fatalf("foreign detail decoded = %#v, want INTERNAL", decoded)
			}
		})
	}

	invalidOwnLimit := ToRecordAccessStatusError(&semantic.Failure{
		Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitKind("SELECTED_FIELDS"), Maximum: 1,
	})
	if status.Code(invalidOwnLimit) != codes.Internal {
		t.Fatalf("foreign record-access limit encoded as %s, want INTERNAL", status.Code(invalidOwnLimit))
	}
	wrongMaximum := ToRecordAccessStatusError(&semantic.Failure{
		Category: semantic.FailureResourceExhausted, LimitKind: semantic.LimitRecordCount, Maximum: semantic.MaxRecords + 1,
	})
	if status.Code(wrongMaximum) != codes.Internal {
		t.Fatalf("false record-access maximum encoded as %s, want INTERNAL", status.Code(wrongMaximum))
	}
	foreignCanonicalField := ToRecordAccessStatusError(&semantic.Failure{
		Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentRecordID, CanonicalField: "ForeignField",
	})
	if status.Code(foreignCanonicalField) != codes.Internal {
		t.Fatalf("foreign canonical field encoded as %s, want INTERNAL", status.Code(foreignCanonicalField))
	}
}

func TestRecordAccessStatusClassifiesReviewedUndetailedTransportFailures(t *testing.T) {
	for code, want := range map[codes.Code]semantic.FailureCategory{
		codes.Unauthenticated:   semantic.FailureUnauthenticated,
		codes.PermissionDenied:  semantic.FailureForbidden,
		codes.Canceled:          semantic.FailureCanceled,
		codes.DeadlineExceeded:  semantic.FailureDeadlineExceeded,
		codes.ResourceExhausted: semantic.FailureResourceExhausted,
		codes.Unavailable:       semantic.FailureUnavailable,
	} {
		var failure *semantic.Failure
		decoded := FromRecordAccessStatusError(status.Error(code, "untrusted transport text"))
		if !errors.As(decoded, &failure) || failure.Category != want {
			t.Fatalf("%s decoded = %#v, want %s", code, decoded, want)
		}
	}

	// FailedPrecondition is ambiguous between STALE_STATE and INCOMPATIBLE;
	// NotFound is forbidden at the top level. Neither is trusted without a
	// capability-bound detail.
	for _, code := range []codes.Code{codes.FailedPrecondition, codes.NotFound, codes.InvalidArgument} {
		var failure *semantic.Failure
		decoded := FromRecordAccessStatusError(status.Error(code, "untrusted transport text"))
		if !errors.As(decoded, &failure) || failure.Category != semantic.FailureInternal {
			t.Fatalf("%s decoded = %#v, want INTERNAL", code, decoded)
		}
	}
}

func validRecordAccessWireResponse() *runtimev1.EvaluateRecordAccessResponse {
	return &runtimev1.EvaluateRecordAccessResponse{
		Results: []*runtimev1.RecordAccessResult{{RecordId: "W-1", Read: true}},
		Receipts: &commonv1.ConsistencyReceipts{
			ProtocolVersion: &commonv1.ProtocolVersion{Major: 1},
			CapabilityVersion: &commonv1.CapabilityVersion{
				Name: semantic.CapabilityName, Major: 1,
			},
		},
	}
}
