package applicationnavigationv1

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func TestSaveDocumentContract(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	good := SaveRequest{ApplicationID: "app", ExpectedRevision: 7, IdempotencyKey: "save-1", Items: []Item{{ID: "start", Label: "Renamed", Target: Target{Page: &PageTarget{ID: "start"}}}}}
	if err := ValidateSaveRequest(ctx, good); err != nil {
		t.Fatal(err)
	}
	tests := map[string]func(*SaveRequest){
		"absent items":       func(r *SaveRequest) { r.Items = nil },
		"duplicate identity": func(r *SaveRequest) { r.Items = append(r.Items, r.Items[0]) },
		"missing revision":   func(r *SaveRequest) { r.ExpectedRevision = 0 },
		"two targets":        func(r *SaveRequest) { u := "https://example.com"; r.Items[0].Target.URL = &u },
		"missing page identity": func(r *SaveRequest) {
			r.Items[0].Target = Target{Page: &PageTarget{}}
		},
		"executable url": func(r *SaveRequest) { u := "javascript:alert(1)"; r.Items[0].Target = Target{URL: &u} },
	}
	for name, change := range tests {
		t.Run(name, func(t *testing.T) {
			r := good
			r.Items = append([]Item{}, good.Items...)
			change(&r)
			if ValidateSaveRequest(ctx, r) == nil {
				t.Fatal("invalid mutation accepted")
			}
		})
	}
	good.Items = []Item{}
	if err := ValidateSaveRequest(ctx, good); err != nil {
		t.Fatalf("explicit empty: %v", err)
	}
	receipts := common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}
	response := SaveResponse{Document: Document{ApplicationID: "app", Revision: 8, Items: []Item{}}, Changed: true, Receipts: receipts}
	if err := ValidateSaveResponse(good, response); err != nil {
		t.Fatal(err)
	}
	response.Document.Revision = 9
	if ValidateSaveResponse(good, response) == nil {
		t.Fatal("uncorrelated revision accepted")
	}
}

func TestNavigationResponseRejectsCatalogBinding(t *testing.T) {
	request := ReadRequest{ApplicationID: "app"}
	response := ReadResponse{Document: Document{ApplicationID: "app", Revision: 1, Items: []Item{}}, Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName, CatalogRevision: 1}}
	if ValidateReadResponse(request, response) == nil {
		t.Fatal("current navigation claimed a Catalog publication")
	}
}

func TestResolveRejectsAggregateOversizeBeforeAllocatingPerItem(t *testing.T) {
	url := "https://example.invalid/" + strings.Repeat("x", MaxURLBytes-len("https://example.invalid/"))
	items := make([]Item, MaxItems)
	for i := range items {
		items[i] = Item{ID: fmt.Sprintf("item-%d", i), Label: "Link", Target: Target{URL: &url}}
	}
	request := ResolveRequest{ApplicationIDs: make([]string, MaxApplications)}
	response := ResolveResponse{Documents: make([]Document, MaxApplications), Receipts: common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}}
	for i := range response.Documents {
		id := fmt.Sprintf("app-%d", i)
		request.ApplicationIDs[i] = id
		response.Documents[i] = Document{ApplicationID: id, Revision: 1, Items: items}
	}
	// Each document and item is valid. The shared input needs little memory, but
	// serializing it as a whole would allocate over 128 MiB before rejection.
	if err := ValidateItems(items); err != nil {
		t.Fatal(err)
	}
	allocations := testing.AllocsPerRun(1, func() {
		if ValidateResolveResponse(request, response) == nil {
			t.Fatal("oversized response accepted")
		}
	})
	// The preflight should return with constant small overhead, rather than
	// allocating a URL parse and duplicate-ID map for all 65,536 items.
	if allocations > 64 {
		t.Fatalf("oversized response traversed/allocated item payloads: %.0f allocations", allocations)
	}
}

func TestNavigationRejectsUnicodeFormatCharacters(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	for _, format := range []string{"\u200b", "\u202e", "\u2066", "\ufeff"} {
		bad := "visible" + format + "text"
		for _, mutate := range []func(*SaveRequest){
			func(r *SaveRequest) { r.ApplicationID = bad },
			func(r *SaveRequest) { r.IdempotencyKey = bad },
			func(r *SaveRequest) { r.Items[0].ID = bad },
			func(r *SaveRequest) { r.Items[0].Label = bad },
			func(r *SaveRequest) { r.Items[0].Icon = bad },
			func(r *SaveRequest) {
				r.Items[0].Target = Target{Resource: &ResourceTarget{ResourceType: "Thing", ResourceID: bad}}
			},
			func(r *SaveRequest) { r.Items[0].Target = Target{Page: &PageTarget{ID: bad}} },
			func(r *SaveRequest) {
				value := "https://example.invalid/" + bad
				r.Items[0].Target = Target{URL: &value}
			},
		} {
			request := SaveRequest{ApplicationID: "app", ExpectedRevision: 1, IdempotencyKey: "save", Items: []Item{{ID: "item", Label: "Label", Target: Target{Page: &PageTarget{ID: "page"}}}}}
			mutate(&request)
			if ValidateSaveRequest(ctx, request) == nil {
				t.Fatalf("format character %q accepted", format)
			}
		}
	}
	if !ValidIdentifier("应用") || !validText("مرحبا 世界", MaxTextBytes, false) {
		t.Fatal("ordinary Unicode text rejected")
	}
}

func TestNavigationUsesExactCaseSensitiveIdentity(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	request := ResolveRequest{ApplicationIDs: []string{"App", "app"}}
	if err := ValidateResolveRequest(ctx, request); err != nil {
		t.Fatal(err)
	}
	receipts := common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}
	response := ResolveResponse{Documents: []Document{{ApplicationID: "App", Revision: 1, Items: []Item{}}, {ApplicationID: "app", Revision: 1, Items: []Item{}}}, Receipts: receipts}
	if err := ValidateResolveResponse(request, response); err != nil {
		t.Fatal(err)
	}
	if ValidateResolveResponse(ResolveRequest{ApplicationIDs: []string{"App"}}, ResolveResponse{Documents: response.Documents[1:], Receipts: receipts}) == nil {
		t.Fatal("case-mismatched response correlated")
	}
	if ValidateResolveRequest(ctx, ResolveRequest{ApplicationIDs: []string{"App", "App"}}) == nil {
		t.Fatal("duplicate exact identity accepted")
	}
	items := []Item{{ID: "Menu", Label: "One", Target: Target{Page: &PageTarget{ID: "Page"}}}, {ID: "menu", Label: "Two", Target: Target{Page: &PageTarget{ID: "page"}}}}
	if err := ValidateItems(items); err != nil {
		t.Fatal(err)
	}
}

func TestNavigationResponseFailureCategoriesAcrossOperations(t *testing.T) {
	receipts := common.Receipts{ProtocolVersion: "1.0", CapabilityVersion: CapabilityName}
	empty := Document{ApplicationID: "app", Revision: 1, Items: []Item{}}
	validators := map[string]func(Document, common.Receipts) error{
		"read": func(d Document, r common.Receipts) error {
			return ValidateReadResponse(ReadRequest{ApplicationID: "app"}, ReadResponse{Document: d, Receipts: r})
		},
		"resolve": func(d Document, r common.Receipts) error {
			return ValidateResolveResponse(ResolveRequest{ApplicationIDs: []string{"app"}}, ResolveResponse{Documents: []Document{d}, Receipts: r})
		},
		"save": func(d Document, r common.Receipts) error {
			return ValidateSaveResponse(SaveRequest{ApplicationID: "app", ExpectedRevision: 1, IdempotencyKey: "save", Items: []Item{}}, SaveResponse{Document: d, Receipts: r})
		},
	}
	for name, validate := range validators {
		t.Run(name, func(t *testing.T) {
			for _, tc := range []struct {
				name     string
				change   func(*Document, *common.Receipts)
				category common.FailureCategory
			}{
				{"protocol", func(_ *Document, r *common.Receipts) { r.ProtocolVersion = "2.0" }, common.FailureIncompatible},
				{"capability", func(_ *Document, r *common.Receipts) { r.CapabilityVersion = "runtime.other.v1" }, common.FailureIncompatible},
				{"catalog", func(_ *Document, r *common.Receipts) { r.CatalogRevision = 1 }, common.FailureInternal},
				{"schema", func(_ *Document, r *common.Receipts) { r.SchemaGeneration = 1 }, common.FailureInternal},
				{"engine", func(_ *Document, r *common.Receipts) { r.EngineGeneration = 1 }, common.FailureInternal},
				{"correlation", func(d *Document, _ *common.Receipts) { d.ApplicationID = "other" }, common.FailureInternal},
				{"absent items", func(d *Document, _ *common.Receipts) { d.Items = nil }, common.FailureInternal},
				{"raw budget", func(d *Document, _ *common.Receipts) {
					d.ApplicationID = strings.Repeat("a", MaxSemanticResponseBytes+1)
				}, common.FailureResourceExhausted},
			} {
				t.Run(tc.name, func(t *testing.T) {
					d, r := empty, receipts
					tc.change(&d, &r)
					err := validate(d, r)
					var failure *common.Failure
					if !errors.As(err, &failure) || failure.Category != tc.category {
						t.Fatalf("failure = %#v, want %s", err, tc.category)
					}
					if tc.category == common.FailureIncompatible && (failure.RequiredCapability != CapabilityName || failure.SupportedMajor != common.ProtocolMajor || failure.SupportedMinorMin != common.ProtocolMinor || failure.SupportedMinorMax != common.ProtocolMinor) {
						t.Fatalf("incomplete compatibility details: %#v", failure)
					}
					if tc.category == common.FailureResourceExhausted && (failure.LimitKind != common.LimitSemanticResponse || failure.Maximum != MaxSemanticResponseBytes) {
						t.Fatalf("incomplete limit details: %#v", failure)
					}
				})
			}
		})
	}
	// JSON escaping can cross the encoded ceiling while raw payloads and every
	// item field remain within their individual limits.
	escaped := empty
	escaped.Items = make([]Item, MaxItems)
	target := "https://example.invalid/" + strings.Repeat("<", MaxURLBytes-len("https://example.invalid/"))
	for i := range escaped.Items {
		escaped.Items[i] = Item{ID: fmt.Sprintf("item-%d", i), Label: strings.Repeat("<", MaxTextBytes), Icon: strings.Repeat("<", MaxTextBytes), Target: Target{URL: &target}}
	}
	if err := ValidateItems(escaped.Items); err != nil {
		t.Fatal(err)
	}
	if err := validateResponseRawBudget([]Document{escaped}, receipts); err != nil {
		t.Fatalf("fixture exceeded raw budget: %v", err)
	}
	encoded, err := json.Marshal(ReadResponse{Document: escaped, Receipts: receipts})
	if err != nil || len(encoded) <= MaxSemanticResponseBytes {
		t.Fatalf("fixture did not exceed encoded budget: %d, %v", len(encoded), err)
	}
	for name, validate := range validators {
		err := validate(escaped, receipts)
		var failure *common.Failure
		if !errors.As(err, &failure) || failure.Category != common.FailureResourceExhausted || failure.LimitKind != common.LimitSemanticResponse || failure.Maximum != MaxSemanticResponseBytes {
			t.Fatalf("%s encoded limit failure = %#v", name, err)
		}
	}
}
