package mapping

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

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

func validAttachmentSourceRequest() semantic.ResolveRequest {
	return semantic.ResolveRequest{
		ParentObjectID: "WorkItem", ParentRecordID: "record-1", ResolverID: "document_publication",
		Sources: []semantic.Source{
			{SourceFieldID: "DocumentLink", SourceValue: "ABCDEF123456", SourceValueOffset: 42},
			{SourceFieldID: "OtherLink", SourceValue: "123456ABCDEF", SourceValueOffset: 9},
		},
	}
}

func validAttachmentSourceResponse() semantic.ResolveResponse {
	return semantic.ResolveResponse{
		Results: []semantic.SourceResult{
			{Index: 0, Outcome: semantic.SourceUnavailable},
			{Index: 1, Outcome: semantic.SourceResolved, FileID: "file-1", FileName: "example.pdf"},
		},
		Receipts: semantic.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName},
	}
}

func TestAttachmentSourceWireRoundTripPreservesFieldProofs(t *testing.T) {
	ctx := attachmentSourceContext(t)
	request := validAttachmentSourceRequest()
	wireRequest, err := ToWireResolveAttachmentSourcesRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	roundRequest, err := FromWireResolveAttachmentSourcesRequest(ctx, wireRequest)
	if err != nil || !reflect.DeepEqual(roundRequest, request) {
		t.Fatalf("request = %#v, %v", roundRequest, err)
	}

	response := validAttachmentSourceResponse()
	wireResponse, err := ToWireResolveAttachmentSourcesResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	roundResponse, err := FromWireResolveAttachmentSourcesResponse(request, wireResponse)
	if err != nil || !reflect.DeepEqual(roundResponse, response) {
		t.Fatalf("response = %#v, %v", roundResponse, err)
	}
}

func TestAttachmentSourceWireRejectsOpenProofShapes(t *testing.T) {
	ctx := attachmentSourceContext(t)
	if _, err := FromWireResolveAttachmentSourcesRequest(ctx, nil); err == nil {
		t.Fatal("nil request accepted")
	}
	if _, err := FromWireResolveAttachmentSourcesRequest(ctx, &runtimev1.ResolveAttachmentSourcesRequest{
		Sources: []*runtimev1.AttachmentSourceProof{nil},
	}); err == nil {
		t.Fatal("nil source proof accepted")
	}
	if _, err := FromWireResolveAttachmentSourcesRequest(ctx, &runtimev1.ResolveAttachmentSourcesRequest{
		Sources: make([]*runtimev1.AttachmentSourceProof, semantic.MaxSources+1),
	}); err == nil {
		t.Fatal("one-over source batch accepted")
	}
	response := validAttachmentSourceResponse()
	response.Results[0].FileID = "must-not-leak"
	if _, err := ToWireResolveAttachmentSourcesResponse(validAttachmentSourceRequest(), response); err == nil {
		t.Fatal("unavailable source leaked file facts")
	}
}

func TestAttachmentSourceWireIgnoresUnknownOptionalFieldsAndRejectsUnknownOutcomes(t *testing.T) {
	ctx := attachmentSourceContext(t)
	request := validAttachmentSourceRequest()
	wireRequest, err := ToWireResolveAttachmentSourcesRequest(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")
	decodedRequest := &runtimev1.ResolveAttachmentSourcesRequest{}
	if err := proto.Unmarshal(encoded, decodedRequest); err != nil {
		t.Fatal(err)
	}
	if round, err := FromWireResolveAttachmentSourcesRequest(ctx, decodedRequest); err != nil || !reflect.DeepEqual(round, request) {
		t.Fatalf("unknown optional request field: %#v, %v", round, err)
	}

	response := validAttachmentSourceResponse()
	wireResponse, err := ToWireResolveAttachmentSourcesResponse(request, response)
	if err != nil {
		t.Fatal(err)
	}
	encoded, err = proto.Marshal(wireResponse)
	if err != nil {
		t.Fatal(err)
	}
	encoded = protowire.AppendTag(encoded, 100, protowire.VarintType)
	encoded = protowire.AppendVarint(encoded, 1)
	decodedResponse := &runtimev1.ResolveAttachmentSourcesResponse{}
	if err := proto.Unmarshal(encoded, decodedResponse); err != nil {
		t.Fatal(err)
	}
	if round, err := FromWireResolveAttachmentSourcesResponse(request, decodedResponse); err != nil || !reflect.DeepEqual(round, response) {
		t.Fatalf("unknown optional response field: %#v, %v", round, err)
	}
	decodedResponse.Results[0].Outcome = runtimev1.AttachmentSourceResolutionOutcome(99)
	if _, err := FromWireResolveAttachmentSourcesResponse(request, decodedResponse); err == nil {
		t.Fatal("unknown source outcome accepted")
	}
}

func TestAttachmentSourceStatusBindsItsOwnCapabilityAndLimits(t *testing.T) {
	for _, category := range []semantic.FailureCategory{semantic.FailureMissing, semantic.FailureForbidden, semantic.FailureUnavailable} {
		wireErr := ToRecordAttachmentSourceStatusError(&semantic.Failure{Category: category})
		var round *semantic.Failure
		if decoded := FromRecordAttachmentSourceStatusError(wireErr); !errors.As(decoded, &round) || round.Category != category {
			t.Fatalf("%s round trip = %#v", category, decoded)
		}
	}
	for _, limit := range []struct {
		kind    semantic.LimitKind
		maximum uint64
	}{
		{semantic.LimitOperationCount, semantic.MaxSources},
		{semantic.LimitSemanticRequest, semantic.MaxSemanticRequestBytes},
		{semantic.LimitSemanticResponse, semantic.MaxSemanticResponseBytes},
	} {
		wireErr := ToRecordAttachmentSourceStatusError(&semantic.Failure{
			Category: semantic.FailureResourceExhausted, LimitKind: limit.kind, Maximum: limit.maximum,
		})
		if status.Code(wireErr) != codes.ResourceExhausted {
			t.Fatalf("%s status = %s", limit.kind, status.Code(wireErr))
		}
		var round *semantic.Failure
		if decoded := FromRecordAttachmentSourceStatusError(wireErr); !errors.As(decoded, &round) ||
			round.Category != semantic.FailureResourceExhausted || round.LimitKind != limit.kind || round.Maximum != limit.maximum {
			t.Fatalf("%s round trip = %#v", limit.kind, decoded)
		}
	}
	wireErr := ToRecordAttachmentSourceStatusError(&semantic.Failure{Category: semantic.FailureInvalidRequest, InvalidComponent: semantic.InvalidComponentFieldValue})
	detail, ok := status.Convert(wireErr).Details()[0].(*commonv1.RuntimeErrorDetail)
	if !ok || detail.GetInvalidRequest().GetComponent() != commonv1.InvalidComponent_INVALID_COMPONENT_FIELD_VALUE {
		t.Fatalf("invalid request detail = %#v", detail)
	}
	wireErr = ToRecordAttachmentSourceStatusError(&semantic.Failure{Category: semantic.FailureIncompatible})
	var incompatible *semantic.Failure
	if decoded := FromRecordAttachmentSourceStatusError(wireErr); !errors.As(decoded, &incompatible) || incompatible.RequiredCapability != semantic.CapabilityName {
		t.Fatalf("incompatible capability = %#v", decoded)
	}
}
