package mapping

import (
	"context"
	"encoding/json"
	"errors"
	"google.golang.org/protobuf/proto"

	"fmt"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/envelope"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"reflect"
	"strings"
	"testing"
	"time"
)

func TestNavigationRoundTripPreservesTargetsAndEmpty(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	u := "https://example.com/help?a=1"
	cases := [][]semantic.Item{{{ID: "page", Label: "Renamed page", Icon: "House", Target: semantic.Target{Page: &semantic.PageTarget{ID: "landing"}}}, {ID: "link", Label: "Help", Target: semantic.Target{URL: &u}}, {ID: "resource", Label: "Overview", Target: semantic.Target{Resource: &semantic.ResourceTarget{ResourceType: "Panel", ResourceID: "panel-123"}}}}, {}}
	for _, items := range cases {
		request := semantic.SaveRequest{ApplicationID: "app", ExpectedRevision: 4, IdempotencyKey: "save-1", Items: items}
		wire, err := ToWireNavigationSaveRequest(ctx, request)
		if err != nil {
			t.Fatal(err)
		}
		got, err := FromWireNavigationSaveRequest(ctx, wire)
		if err != nil || !reflect.DeepEqual(got, request) {
			t.Fatalf("request loss: %+v %v", got, err)
		}
		response := semantic.SaveResponse{Document: semantic.Document{ApplicationID: "app", Revision: 5, Items: items}, Changed: true, Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}}
		reply, err := ToWireNavigationSaveResponse(request, response)
		if err != nil {
			t.Fatal(err)
		}
		result, err := FromWireNavigationSaveResponse(request, reply)
		if err != nil || !reflect.DeepEqual(result, response) {
			t.Fatalf("response loss: %+v %v", result, err)
		}
	}
	if _, err := FromWireNavigationSaveRequest(ctx, &runtimev1.ApplicationNavigationServiceSaveRequest{ApplicationId: "app", ExpectedRevision: 4, IdempotencyKey: "save-1"}); err == nil {
		t.Fatal("absent items accepted")
	}
}

func TestNavigationWireRejectsAggregateOversizeBeforeMapping(t *testing.T) {
	url := "https://example.invalid/" + strings.Repeat("x", semantic.MaxURLBytes-len("https://example.invalid/"))
	items := &runtimev1.ApplicationNavigationItems{Values: make([]*runtimev1.ApplicationNavigationItem, semantic.MaxItems)}
	for i := range items.Values {
		items.Values[i] = &runtimev1.ApplicationNavigationItem{Id: fmt.Sprintf("item-%d", i), Label: "Link", Target: &runtimev1.ApplicationNavigationItem_Url{Url: url}}
	}
	request := semantic.ResolveRequest{ApplicationIDs: make([]string, semantic.MaxApplications)}
	response := &runtimev1.ApplicationNavigationServiceResolveResponse{Documents: make([]*runtimev1.ApplicationNavigationDocument, semantic.MaxApplications), Receipts: toWireReceipts(common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}, platformCapability(semantic.CapabilityName))}
	for i := range response.Documents {
		id := fmt.Sprintf("app-%d", i)
		request.ApplicationIDs[i] = id
		response.Documents[i] = &runtimev1.ApplicationNavigationDocument{ApplicationId: id, Revision: 1, Items: items}
	}
	allocations := testing.AllocsPerRun(1, func() {
		_, err := FromWireNavigationResolveResponse(request, response)
		var failure *common.Failure
		if !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.LimitKind != common.LimitWireFrame || failure.Maximum != envelope.MaxResponseWireBytes {
			t.Fatalf("wire limit failure: %#v", err)
		}
	})
	if allocations > 64 {
		t.Fatalf("oversized response allocated semantic payloads: %.0f allocations", allocations)
	}
}

func TestNavigationFailureDetailsSurviveStatusMapping(t *testing.T) {
	cases := []*common.Failure{
		{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticResponse, Maximum: semantic.MaxSemanticResponseBytes},
		{Category: common.FailureResourceExhausted, LimitKind: common.LimitWireFrame, Maximum: envelope.MaxResponseWireBytes},
		{Category: common.FailureIncompatible, RequiredCapability: semantic.CapabilityName, SupportedMajor: common.ProtocolMajor, SupportedMinorMin: common.ProtocolMinor, SupportedMinorMax: common.ProtocolMinor},
	}
	for _, want := range cases {
		gotErr := FromPlatformStatusError(ToPlatformStatusError(want, semantic.CapabilityName), semantic.CapabilityName)
		var got *common.Failure
		if !errors.As(gotErr, &got) || !reflect.DeepEqual(got, want) {
			t.Fatalf("failure round trip: %#v, want %#v", gotErr, want)
		}
	}
}

// UI app discovery resolves an empty selector when no applications are visible.
// Exercise protobuf serialization, which erases presence on repeated fields.
func TestNavigationResolveEmptyProtobufRoundTrip(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := semantic.ResolveRequest{ApplicationIDs: []string{}}
	wire, err := ToWireNavigationResolveRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	encoded, err := proto.Marshal(wire)
	if err != nil {
		t.Fatal(err)
	}
	var decoded runtimev1.ApplicationNavigationServiceResolveRequest
	if err := proto.Unmarshal(encoded, &decoded); err != nil {
		t.Fatal(err)
	}
	got, err := FromWireNavigationResolveRequest(ctx, &decoded)
	if err != nil || !reflect.DeepEqual(got, request) {
		t.Fatalf("empty selector round trip: %+v %v", got, err)
	}
	response := semantic.ResolveResponse{Documents: []semantic.Document{}, Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}}
	reply, err := ToWireNavigationResolveResponse(got, response)
	if err != nil {
		t.Fatal(err)
	}
	encoded, err = proto.Marshal(reply)
	if err != nil {
		t.Fatal(err)
	}
	var decodedReply runtimev1.ApplicationNavigationServiceResolveResponse
	if err := proto.Unmarshal(encoded, &decodedReply); err != nil {
		t.Fatal(err)
	}
	result, err := FromWireNavigationResolveResponse(request, &decodedReply)
	if err != nil || !reflect.DeepEqual(result, response) {
		t.Fatalf("empty response round trip: %+v %v", result, err)
	}
}

func TestNavigationRejectsSemanticExpansionBeforeMapping(t *testing.T) {
	request := semantic.ResolveRequest{ApplicationIDs: make([]string, semantic.MaxApplications)}
	response := semantic.ResolveResponse{Documents: make([]semantic.Document, semantic.MaxApplications), Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}}
	items := make([]semantic.Item, semantic.MaxItems)
	for i := range items {
		items[i] = semantic.Item{ID: fmt.Sprintf("i%d", i), Label: "I", Target: semantic.Target{Page: &semantic.PageTarget{ID: "p"}}}
	}
	wire := &runtimev1.ApplicationNavigationServiceResolveResponse{Receipts: toWireReceipts(response.Receipts, platformCapability(semantic.CapabilityName))}
	for i := range response.Documents {
		id := fmt.Sprintf("a%d", i)
		request.ApplicationIDs[i] = id
		response.Documents[i] = semantic.Document{ApplicationID: id, Revision: 1, Items: items}
		wire.Documents = append(wire.Documents, navigationDocumentToWire(response.Documents[i]))
	}
	encoded, err := proto.Marshal(wire)
	if err != nil {
		t.Fatal(err)
	}
	var decoded runtimev1.ApplicationNavigationServiceResolveResponse
	if err := proto.Unmarshal(encoded, &decoded); err != nil {
		t.Fatal(err)
	}
	semanticJSON, err := json.Marshal(response)
	if err != nil {
		t.Fatal(err)
	}
	if len(encoded) >= envelope.MaxResponseWireBytes || len(semanticJSON) <= semantic.MaxSemanticResponseBytes {
		t.Fatalf("fixture missed budget gap: protobuf=%d JSON=%d", len(encoded), len(semanticJSON))
	}
	allocations := testing.AllocsPerRun(1, func() {
		_, err := FromWireNavigationResolveResponse(request, &decoded)
		var failure *common.Failure
		if !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.LimitKind != common.LimitSemanticResponse {
			t.Fatalf("semantic limit failure: %#v", err)
		}
	})
	t.Logf("protobuf=%d JSON=%d mapping allocations=%.0f", len(encoded), len(semanticJSON), allocations)
	if allocations > 64 {
		t.Fatalf("semantic expansion allocated item graph before rejection: %.0f", allocations)
	}
	// The same real payload just below the semantic cap must remain usable.
	response.Documents = response.Documents[:220]
	decoded.Documents = decoded.Documents[:220]
	if err := semantic.ValidateResolveResponse(request, response); err != nil {
		t.Fatalf("under-limit fixture: %v", err)
	}
	got, err := FromWireNavigationResolveResponse(request, &decoded)
	if err != nil || !reflect.DeepEqual(got, response) {
		t.Fatalf("under-limit response rejected or changed: %v", err)
	}
}

func TestNavigationSingleDocumentRejectsOversizedContentBeforeMapping(t *testing.T) {
	// A nonconforming peer can fit an oversized semantic string below the wire
	// cap. All response entrypoints must reject before allocating semantic items.
	document := &runtimev1.ApplicationNavigationDocument{ApplicationId: "app", Revision: 1, Items: &runtimev1.ApplicationNavigationItems{Values: []*runtimev1.ApplicationNavigationItem{{Id: "item", Label: strings.Repeat("x", semantic.MaxSemanticResponseBytes+1), Target: &runtimev1.ApplicationNavigationItem_Page{Page: &runtimev1.ApplicationNavigationPageTarget{Id: "p"}}}}}}
	receipts := toWireReceipts(common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: semantic.CapabilityName}, platformCapability(semantic.CapabilityName))
	read := &runtimev1.ApplicationNavigationServiceReadResponse{Document: document, Receipts: receipts}
	save := &runtimev1.ApplicationNavigationServiceSaveResponse{Document: document, Receipts: receipts}
	resolve := &runtimev1.ApplicationNavigationServiceResolveResponse{Documents: []*runtimev1.ApplicationNavigationDocument{document}, Receipts: receipts}
	for name, check := range map[string]func() error{
		"read": func() error {
			_, err := FromWireNavigationReadResponse(semantic.ReadRequest{ApplicationID: "app"}, read)
			return err
		},
		"save": func() error {
			_, err := FromWireNavigationSaveResponse(semantic.SaveRequest{ApplicationID: "app", ExpectedRevision: 1, IdempotencyKey: "save", Items: []semantic.Item{}}, save)
			return err
		},
		"resolve": func() error {
			_, err := FromWireNavigationResolveResponse(semantic.ResolveRequest{ApplicationIDs: []string{"app"}}, resolve)
			return err
		},
	} {
		t.Run(name, func(t *testing.T) {
			allocations := testing.AllocsPerRun(1, func() {
				var failure *common.Failure
				if err := check(); !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.LimitKind != common.LimitSemanticResponse || failure.Maximum != semantic.MaxSemanticResponseBytes {
					t.Fatalf("semantic budget: %#v", err)
				}
			})
			if allocations > 64 {
				t.Fatalf("oversized response allocated graph: %.0f", allocations)
			}
		})
	}
}
