package connector

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

func TestConnectorPushConflictNoOldSnapshotPolicyIsExplicitOptIn(t *testing.T) {
	var zero ConnectorPushCreateConflictResolutionContract
	if zero.NoOldSnapshotPolicy != "" {
		t.Fatalf("zero-value no-old-snapshot policy = %q, want fail-closed blank", zero.NoOldSnapshotPolicy)
	}
	if ConnectorPushConflictNoOldSnapshotAdoptLinkOnly != "adoptLinkOnly" {
		t.Fatalf("adopt-only policy = %q", ConnectorPushConflictNoOldSnapshotAdoptLinkOnly)
	}
	if ConnectorPushConflictNoOldSnapshotAdoptLinkThenPatchAuthorizedFrozenCreatePayload != "adoptLinkThenPatchAuthorizedFrozenCreatePayload" {
		t.Fatalf("authorized frozen create payload policy = %q", ConnectorPushConflictNoOldSnapshotAdoptLinkThenPatchAuthorizedFrozenCreatePayload)
	}
}

func TestConnectorPushMatchBeforeCreatePreservesExistingProjection(t *testing.T) {
	legacy := ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey: "connector-a",
		TargetObject:      "targets",
	}
	encoded, err := json.Marshal(legacy)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Contains(encoded, []byte("MatchBeforeCreate")) {
		t.Fatalf("V1 durable projection contains V2 policy: %s", encoded)
	}

	// Keep one unkeyed literal as a compile-time compatibility fence for the
	// exported V1 shape consumed by independently pinned modules.
	unkeyed := ConnectorPushDurableMetadataContract{
		"connector-a", "slot-a", nil, "PushToConnector", "suppress",
		"FlowA", "ActionA", "Source", "destination-a", "owner-a",
		"profile-a", "targets", "properties", "/targets", "/targets/{remoteId}",
		"id", "Remote_Id__c", "", nil, nil, nil,
	}
	if unkeyed.ConnectorOwnerKey != legacy.ConnectorOwnerKey {
		t.Fatalf("legacy unkeyed contract owner = %q", unkeyed.ConnectorOwnerKey)
	}
}

func TestConnectorPushMatchBeforeCreateUsesClosedDispositions(t *testing.T) {
	policy := ConnectorPushMatchBeforeCreateContract{
		OnZeroMatches:     ConnectorPushMatchOnZeroCreate,
		OnOneMatch:        ConnectorPushMatchOnOneAdoptLinkThenPatch,
		OnMultipleMatches: ConnectorPushMatchOnMultipleRejectAmbiguous,
		OnSearchUncertain: ConnectorPushMatchOnUncertainRetryWithoutCreate,
	}
	if !policy.DispositionsValid() {
		t.Fatal("authorized match-before-create dispositions were rejected")
	}

	invalid := []func(*ConnectorPushMatchBeforeCreateContract){
		func(value *ConnectorPushMatchBeforeCreateContract) { value.OnZeroMatches = "skip" },
		func(value *ConnectorPushMatchBeforeCreateContract) { value.OnOneMatch = "create" },
		func(value *ConnectorPushMatchBeforeCreateContract) { value.OnMultipleMatches = "first" },
		func(value *ConnectorPushMatchBeforeCreateContract) { value.OnSearchUncertain = "create" },
	}
	for index, mutate := range invalid {
		candidate := policy
		mutate(&candidate)
		if candidate.DispositionsValid() {
			t.Fatalf("invalid disposition set %d was accepted", index)
		}
	}
}

func TestConnectorPushMatchBeforeCreateProjectionFreezesSearchTemplate(t *testing.T) {
	contract := ConnectorPushDurableMetadataContractV2{
		FormatVersion: ConnectorPushDurableMetadataContractV2FormatVersion,
		Contract: ConnectorPushDurableMetadataContract{
			ConnectorOwnerKey: "connector-a",
			TargetObject:      "targets",
		},
		MatchBeforeCreate: &ConnectorPushMatchBeforeCreateContract{
			Identity: ConnectorPushMatchIdentityContract{
				SourceField:     "ExternalKey",
				TargetProperty:  "external_key",
				RequestBodyPath: "properties.external_key",
				Normalization:   ConnectorPushMatchNormalizationExact,
			},
			Search: ConnectorPushMatchSearchContract{
				Method:       "POST",
				PathTemplate: "/objects/targets/search",
				RequestBodyTemplate: map[string]interface{}{
					"filters": []interface{}{map[string]interface{}{
						"property": ConnectorPushMatchTargetPropertyTemplateToken,
						"value":    ConnectorPushMatchIdentityTemplateToken,
					}},
				},
				ResponseCollectionPath: "results",
				ResponseIDPath:         "id",
				ResponseTotalPath:      "total",
			},
			OnZeroMatches:     ConnectorPushMatchOnZeroCreate,
			OnOneMatch:        ConnectorPushMatchOnOneAdoptLinkThenPatch,
			OnMultipleMatches: ConnectorPushMatchOnMultipleRejectAmbiguous,
			OnSearchUncertain: ConnectorPushMatchOnUncertainRetryWithoutCreate,
		},
	}
	first, err := json.Marshal(contract)
	if err != nil {
		t.Fatal(err)
	}
	second, err := json.Marshal(contract)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(first, second) {
		t.Fatalf("match-before-create projection is not deterministic:\n%s\n%s", first, second)
	}
	for _, required := range [][]byte{
		[]byte(`"FormatVersion":2`),
		[]byte(`"Contract":{"ConnectorOwnerKey":"connector-a"`),
		[]byte(`"MatchBeforeCreate"`),
		[]byte(`"{identity}"`),
		[]byte(`"{targetProperty}"`),
		[]byte(`"ResponseCollectionPath":"results"`),
		[]byte(`"ResponseTotalPath":"total"`),
	} {
		if !bytes.Contains(first, required) {
			t.Fatalf("frozen match-before-create projection %s is missing %s", first, required)
		}
	}
}

func TestConnectorPushDurableMetadataContractV2OmitsAbsentMatchPolicy(t *testing.T) {
	contract := ConnectorPushDurableMetadataContractV2{
		FormatVersion: ConnectorPushDurableMetadataContractV2FormatVersion,
		Contract: ConnectorPushDurableMetadataContract{
			ConnectorOwnerKey: "connector-a",
			TargetObject:      "targets",
		},
	}
	encoded, err := json.Marshal(contract)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Contains(encoded, []byte(`"MatchBeforeCreate"`)) {
		t.Fatalf("absent match-before-create policy was serialized: %s", encoded)
	}
}

func TestConnectorPushMatchNormalizationIsClosed(t *testing.T) {
	for _, value := range []string{
		ConnectorPushMatchNormalizationExact,
		ConnectorPushMatchNormalizationTrim,
		ConnectorPushMatchNormalizationEmail,
	} {
		if !ValidConnectorPushMatchNormalization(value) {
			t.Fatalf("normalization %q was rejected", value)
		}
	}
	for _, value := range []string{"", "lower", "runtime-selected"} {
		if ValidConnectorPushMatchNormalization(value) {
			t.Fatalf("normalization %q was accepted", value)
		}
	}
}
