package connectorbindingownerv1

import (
	"bytes"
	"encoding/json"
	"strings"
	"testing"

	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
)

func TestDefinitionEnvelopeGoldenCanonicalContract(t *testing.T) {
	value, err := NewDefinitionEnvelope(validAuthoringInput())
	if err != nil {
		t.Fatal(err)
	}
	if value.CanonicalDefinitionDigest != "sha256:28f8e6e3da7a1561eab33e6ce2c0a7cc48c668262f5adbfd7370b1cffc335bc3" {
		t.Fatalf("definition digest = %q", value.CanonicalDefinitionDigest)
	}
	if value.OwnerReceiptDigest != "sha256:9521146a9c8461dfdfe6496f75358e5b9d50a06dbb203a56f3c9eacb5de577bd" {
		t.Fatalf("receipt digest = %q", value.OwnerReceiptDigest)
	}
	envelopeDigest, err := DefinitionEnvelopeDigest(value)
	if err != nil {
		t.Fatal(err)
	}
	if envelopeDigest != "sha256:a91b0a8496e71dc1022e55f846f5d15c03ce31b9d283a0c6168680b4fea052bb" {
		t.Fatalf("envelope digest = %q", envelopeDigest)
	}
	if len(value.OwnerReceipt) == 0 || bytes.Contains(value.OwnerReceipt, value.CanonicalDefinition) {
		t.Fatal("owner receipt must be compact and bind the definition by digest")
	}
}

func TestDefinitionEnvelopeJSONIsStrictCanonicalAndDetached(t *testing.T) {
	value, err := NewDefinitionEnvelope(validAuthoringInput())
	if err != nil {
		t.Fatal(err)
	}
	raw, err := MarshalDefinitionEnvelopeJSON(value)
	if err != nil {
		t.Fatal(err)
	}
	parsed, err := ParseDefinitionEnvelopeJSON(raw)
	if err != nil {
		t.Fatal(err)
	}
	parsed.CanonicalDefinition[0] ^= 0xff
	parsed.OwnerReceipt[0] ^= 0xff
	if bytes.Equal(parsed.CanonicalDefinition, value.CanonicalDefinition) || bytes.Equal(parsed.OwnerReceipt, value.OwnerReceipt) {
		t.Fatal("parsed envelope aliases source bytes")
	}

	var document map[string]any
	if err := json.Unmarshal(raw, &document); err != nil {
		t.Fatal(err)
	}
	document["unknown"] = true
	withUnknown, _ := json.Marshal(document)
	if _, err := ParseDefinitionEnvelopeJSON(withUnknown); err == nil || !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("unknown field error = %v", err)
	}
	if _, err := ParseDefinitionEnvelopeJSON(append([]byte(" "), raw...)); err == nil || !strings.Contains(err.Error(), "not canonical JSON") {
		t.Fatalf("non-canonical JSON error = %v", err)
	}
}

func TestDefinitionEnvelopeRejectsEveryIndependentMutation(t *testing.T) {
	base, err := NewDefinitionEnvelope(validAuthoringInput())
	if err != nil {
		t.Fatal(err)
	}
	tests := []struct {
		name   string
		mutate func(*DefinitionEnvelope)
	}{
		{name: "envelope version", mutate: func(value *DefinitionEnvelope) { value.EnvelopeVersion = "v2" }},
		{name: "receipt version", mutate: func(value *DefinitionEnvelope) { value.ReceiptVersion = "v2" }},
		{name: "kind", mutate: func(value *DefinitionEnvelope) { value.Kind = catalogv1.IntegrationBindingKind("UNKNOWN") }},
		{name: "owner", mutate: func(value *DefinitionEnvelope) { value.OwnerPack = "other" }},
		{name: "install", mutate: func(value *DefinitionEnvelope) { value.PackInstallationID = "install-2" }},
		{name: "pack digest", mutate: func(value *DefinitionEnvelope) { value.PackPayloadDigest = packDigest('b') }},
		{name: "business key", mutate: func(value *DefinitionEnvelope) { value.BusinessKey = "connector-a/other" }},
		{name: "definition version", mutate: func(value *DefinitionEnvelope) { value.DefinitionVersion++ }},
		{name: "definition", mutate: func(value *DefinitionEnvelope) { value.CanonicalDefinition[0] ^= 0xff }},
		{name: "definition digest", mutate: func(value *DefinitionEnvelope) { value.CanonicalDefinitionDigest = digest('b') }},
		{name: "receipt", mutate: func(value *DefinitionEnvelope) { value.OwnerReceipt[0] ^= 0xff }},
		{name: "receipt digest", mutate: func(value *DefinitionEnvelope) { value.OwnerReceiptDigest = digest('c') }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			changed := CloneDefinitionEnvelope(base)
			test.mutate(&changed)
			if err := ValidateDefinitionEnvelope(changed); err == nil {
				t.Fatal("mutation unexpectedly validated")
			}
		})
	}
}

func TestDefinitionEnvelopeAcceptsExactlyTheFourClosedBindingKinds(t *testing.T) {
	for _, kind := range []catalogv1.IntegrationBindingKind{
		catalogv1.BindingConnectorInboundRoute,
		catalogv1.BindingConnectorCalloutProfile,
		catalogv1.BindingConnectorFieldMapping,
		catalogv1.BindingConnectorPushContract,
	} {
		t.Run(string(kind), func(t *testing.T) {
			input := validAuthoringInput()
			input.Identity.Kind = kind
			value, err := NewDefinitionEnvelope(input)
			if err != nil {
				t.Fatal(err)
			}
			if value.Kind != kind {
				t.Fatalf("kind = %q", value.Kind)
			}
		})
	}
}

func TestIdentityMatchesOnlyCompleteCatalogOwnerCoordinate(t *testing.T) {
	input := validAuthoringInput()
	binding := catalogv1.IntegrationBinding{
		Kind:               input.Identity.Kind,
		OwnerPack:          input.Identity.OwnerPack,
		PackInstallationID: input.Identity.PackInstallationID,
		PackPayloadDigest:  input.Identity.PackPayloadDigest,
		BusinessKey:        input.Identity.BusinessKey,
		DefinitionVersion:  input.Identity.DefinitionVersion,
		CredentialRefs:     []string{"credential-a"},
	}
	identity := IdentityFromBinding(binding)
	if identity != input.Identity || !identity.MatchesBinding(binding) {
		t.Fatalf("identity = %#v", identity)
	}
	binding.DefinitionVersion++
	if identity.MatchesBinding(binding) {
		t.Fatal("changed definition version unexpectedly matched")
	}
}

func TestDefinitionEnvelopeRejectsInvalidSourceAndBudgets(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*AuthoringInput)
	}{
		{name: "zero version", mutate: func(input *AuthoringInput) { input.Identity.DefinitionVersion = 0 }},
		{name: "blank owner", mutate: func(input *AuthoringInput) { input.Identity.OwnerPack = "" }},
		{name: "noncanonical key", mutate: func(input *AuthoringInput) { input.Identity.BusinessKey += " " }},
		{name: "invalid digest", mutate: func(input *AuthoringInput) { input.Identity.PackPayloadDigest = digest('a') }},
		{name: "empty definition", mutate: func(input *AuthoringInput) { input.CanonicalDefinition = nil }},
		{name: "oversize definition", mutate: func(input *AuthoringInput) { input.CanonicalDefinition = make([]byte, MaxCanonicalDefinitionBytes+1) }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			input := validAuthoringInput()
			test.mutate(&input)
			if _, err := NewDefinitionEnvelope(input); err == nil {
				t.Fatal("invalid source unexpectedly accepted")
			}
		})
	}
}

func validAuthoringInput() AuthoringInput {
	return AuthoringInput{
		Identity: Identity{
			Kind:               catalogv1.BindingConnectorInboundRoute,
			OwnerPack:          "connector-a",
			PackInstallationID: "install-1",
			PackPayloadDigest:  packDigest('a'),
			BusinessKey:        "connector-a/event.created",
			DefinitionVersion:  7,
		},
		CanonicalDefinition: []byte(`{"eventKind":"event.created","flowDeveloperName":"Apply_Event"}`),
	}
}

func packDigest(character byte) string {
	return "nexus-pack-sha256-v1:" + strings.Repeat(string(character), 64)
}

func digest(character byte) string {
	return "sha256:" + strings.Repeat(string(character), 64)
}
