package configwire

import (
	"crypto/sha256"
	"fmt"
	"reflect"
	"strings"
	"testing"
)

func TestEnvelopeMappingRule_AllowsPreTargetLaneUnkeyedLiteralShape(t *testing.T) {
	rule := EnvelopeMappingRule{
		"",
		"",
		nil,
		"",
		"thing.updated",
		true,
		"thing",
		".objectId",
		"",
		"",
		".eventId",
		nil,
	}
	if rule.EventTypeLiteral != "thing.updated" || rule.IdempotencyKeyPath != ".eventId" {
		t.Fatalf("unkeyed EnvelopeMappingRule layout shifted: %+v", rule)
	}
}

func TestDecodeEnvelopeMappingConfig_PreservesOmittedInboundProcessingMode(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[{"name":"objectId","path":".objectId"}]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	if cfg.InboundProcessingMode != "" {
		t.Fatalf("InboundProcessingMode = %q, want omitted empty mode", cfg.InboundProcessingMode)
	}
	if strings.Contains(canonical, `"inboundProcessingMode"`) {
		t.Fatalf("canonical config %q should preserve omitted inboundProcessingMode", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_AcceptsExplicitPayloadOnlyAllowed(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "inboundProcessingMode":" payload_only_allowed ",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[{"name":"objectId","path":".objectId"}]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	if cfg.InboundProcessingMode != InboundProcessingModePayloadOnlyAllowed {
		t.Fatalf("InboundProcessingMode = %q, want %q", cfg.InboundProcessingMode, InboundProcessingModePayloadOnlyAllowed)
	}
	if !strings.Contains(canonical, `"inboundProcessingMode":"payload_only_allowed"`) {
		t.Fatalf("canonical config %q missing normalized inboundProcessingMode", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_AcceptsEventObjectPath(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[{"name":"objectId","path":".objectId"}]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	if cfg.EventObjectPath != "." {
		t.Fatalf("EventObjectPath = %q, want .", cfg.EventObjectPath)
	}
	if cfg.EventArrayPath != "" {
		t.Fatalf("EventArrayPath = %q, want empty", cfg.EventArrayPath)
	}
	if !strings.Contains(canonical, `"eventObjectPath":"."`) {
		t.Fatalf("canonical config %q missing eventObjectPath", canonical)
	}
	if strings.Contains(canonical, `"eventArrayPath"`) {
		t.Fatalf("canonical config %q should not include eventArrayPath", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_AcceptsSourceLaneOverride(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"workflow.action.execute",
    "namespaceFromCtx":true,
    "objectKind":"workflow-actions",
    "sourceIdPath":".object.objectId",
    "sourceLaneObjectKind":" shared-parent ",
    "sourceLaneIdPath":".inputFields.parent_id",
    "idempotencyKeyPath":".callbackId",
    "payloadProjection":[{"name":"parent_id","path":".inputFields.parent_id"}]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	rule := cfg.Rules[0]
	if rule.SourceIDPath != ".object.objectId" {
		t.Fatalf("SourceIDPath = %q, want contact object path", rule.SourceIDPath)
	}
	if rule.SourceLaneObjectKind != "shared-parent" || rule.SourceLaneIDPath != ".inputFields.parent_id" {
		t.Fatalf("source lane = %q/%q, want shared-parent/.inputFields.parent_id", rule.SourceLaneObjectKind, rule.SourceLaneIDPath)
	}
	if !strings.Contains(canonical, `"sourceLaneObjectKind":"shared-parent"`) || !strings.Contains(canonical, `"sourceLaneIdPath":".inputFields.parent_id"`) {
		t.Fatalf("canonical config %q missing source lane fields", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_AcceptsTargetMutationLane(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"workflow.action.execute",
    "namespaceFromCtx":true,
    "objectKind":"workflow-actions",
    "sourceIdPath":".object.objectId",
    "targetMutationLaneObjectKind":" campaign ",
    "targetMutationLaneIdPath":".inputFields.campaign_id",
    "idempotencyKeyPath":".callbackId",
    "payloadProjection":[{"name":"campaign_id","path":".inputFields.campaign_id"}]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	rule := cfg.Rules[0]
	if rule.SourceIDPath != ".object.objectId" {
		t.Fatalf("SourceIDPath = %q, want contact object path", rule.SourceIDPath)
	}
	if rule.SourceLaneObjectKind != "campaign" || rule.SourceLaneIDPath != ".inputFields.campaign_id" {
		t.Fatalf("runtime source lane = %q/%q, want campaign/.inputFields.campaign_id", rule.SourceLaneObjectKind, rule.SourceLaneIDPath)
	}
	if !strings.Contains(canonical, `"targetMutationLaneObjectKind":"campaign"`) || !strings.Contains(canonical, `"targetMutationLaneIdPath":".inputFields.campaign_id"`) {
		t.Fatalf("canonical config %q missing target mutation lane fields", canonical)
	}
	if strings.Contains(canonical, `"sourceLaneObjectKind"`) || strings.Contains(canonical, `"sourceLaneIdPath"`) {
		t.Fatalf("canonical config %q should not emit legacy source lane fields", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsPartialSourceLaneOverride(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"workflow.action.execute",
    "objectKind":"workflow-actions",
    "sourceIdPath":".object.objectId",
    "sourceLaneIdPath":".inputFields.parent_id",
    "idempotencyKeyPath":".callbackId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want partial source lane error")
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsPartialTargetMutationLane(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"workflow.action.execute",
    "objectKind":"workflow-actions",
    "sourceIdPath":".object.objectId",
    "targetMutationLaneIdPath":".inputFields.campaign_id",
    "idempotencyKeyPath":".callbackId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want partial target mutation lane error")
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsLegacyAndTargetMutationLaneTogether(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"workflow.action.execute",
    "objectKind":"workflow-actions",
    "sourceIdPath":".object.objectId",
    "sourceLaneObjectKind":"shared-parent",
    "sourceLaneIdPath":".inputFields.parent_id",
    "targetMutationLaneObjectKind":"campaign",
    "targetMutationLaneIdPath":".inputFields.campaign_id",
    "idempotencyKeyPath":".callbackId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want dual lane config error")
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsBothEventArrayAndObjectPath(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "eventObjectPath":".",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want mutually exclusive event source error")
	}
	if !strings.Contains(err.Error(), "exactly one of EventArrayPath or EventObjectPath") {
		t.Fatalf("error = %q, want envelope source diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsMissingEventArrayAndObjectPath(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want missing event source error")
	}
	if !strings.Contains(err.Error(), "exactly one of EventArrayPath or EventObjectPath") {
		t.Fatalf("error = %q, want envelope source diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_AcceptsMatchAll(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".eventCategory","equals":"entity.propertyChange"},
      {"path":".entityTypeId","equals":"0-2"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[{"name":"objectId","path":".objectId"}]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	if got := len(cfg.Rules[0].MatchAll); got != 2 {
		t.Fatalf("len(MatchAll) = %d, want 2", got)
	}
	if !strings.Contains(canonical, `"matchAll":[{"path":".entityTypeId","equals":"0-2"},{"path":".eventCategory","equals":"entity.propertyChange"}]`) {
		t.Fatalf("canonical config %q missing matchAll", canonical)
	}
	if strings.Contains(canonical, `"matchPath"`) {
		t.Fatalf("canonical config %q should not synthesize legacy matchPath for matchAll", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsEmptyMatchAll(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[],
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want empty matchAll error")
	}
	if !strings.Contains(err.Error(), "matchAll must contain at least one condition") {
		t.Fatalf("error = %q, want empty matchAll diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsNullMatchAll(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":null,
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want null matchAll error")
	}
	if !strings.Contains(err.Error(), "matchAll must contain at least one condition") {
		t.Fatalf("error = %q, want null matchAll diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsUnknownRuleField(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAlll":[{"path":".eventCategory","equals":"entity.propertyChange"}],
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want unknown rule field error")
	}
	if !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("error = %q, want unknown field diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsUnknownMatchAllConditionField(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[{"path":".eventCategory","equals":"entity.propertyChange","equalsTypo":"entity.propertyChange"}],
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want unknown matchAll condition field error")
	}
	if !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("error = %q, want unknown field diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsUnknownPayloadProjectionField(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[{"name":"objectId","path":".objectId","pathTypo":".otherId"}]
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want unknown payloadProjection field error")
	}
	if !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("error = %q, want unknown field diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_CanonicalizesMatchAllConditionOrder(t *testing.T) {
	_, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".eventCategory","equals":"entity.propertyChange"},
      {"path":".entityTypeId","equals":"0-2"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig first: %v", err)
	}
	_, reversedCanonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".entityTypeId","equals":"0-2"},
      {"path":".eventCategory","equals":"entity.propertyChange"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig reversed: %v", err)
	}
	if canonical != reversedCanonical {
		t.Fatalf("canonical configs differ:\nfirst:    %s\nreversed: %s", canonical, reversedCanonical)
	}
	if !strings.Contains(canonical, `"matchAll":[{"path":".entityTypeId","equals":"0-2"},{"path":".eventCategory","equals":"entity.propertyChange"}]`) {
		t.Fatalf("canonical config %q missing sorted matchAll", canonical)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsMatchAllWithLegacySelector(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchPath":".entityTypeId",
    "matchEquals":"0-2",
    "matchAll":[{"path":".eventCategory","equals":"entity.propertyChange"}],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want matchAll/legacy selector error")
	}
	if !strings.Contains(err.Error(), "matchAll is mutually exclusive") {
		t.Fatalf("error = %q, want matchAll mutual exclusion diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsDuplicateMatchAllSelector(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".eventCategory","equals":"entity.propertyChange"},
      {"path":".entityTypeId","equals":"0-2"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  },{
    "matchAll":[
      {"path":".entityTypeId","equals":"0-2"},
      {"path":".eventCategory","equals":"entity.propertyChange"}
    ],
    "eventTypeLiteral":"entity.propertychange.v2",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want duplicate matchAll selector error")
	}
	if !strings.Contains(err.Error(), "same selector") {
		t.Fatalf("error = %q, want duplicate selector diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsContradictoryMatchAllConditionsOnSamePath(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".eventType","equals":"create"},
      {"path":".eventType","equals":"update"}
    ],
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want contradictory matchAll condition error")
	}
	if !strings.Contains(err.Error(), "repeats path") {
		t.Fatalf("error = %q, want repeated path diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsRedundantMatchAllConditionsOnSamePath(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".eventType"},
      {"path":".eventType","equals":"create"}
    ],
    "eventTypeLiteral":"thing.created",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want redundant matchAll condition error")
	}
	if !strings.Contains(err.Error(), "repeats path") {
		t.Fatalf("error = %q, want repeated path diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsEquivalentPathSpellingsInMatchAllConditions(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".items[1]","equals":"create"},
      {"path":".items[01]","equals":"update"}
    ],
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want equivalent matchAll path error")
	}
	if !strings.Contains(err.Error(), "repeats path") {
		t.Fatalf("error = %q, want repeated path diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsLegacySelectorShadowingLaterMatchAllSelector(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchPath":".eventCategory",
    "matchEquals":"entity.propertyChange",
    "eventTypeLiteral":"generic.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"objects",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  },{
    "matchAll":[
      {"path":".eventCategory","equals":"entity.propertyChange"},
      {"path":".entityTypeId","equals":"0-2"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want shadowed matchAll selector error")
	}
	if !strings.Contains(err.Error(), "shadowed by broader selector") {
		t.Fatalf("error = %q, want shadowed selector diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsEquivalentLegacySelectorShadowingLaterMatchAllSelector(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchPath":".items[1]",
    "matchEquals":"entity.propertyChange",
    "eventTypeLiteral":"generic.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"objects",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  },{
    "matchAll":[
      {"path":".items[01]","equals":"entity.propertyChange"},
      {"path":".entityTypeId","equals":"0-2"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want equivalent-path shadowed matchAll selector error")
	}
	if !strings.Contains(err.Error(), "shadowed by broader selector") {
		t.Fatalf("error = %q, want shadowed selector diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsMatchAllSelectorShadowingLaterNarrowerMatchAllSelector(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "matchAll":[
      {"path":".eventCategory","equals":"entity.propertyChange"}
    ],
    "eventTypeLiteral":"generic.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"objects",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  },{
    "matchAll":[
      {"path":".eventCategory","equals":"entity.propertyChange"},
      {"path":".entityTypeId","equals":"0-2"}
    ],
    "eventTypeLiteral":"entity.propertychange",
    "namespaceFromCtx":true,
    "objectKind":"entities",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId"
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want shadowed matchAll selector error")
	}
	if !strings.Contains(err.Error(), "shadowed by broader selector") {
		t.Fatalf("error = %q, want shadowed selector diagnostic", err)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsUnknownInboundProcessingMode(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "inboundProcessingMode":"surprise",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[{"name":"objectId","path":".objectId"}]
  }]
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfig = nil, want invalid inboundProcessingMode error")
	}
	if !strings.Contains(err.Error(), "InboundProcessingMode") {
		t.Fatalf("error %q does not mention InboundProcessingMode", err)
	}
}

func TestDecodeEnvelopeMappingConfig_PreservesPayloadProjectionModes(t *testing.T) {
	cfg, canonical, err := DecodeEnvelopeMappingConfig("acme-connector", []byte(`{
  "eventArrayPath":".events",
  "rules":[{
    "eventTypeLiteral":"thing.merged",
    "namespaceFromCtx":true,
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    "idempotencyKeyPath":".eventId",
    "payloadProjection":[
      {"name":"primaryObjectId","path":".primaryObjectId","mode":" scalar_string "},
      {"name":"mergedObjectIds","path":".mergedObjectIds","mode":"scalar_string_array"}
    ]
  }]
}`))
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	if got := cfg.Rules[0].PayloadProjection[0].Mode; got != "scalar_string" {
		t.Fatalf("primaryObjectId mode = %q, want scalar_string", got)
	}
	if got := cfg.Rules[0].PayloadProjection[1].Mode; got != "scalar_string_array" {
		t.Fatalf("mergedObjectIds mode = %q, want scalar_string_array", got)
	}
	if !strings.Contains(canonical, `"mode":"scalar_string"`) || !strings.Contains(canonical, `"mode":"scalar_string_array"`) {
		t.Fatalf("canonical config %q missing payload projection modes", canonical)
	}
}

func TestDecodeEnvelopeMappingConfigSet_RejectsEndpointModeSplitForSameObjectKind(t *testing.T) {
	_, _, err := DecodeEnvelopeMappingConfigSet("acme-connector", []byte(`{
  "endpointConfigs":{
    "primary":{
      "eventArrayPath":".events",
      "inboundProcessingMode":"payload_only_allowed",
      "rules":[{
        "eventTypeLiteral":"thing.updated",
        "namespaceFromCtx":true,
        "objectKind":"thing",
        "sourceIdPath":".objectId",
        "idempotencyKeyPath":".eventId"
      }]
    },
    "secondary":{
      "eventArrayPath":".events",
      "inboundProcessingMode":"enrichment_required",
      "rules":[{
        "eventTypeLiteral":"thing.updated",
        "namespaceFromCtx":true,
        "objectKind":"thing",
        "sourceIdPath":".objectId",
        "idempotencyKeyPath":".eventId"
      }]
    }
  }
}`))
	if err == nil {
		t.Fatal("DecodeEnvelopeMappingConfigSet = nil, want conflicting endpoint mode error")
	}
	if !strings.Contains(err.Error(), "do not carry endpoint identity") {
		t.Fatalf("error = %q, want endpoint identity diagnostic", err)
	}
}

func validSignatureSchemeConfigJSON(signingTemplate string) []byte {
	return []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"` + signingTemplate + `",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "skewWindow":"5m",
  "secretName":"default"
}`)
}

func TestDecodeSignatureSchemeConfigAcceptsRequestURLToken(t *testing.T) {
	cfg, canonical, err := DecodeSignatureSchemeConfig(validSignatureSchemeConfigJSON("{Method}\\n{RequestURI}\\n{RequestURL}\\n{RawBody}\\n{Timestamp}"))
	if err != nil {
		t.Fatalf("DecodeSignatureSchemeConfig returned error for RequestURL token: %v", err)
	}
	if cfg.SigningTemplate != "{Method}\n{RequestURI}\n{RequestURL}\n{RawBody}\n{Timestamp}" {
		t.Fatalf("SigningTemplate = %q, want RequestURL-preserving template", cfg.SigningTemplate)
	}
	if !strings.Contains(canonical, `{RequestURL}`) {
		t.Fatalf("canonical config = %q, want RequestURL token retained", canonical)
	}
}

func TestDecodeSignatureSchemeConfigAcceptsTimestampFreeSHA256SecretPrefix(t *testing.T) {
	raw := []byte(`{
  "scheme":"sha256-secret-prefix-template",
  "signingTemplate":"{RawBody}",
  "signatureHeader":"X-Legacy-Signature",
  "signatureEncoding":"hex",
  "verifiedIngressReceipt":true,
  "secretName":"webhook",
  "deliveryIdentityCollectionPath":".",
  "deliveryIdentityCollectionKeyPaths":[".eventId"]
}`)
	cfg, canonical, err := DecodeSignatureSchemeConfig(raw)
	if err != nil {
		t.Fatalf("DecodeSignatureSchemeConfig returned error: %v", err)
	}
	if cfg.TimestampHeader != "" || cfg.SkewWindow != 0 {
		t.Fatalf("timestamp-free config gained timestamp requirements: header=%q skew=%v", cfg.TimestampHeader, cfg.SkewWindow)
	}
	if cfg.SignatureHeader != "X-Legacy-Signature" {
		t.Fatalf("SignatureHeader = %q", cfg.SignatureHeader)
	}
	for _, forbidden := range []string{"timestampHeader", "skewWindow"} {
		if strings.Contains(canonical, forbidden) {
			t.Fatalf("canonical config = %q, want %s omitted", canonical, forbidden)
		}
	}
}

func TestDecodeSignatureSchemeConfigRejectsTimestampTokenForSHA256SecretPrefix(t *testing.T) {
	raw := []byte(`{
  "scheme":"sha256-secret-prefix-template",
  "signingTemplate":"{RawBody}{Timestamp}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Legacy-Signature",
  "signatureEncoding":"hex",
  "skewWindow":"5m",
  "secretName":"webhook"
}`)
	_, _, err := DecodeSignatureSchemeConfig(raw)
	if err == nil || !strings.Contains(err.Error(), "does not support {Timestamp}") {
		t.Fatalf("error = %v, want timestamp-token rejection", err)
	}
}

func TestDecodeSignatureSchemeConfigAcceptsReceiptIdentityAndHeaderAllowlist(t *testing.T) {
	raw := []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}.{RawBody}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "verifiedIngressReceipt":true,
  "receiptHeaderAllowlist":["x-receipt-trace"," X-Receipt-Trace ","X-Request-Id"],
  "skewWindow":"5m",
  "secretName":"default",
  "deliveryIdentityBodyPath":".objectId"
}`)
	cfg, canonical, err := DecodeSignatureSchemeConfig(raw)
	if err != nil {
		t.Fatalf("DecodeSignatureSchemeConfig returned error for deliveryIdentityBodyPath: %v", err)
	}
	if cfg.DeliveryIdentityBodyPath != ".objectId" {
		t.Fatalf("DeliveryIdentityBodyPath = %q, want .objectId", cfg.DeliveryIdentityBodyPath)
	}
	wantHeaders := []string{"X-Receipt-Trace", "X-Request-Id"}
	if fmt.Sprint(cfg.ReceiptHeaderAllowlist) != fmt.Sprint(wantHeaders) {
		t.Fatalf("ReceiptHeaderAllowlist = %#v, want %#v", cfg.ReceiptHeaderAllowlist, wantHeaders)
	}
	if !strings.Contains(canonical, `"verifiedIngressReceipt":true`) || !strings.Contains(canonical, `"deliveryIdentityBodyPath":".objectId"`) || !strings.Contains(canonical, `"receiptHeaderAllowlist":["X-Receipt-Trace","X-Request-Id"]`) {
		t.Fatalf("canonical config = %q, want receipt identity/header allowlist retained", canonical)
	}
}

func TestDecodeSignatureSchemeConfigAcceptsReceiptCollectionIdentity(t *testing.T) {
	raw := []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}.{RawBody}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "verifiedIngressReceipt":true,
  "receiptHeaderAllowlist":["X-Receipt-Trace"],
  "skewWindow":"5m",
  "secretName":"default",
  "deliveryIdentityCollectionPath":".items",
  "deliveryIdentityCollectionKeyPaths":[".tenant",".type",".id"]
}`)
	cfg, canonical, err := DecodeSignatureSchemeConfig(raw)
	if err != nil {
		t.Fatalf("DecodeSignatureSchemeConfig returned error for collection identity: %v", err)
	}
	if cfg.DeliveryIdentityBodyPath != "" {
		t.Fatalf("DeliveryIdentityBodyPath = %q, want scalar source empty", cfg.DeliveryIdentityBodyPath)
	}
	if cfg.DeliveryIdentityCollectionPath != ".items" {
		t.Fatalf("DeliveryIdentityCollectionPath = %q, want .items", cfg.DeliveryIdentityCollectionPath)
	}
	wantPaths := []string{".tenant", ".type", ".id"}
	if fmt.Sprint(cfg.DeliveryIdentityCollectionKeyPaths) != fmt.Sprint(wantPaths) {
		t.Fatalf("DeliveryIdentityCollectionKeyPaths = %#v, want %#v", cfg.DeliveryIdentityCollectionKeyPaths, wantPaths)
	}
	for _, required := range []string{
		`"verifiedIngressReceipt":true`,
		`"deliveryIdentityCollectionPath":".items"`,
		`"deliveryIdentityCollectionKeyPaths":[".tenant",".type",".id"]`,
		`"receiptHeaderAllowlist":["X-Receipt-Trace"]`,
	} {
		if !strings.Contains(canonical, required) {
			t.Fatalf("canonical config = %q, want %s retained", canonical, required)
		}
	}
}

func TestDecodeSignatureSchemeConfigIgnoresLegacyReceiptFieldsWithoutOptIn(t *testing.T) {
	raw := []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "receiptHeaderAllowlist":["X-Receipt-Trace"],
  "skewWindow":"5m",
  "secretName":"default",
  "deliveryIdentityBodyPath":".objectId"
}`)
	cfg, canonical, err := DecodeSignatureSchemeConfig(raw)
	if err != nil {
		t.Fatalf("DecodeSignatureSchemeConfig returned error for legacy receipt fields: %v", err)
	}
	if cfg.DeliveryIdentityBodyPath != "" || cfg.DeliveryIdentityCollectionPath != "" || len(cfg.DeliveryIdentityCollectionKeyPaths) != 0 || len(cfg.ReceiptHeaderAllowlist) != 0 {
		t.Fatalf("legacy receipt fields became runtime-active: scalar=%q collection=%q keys=%#v headers=%#v",
			cfg.DeliveryIdentityBodyPath, cfg.DeliveryIdentityCollectionPath, cfg.DeliveryIdentityCollectionKeyPaths, cfg.ReceiptHeaderAllowlist)
	}
	for _, forbidden := range []string{"deliveryIdentityBodyPath", "deliveryIdentityCollectionPath", "deliveryIdentityCollectionKeyPaths", "receiptHeaderAllowlist", "verifiedIngressReceipt"} {
		if strings.Contains(canonical, forbidden) {
			t.Fatalf("canonical config = %q, want legacy %s omitted without explicit receipt opt-in", canonical, forbidden)
		}
	}
}

func TestDecodeSignatureSchemeConfigRejectsUnsignedReceiptIdentityPath(t *testing.T) {
	raw := []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "verifiedIngressReceipt":true,
  "skewWindow":"5m",
  "secretName":"default",
  "deliveryIdentityBodyPath":".objectId"
}`)
	_, _, err := DecodeSignatureSchemeConfig(raw)
	if err == nil {
		t.Fatal("DecodeSignatureSchemeConfig err = nil, want unsigned deliveryIdentityBodyPath rejection")
	}
	if !strings.Contains(err.Error(), "delivery identity source is configured but SigningTemplate does not include {RawBody}") {
		t.Fatalf("err = %q, want unsigned identity diagnostic", err)
	}
}

func TestDecodeSignatureSchemeConfigRejectsAmbiguousReceiptIdentitySources(t *testing.T) {
	raw := []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}.{RawBody}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "verifiedIngressReceipt":true,
  "skewWindow":"5m",
  "secretName":"default",
  "deliveryIdentityBodyPath":".objectId",
  "deliveryIdentityCollectionPath":".items",
  "deliveryIdentityCollectionKeyPaths":[".id"]
}`)
	_, _, err := DecodeSignatureSchemeConfig(raw)
	if err == nil {
		t.Fatal("DecodeSignatureSchemeConfig err = nil, want ambiguous identity source rejection")
	}
	if !strings.Contains(err.Error(), "exactly one delivery identity source is required") {
		t.Fatalf("err = %q, want exactly-one identity diagnostic", err)
	}
}

func TestDecodeSignatureSchemeConfigRejectsIncompleteReceiptCollectionIdentity(t *testing.T) {
	raw := []byte(`{
  "scheme":"hmac-sha256-template",
  "signingTemplate":"{Timestamp}.{RawBody}",
  "timestampHeader":"X-Test-Timestamp",
  "signatureHeader":"X-Test-Signature",
  "signatureEncoding":"hex",
  "verifiedIngressReceipt":true,
  "skewWindow":"5m",
  "secretName":"default",
  "deliveryIdentityCollectionPath":".items"
}`)
	_, _, err := DecodeSignatureSchemeConfig(raw)
	if err == nil {
		t.Fatal("DecodeSignatureSchemeConfig err = nil, want missing collection key paths rejection")
	}
	if !strings.Contains(err.Error(), "DeliveryIdentityCollectionKeyPaths is required") {
		t.Fatalf("err = %q, want collection key paths diagnostic", err)
	}
}

func TestValidateCapabilityConfigAcceptsEndpointRequestURLToken(t *testing.T) {
	raw := []byte(`{
  "endpointConfigs":{
    "primary":{
      "scheme":"hmac-sha256-template",
      "signingTemplate":"{Method}\n{RequestURL}\n{RawBody}\n{Timestamp}",
      "timestampHeader":"X-Test-Timestamp",
      "signatureHeader":"X-Test-Signature",
      "signatureEncoding":"hex",
      "skewWindow":"5m"
    }
  }
}`)
	if _, err := ValidateCapabilityConfig("test-connector", SignatureVerifierKind, raw); err != nil {
		t.Fatalf("ValidateCapabilityConfig returned error for endpoint RequestURL token: %v", err)
	}
}

func TestDecodeSignatureSchemeConfigRejectsUnknownTemplateToken(t *testing.T) {
	_, _, err := DecodeSignatureSchemeConfig(validSignatureSchemeConfigJSON("{Timestamp}.{RequestURL}.{Bogus}"))
	if err == nil {
		t.Fatal("DecodeSignatureSchemeConfig returned nil error for unknown token")
	}
	if !strings.Contains(err.Error(), `unknown signing template token "{Bogus}"`) {
		t.Fatalf("error = %q, want unknown token diagnostic", err)
	}
}

func mixedCompositeEnvelopeConfigJSON() []byte {
	return []byte(`{
  "eventArrayPath":".events",
  "rules":[
    {
      "matchPath":".kind",
      "matchEquals":"legacy",
      "eventTypeLiteral":"thing.legacy",
      "objectKind":"thing",
      "sourceIdPath":".objectId",
      "idempotencyKeyPath":".eventId"
    },
    {
      "eventTypeLiteral":"thing.composite",
      "objectKind":"thing",
      "sourceIdPath":".objectId",
      "idempotencyKeyComposite":{
        "version":" V1 ",
        "encoding":" SHA256 ",
        "parts":[{"path":" .eventId "},{"path":".occurredAt"}]
      }
    }
  ]
}`)
}

func TestDecodeEnvelopeMappingConfig_CompositeRoundTripHashAndRuleAlignment(t *testing.T) {
	if _, _, err := DecodeEnvelopeMappingConfig("acme-connector", mixedCompositeEnvelopeConfigJSON()); err == nil || !strings.Contains(err.Error(), "WithComposites") {
		t.Fatalf("legacy decode error = %v, want explicit composite-aware API diagnostic", err)
	}

	cfg, composites, canonical, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", mixedCompositeEnvelopeConfigJSON())
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfigWithComposites: %v", err)
	}
	if len(composites) != 2 || composites[0] != nil {
		t.Fatalf("sidecar alignment = %#v, want [nil, composite]", composites)
	}
	composite := composites[1]
	if composite == nil || composite.Version != "v1" || composite.Encoding != "sha256" {
		t.Fatalf("composite = %#v, want normalized v1/sha256", composite)
	}
	if got := []string{composite.Parts[0].Path, composite.Parts[1].Path}; !reflect.DeepEqual(got, []string{".eventId", ".occurredAt"}) {
		t.Fatalf("part order = %v, want declared order", got)
	}
	if strings.Contains(canonical, `"idempotencyKeyPath":""`) {
		t.Fatalf("canonical composite rule contains empty legacy field: %s", canonical)
	}

	roundTrip, roundTripComposites, canonicalAgain, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", []byte(canonical))
	if err != nil {
		t.Fatalf("round-trip decode: %v", err)
	}
	if canonicalAgain != canonical {
		t.Fatalf("canonical round-trip changed:\nfirst  %s\nsecond %s", canonical, canonicalAgain)
	}
	if sha256.Sum256([]byte(canonicalAgain)) != sha256.Sum256([]byte(canonical)) {
		t.Fatal("canonical config hash changed across round trip")
	}
	if !reflect.DeepEqual(roundTripComposites, composites) {
		t.Fatalf("round-trip sidecar = %#v, want %#v", roundTripComposites, composites)
	}
	if !reflect.DeepEqual(roundTrip, cfg) {
		t.Fatalf("round-trip config = %#v, want %#v", roundTrip, cfg)
	}
}

func TestDecodeEnvelopeMappingConfig_CompositeAlignmentSurvivesReverseRuleOrder(t *testing.T) {
	raw := []byte(`{
  "eventArrayPath":".events",
  "rules":[
    {
      "matchPath":".kind",
      "matchEquals":"composite",
      "eventTypeLiteral":"thing.composite",
      "objectKind":"thing",
      "sourceIdPath":".objectId",
      "idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".eventId"},{"path":".occurredAt"}]}
    },
    {
      "eventTypeLiteral":"thing.legacy",
      "objectKind":"thing",
      "sourceIdPath":".objectId",
      "idempotencyKeyPath":".eventId"
    }
  ]
}`)
	_, composites, canonical, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", raw)
	if err != nil {
		t.Fatalf("DecodeEnvelopeMappingConfig: %v", err)
	}
	if len(composites) != 2 || composites[0] == nil || composites[1] != nil {
		t.Fatalf("sidecar alignment = %#v, want [composite, nil]", composites)
	}
	_, roundTripComposites, _, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", []byte(canonical))
	if err != nil || !reflect.DeepEqual(roundTripComposites, composites) {
		t.Fatalf("round-trip sidecar = %#v, %v; want %#v", roundTripComposites, err, composites)
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsInvalidCompositeContract(t *testing.T) {
	base := `{
  "eventArrayPath":".events",
  "rules":[{
    "eventTypeLiteral":"thing.updated",
    "objectKind":"thing",
    "sourceIdPath":".objectId",
    %s
  }]
}`
	tests := []struct {
		name      string
		identity  string
		wantError string
	}{
		{name: "legacy and composite", identity: `"idempotencyKeyPath":".eventId","idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".eventId"}]}`, wantError: "exactly one"},
		{name: "null composite", identity: `"idempotencyKeyComposite":null`, wantError: "must not be null"},
		{name: "empty parts", identity: `"idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[]}`, wantError: "Parts is empty"},
		{name: "unknown version", identity: `"idempotencyKeyComposite":{"version":"v2","encoding":"sha256","parts":[{"path":".eventId"}]}`, wantError: "Version"},
		{name: "unknown encoding", identity: `"idempotencyKeyComposite":{"version":"v1","encoding":"plain","parts":[{"path":".eventId"}]}`, wantError: "Encoding"},
		{name: "empty path", identity: `"idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":" "}]}`, wantError: "Path is empty"},
		{name: "duplicate canonical path", identity: `"idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".items[1]"},{"path":".items[01]"}]}`, wantError: "duplicates"},
		{name: "unknown composite field", identity: `"idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".eventId"}],"mystery":true}`, wantError: "unknown field"},
		{name: "unknown part field", identity: `"idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".eventId","mystery":true}]}`, wantError: "unknown field"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, _, _, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", []byte(fmt.Sprintf(base, tt.identity)))
			if err == nil || !strings.Contains(err.Error(), tt.wantError) {
				t.Fatalf("error = %v, want substring %q", err, tt.wantError)
			}
		})
	}
}

func TestDecodeEnvelopeMappingConfig_RejectsDegenerateOnePartComposite(t *testing.T) {
	tests := []struct {
		name      string
		rule      string
		wantError string
	}{
		{
			name:      "source id",
			rule:      `"eventTypeLiteral":"thing.updated","sourceIdPath":".objectId","idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".objectId"}]}`,
			wantError: "SourceIDPath",
		},
		{
			name:      "event type",
			rule:      `"eventTypeFromPath":".eventType","sourceIdPath":".objectId","idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".eventType"}]}`,
			wantError: "EventTypeFromPath",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			raw := []byte(fmt.Sprintf(`{"eventArrayPath":".events","rules":[{"objectKind":"thing",%s}]}`, tt.rule))
			_, _, _, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", raw)
			if err == nil || !strings.Contains(err.Error(), tt.wantError) {
				t.Fatalf("error = %v, want substring %q", err, tt.wantError)
			}
		})
	}

	raw := []byte(`{"eventArrayPath":".events","rules":[{"eventTypeLiteral":"thing.updated","objectKind":"thing","sourceIdPath":".objectId","idempotencyKeyComposite":{"version":"v1","encoding":"sha256","parts":[{"path":".objectId"},{"path":".eventId"}]}}]}`)
	if _, _, _, err := DecodeEnvelopeMappingConfigWithComposites("acme-connector", raw); err != nil {
		t.Fatalf("multi-part composite containing SourceIDPath must remain valid: %v", err)
	}
}

func TestValidateEnvelopeMappingConfig_RejectsCompositeSidecarLengthMismatch(t *testing.T) {
	cfg := EnvelopeMappingConfig{
		EventArrayPath: ".events",
		Rules: []EnvelopeMappingRule{{
			EventTypeLiteral:   "thing.updated",
			ObjectKind:         "thing",
			SourceIDPath:       ".objectId",
			IdempotencyKeyPath: ".eventId",
		}},
	}
	if err := validateEnvelopeMappingConfig(&cfg, []*IdempotencyKeyComposite{nil, nil}); err == nil || !strings.Contains(err.Error(), "length") {
		t.Fatalf("error = %v, want sidecar length mismatch", err)
	}
}
