package connector_outboundintent

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"reflect"
	"testing"

	connector_callout "github.com/pingcap-inc/nexuscrm-connectors/features/connector/callout"
	connectorv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
)

func TestDecodeProviderDesiredStatePreservesV1AndV2WriteConstraint(t *testing.T) {
	v1 := outboundIntentTestIntent(41, "intent-v1", "record-v1")
	decodedV1, err := decodeProviderDesiredState(v1)
	if err != nil || decodedV1.writeDisposition != outboundintentv1.UpsertWriteCreateOrUpdate || decodedV1.payload.TargetObject != "target" {
		t.Fatalf("v1 = %#v, err=%v", decodedV1, err)
	}

	v2 := outboundIntentTestIntent(41, "intent-v2", "record-v2")
	payload, err := json.Marshal(outboundintentv1.UpsertDesiredStateV2{
		Version: 2, WriteDisposition: outboundintentv1.UpsertWriteUpdateExistingOnly,
		TargetObject: "target", MappedFields: map[string]json.RawMessage{"name": json.RawMessage(`"value"`)},
	})
	if err != nil {
		t.Fatal(err)
	}
	digest := sha256.Sum256(payload)
	v2.PayloadFormatVersion = 2
	v2.CanonicalPayload = payload
	v2.PayloadDigest = "sha256:" + hex.EncodeToString(digest[:])
	decodedV2, err := decodeProviderDesiredState(v2)
	if err != nil || decodedV2.writeDisposition != outboundintentv1.UpsertWriteUpdateExistingOnly || decodedV2.payload.TargetObject != "target" {
		t.Fatalf("v2 = %#v, err=%v", decodedV2, err)
	}
}

func TestScalarAtJSONPathAcceptsOnlyStringOrNumberIdentity(t *testing.T) {
	for name, test := range map[string]struct {
		body    string
		want    string
		wantErr bool
	}{
		"string":            {body: `{"result":{"id":"remote-1"}}`, want: "remote-1"},
		"number":            {body: `{"result":{"id":42}}`, want: "42"},
		"object":            {body: `{"result":{"id":{"nested":"remote-1"}}}`, wantErr: true},
		"array":             {body: `{"result":{"id":["remote-1"]}}`, wantErr: true},
		"bool":              {body: `{"result":{"id":true}}`, wantErr: true},
		"null":              {body: `{"result":{"id":null}}`, wantErr: true},
		"second JSON value": {body: `{"result":{"id":"remote-1"}} {"ignored":true}`, wantErr: true},
		"trailing garbage":  {body: `{"result":{"id":"remote-1"}} garbage`, wantErr: true},
	} {
		t.Run(name, func(t *testing.T) {
			got, err := scalarAtJSONPath([]byte(test.body), "result.id")
			if got != test.want || (err != nil) != test.wantErr {
				t.Fatalf("scalar = %q, err=%v", got, err)
			}
		})
	}
}

func TestSelectProviderWritePhaseFailsClosedWithoutRequiredRemoteLink(t *testing.T) {
	for _, test := range []struct {
		name        string
		disposition outboundintentv1.UpsertWriteDisposition
		found       bool
		want        string
		wantErr     bool
	}{
		{name: "v1-compatible create", disposition: outboundintentv1.UpsertWriteCreateOrUpdate, want: "create"},
		{name: "create-or-update existing", disposition: outboundintentv1.UpsertWriteCreateOrUpdate, found: true, want: "update"},
		{name: "update-existing found", disposition: outboundintentv1.UpsertWriteUpdateExistingOnly, found: true, want: "update"},
		{name: "update-existing missing", disposition: outboundintentv1.UpsertWriteUpdateExistingOnly, wantErr: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			got, err := selectProviderWritePhase(test.disposition, test.found)
			if got != test.want || (err != nil) != test.wantErr {
				t.Fatalf("phase = %q, err=%v", got, err)
			}
		})
	}
}

func TestProviderMutationPathsMustBindDurableRemoteIdentities(t *testing.T) {
	if got, err := renderUpdatePath("/objects/{remoteId}", "remote/1"); err != nil || got != "/objects/remote%2F1" {
		t.Fatalf("update path = %q err=%v", got, err)
	}
	for name, template := range map[string]string{
		"static":       "/objects/static",
		"wrong token":  "/objects/{id}",
		"empty remote": "/objects/{remoteId}",
	} {
		t.Run("update "+name, func(t *testing.T) {
			remoteID := "remote-1"
			if name == "empty remote" {
				remoteID = ""
			}
			if _, err := renderUpdatePath(template, remoteID); err == nil {
				t.Fatal("identity-free update path accepted")
			}
		})
	}
	valid := "/objects/{fromObject}/{fromId}/related/{toObject}/{toId}"
	if got, err := renderAssociationPath(valid, "from", "from/1", "to", "to/1"); err != nil ||
		got != "/objects/from/from%2F1/related/to/to%2F1" {
		t.Fatalf("association path = %q err=%v", got, err)
	}
	for name, template := range map[string]string{
		"static":          "/relationships/static",
		"missing from id": "/relationships/{toId}",
		"missing to id":   "/relationships/{fromId}",
	} {
		t.Run("association "+name, func(t *testing.T) {
			if _, err := renderAssociationPath(template, "from", "from-1", "to", "to-1"); err == nil {
				t.Fatal("identity-free association path accepted")
			}
		})
	}
}

type scriptedCallouts struct {
	requests []connector_callout.CalloutRequest
	policies []connector_callout.RetryPolicy
	results  []connector_callout.CalloutResponse
	errors   []error
	before   func(int)
}

func (s *scriptedCallouts) Do(_ context.Context, request connector_callout.CalloutRequest, policy connector_callout.RetryPolicy) (connector_callout.CalloutResponse, error) {
	s.requests = append(s.requests, request)
	s.policies = append(s.policies, policy)
	index := len(s.requests) - 1
	if s.before != nil {
		s.before(index)
	}
	if index >= len(s.results) {
		return connector_callout.CalloutResponse{}, ErrDependency
	}
	var err error
	if index < len(s.errors) {
		err = s.errors[index]
	}
	return s.results[index], err
}

func TestProviderCreateFreezesOnePostAndReturnsRemoteIdentity(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{{StatusCode: http.StatusCreated, Body: []byte(`{"id":"remote-1"}`)}}}
	effect := providerEffect{
		callouts: callouts,
		profile:  &connector_callout.ConnectorCalloutProfile{RetryPolicy: connector_callout.RetryPolicy{MaxAttempts: 5}},
		contract: connectorv1.ConnectorPushDurableMetadataContract{ResponseIDPath: "id"},
		main:     connector_callout.CalloutRequest{Method: http.MethodPost, RelativePath: "/objects"},
		phase:    "create", linkFinalizationBase: RemoteLinkFinalization{Create: true},
	}
	result := effect.Dispatch(context.Background())
	if result.Outcome != EffectSucceeded || result.RemoteID != "remote-1" || result.LinkFinalization == nil || result.LinkFinalization.RemoteID != "remote-1" {
		t.Fatalf("result = %#v", result)
	}
	if len(callouts.requests) != 1 || callouts.requests[0].Method != http.MethodPost || callouts.policies[0].MaxAttempts != 1 {
		t.Fatalf("POST dispatch = requests %#v policies %#v", callouts.requests, callouts.policies)
	}
}

func TestProviderUpdateRunsOnlyIdempotentAssociationCallout(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{{StatusCode: http.StatusOK}, {StatusCode: http.StatusNoContent}}}
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
		contract: connectorv1.ConnectorPushDurableMetadataContract{TargetObject: "targets"},
		main:     connector_callout.CalloutRequest{Method: http.MethodPatch, RelativePath: "/targets/target-1"},
		phase:    "update", remoteID: "target-1",
		associations: []resolvedAssociation{{
			contract: connectorv1.ConnectorPushAssociationContract{
				TargetObject: "related", AssociationMethod: http.MethodPut,
				AssociationPath: "/targets/{fromId}/relationships/related/{toId}",
				CalloutPhases:   []string{"update"},
			},
			remoteID: "related-1",
		}},
		linkFinalizationBase: RemoteLinkFinalization{RemoteID: "target-1"},
	}
	result := effect.Dispatch(context.Background())
	if result.Outcome != EffectSucceeded || len(callouts.requests) != 2 {
		t.Fatalf("result=%#v requests=%#v", result, callouts.requests)
	}
	if callouts.policies[0].MaxAttempts != 1 || callouts.policies[1].MaxAttempts != 1 {
		t.Fatalf("provider mutations used in-memory retries: %#v", callouts.policies)
	}
	association := callouts.requests[1]
	if association.Method != http.MethodPut || association.RelativePath != "/targets/target-1/relationships/related/related-1" {
		t.Fatalf("association request = %#v", association)
	}
}

func TestProviderCreateCheckpointsRemoteIdentityBeforeAssociationRetry(t *testing.T) {
	checkpointed := false
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{
		{StatusCode: http.StatusCreated, Body: []byte(`{"id":"target-1"}`)},
		{StatusCode: http.StatusServiceUnavailable},
	}}
	callouts.before = func(index int) {
		if index == 1 && !checkpointed {
			t.Fatal("association I/O began before durable provider-success checkpoint")
		}
	}
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
		contract: connectorv1.ConnectorPushDurableMetadataContract{
			TargetObject: "targets", ResponseIDPath: "id",
		},
		main: connector_callout.CalloutRequest{Method: http.MethodPost}, phase: "create",
		attemptCount: 1, maxAttempts: 3,
		associations: []resolvedAssociation{{
			contract: connectorv1.ConnectorPushAssociationContract{
				TargetObject: "related", AssociationMethod: http.MethodPut,
				AssociationPath: "/targets/{fromId}/related/{toId}", CalloutPhases: []string{"create"},
			},
			remoteID: "related-1",
		}},
		linkFinalizationBase: RemoteLinkFinalization{Create: true, LinkKey: "link-1"},
	}
	result := effect.DispatchWithProviderSuccessCheckpoint(
		context.Background(),
		func(_ context.Context, status int, finalization RemoteLinkFinalization, code string) error {
			if status != http.StatusCreated || finalization.RemoteID != "target-1" ||
				code != "association_create_outcome_uncertain" {
				t.Fatalf("checkpoint = status %d finalization %#v code %q", status, finalization, code)
			}
			checkpointed = true
			return nil
		},
	)
	if result.Outcome != EffectRetryableAfterSuccess || result.ResponseStatus != http.StatusCreated ||
		result.RemoteID != "target-1" || result.LinkFinalization == nil || result.LinkFinalization.RemoteID != "target-1" ||
		result.FailureCode != "association_create_outcome_uncertain" {
		t.Fatalf("result = %#v", result)
	}
	if !checkpointed {
		t.Fatal("provider success was not checkpointed")
	}
}

func TestProviderMainRetriesDoNotConsumeAssociationBudget(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{
		{StatusCode: http.StatusCreated, Body: []byte(`{"id":"target-1"}`)},
		{StatusCode: http.StatusServiceUnavailable},
	}}
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
		contract: connectorv1.ConnectorPushDurableMetadataContract{
			TargetObject: "targets", ResponseIDPath: "id",
		},
		main: connector_callout.CalloutRequest{Method: http.MethodPost}, phase: "create",
		// Simulate a main call that succeeds on its last allowed durable attempt.
		// The first association attempt starts only after that success checkpoint.
		attemptCount: 3, maxAttempts: 3,
		associations: []resolvedAssociation{{
			contract: connectorv1.ConnectorPushAssociationContract{
				TargetObject: "related", AssociationMethod: http.MethodPut,
				AssociationPath: "/targets/{fromId}/related/{toId}", CalloutPhases: []string{"create"},
			},
			remoteID: "related-1",
		}},
		linkFinalizationBase: RemoteLinkFinalization{Create: true, LinkKey: "link-1"},
	}
	result := effect.DispatchWithProviderSuccessCheckpoint(
		context.Background(),
		func(context.Context, int, RemoteLinkFinalization, string) error { return nil },
	)
	if result.Outcome != EffectRetryableAfterSuccess ||
		result.FailureCode != "association_create_outcome_uncertain" {
		t.Fatalf("result = %#v", result)
	}
}

func TestProviderAssociationCheckpointResumesOriginalPhaseWithoutMainReplay(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{{StatusCode: http.StatusNoContent}}}
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
		contract: connectorv1.ConnectorPushDurableMetadataContract{TargetObject: "targets"},
		phase:    "create", associationOnly: true, checkpointStatus: http.StatusCreated,
		remoteID: "target-1",
		associations: []resolvedAssociation{{
			contract: connectorv1.ConnectorPushAssociationContract{
				TargetObject: "related", AssociationMethod: http.MethodPut,
				AssociationPath: "/targets/{fromId}/related/{toId}", CalloutPhases: []string{"create"},
			},
			remoteID: "related-1",
		}},
		linkFinalizationBase: RemoteLinkFinalization{LinkKey: "link-1"},
	}
	result := effect.Dispatch(context.Background())
	if result.Outcome != EffectSucceeded || len(callouts.requests) != 1 || callouts.requests[0].Method != http.MethodPut ||
		result.LinkFinalization == nil || result.LinkFinalization.Create {
		t.Fatalf("result=%#v requests=%#v", result, callouts.requests)
	}
}

func TestCheckpointedAssociationPhaseRequiresDurableMainIdentity(t *testing.T) {
	command := Command{RemoteID: "remote-1", ResponseStatus: http.StatusCreated, FailureCode: "association_create_outcome_uncertain"}
	if phase, ok := checkpointedAssociationPhase(command); !ok || phase != "create" {
		t.Fatalf("checkpoint = %q, %v", phase, ok)
	}
	command.RemoteID = ""
	if _, ok := checkpointedAssociationPhase(command); ok {
		t.Fatal("checkpoint without durable remote ID was accepted")
	}
}

func TestProviderCreateConflictCheckpointsRemoteIdentityBeforePatch(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{
		{StatusCode: http.StatusConflict, Body: []byte(`{"category":"CONFLICT"}`)},
		{StatusCode: http.StatusOK, Body: []byte(`{"id":"target-1"}`)},
	}}
	body := []byte(`{"properties":{"email":"User+Tag@example.com"}}`)
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{}, body: body,
		contract: connectorv1.ConnectorPushDurableMetadataContract{
			UpdatePathTemplate: "/targets/{remoteId}",
			CreateConflictResolution: &connectorv1.ConnectorPushCreateConflictResolutionContract{
				StatusCodes: []int{http.StatusConflict}, DiagnosticMatch: map[string]string{"category": "CONFLICT"},
				Identity:            connectorv1.ConnectorPushConflictIdentityContract{RequestBodyPath: "properties.email", Normalization: connectorv1.ConnectorPushConflictNormalizationEmail},
				Resolve:             connectorv1.ConnectorPushConflictResolverContract{Method: http.MethodGet, PathTemplate: "/targets/{identity}?identityField=email&needle={identity}", ResponseIDPath: "id", RequireExactlyOne: true},
				OnResolved:          connectorv1.ConnectorPushConflictOnResolvedAdoptLinkThenPatch,
				NoOldSnapshotPolicy: connectorv1.ConnectorPushConflictNoOldSnapshotAdoptLinkThenPatchAuthorizedFrozenCreatePayload,
			},
		},
		main: connector_callout.CalloutRequest{Method: http.MethodPost}, phase: "create",
		linkFinalizationBase: RemoteLinkFinalization{Create: true},
	}
	result := effect.Dispatch(context.Background())
	if result.Outcome != EffectRetryableAfterSuccess || result.RemoteID != "target-1" ||
		result.FailureCode != "conflict_adopted_patch_pending" || result.LinkFinalization == nil ||
		len(callouts.requests) != 2 {
		t.Fatalf("result=%#v requests=%#v", result, callouts.requests)
	}
	if callouts.requests[1].Method != http.MethodGet || callouts.requests[1].RelativePath != "/targets/user+tag@example.com" ||
		callouts.requests[1].QueryParams.Get("needle") != "user+tag@example.com" {
		t.Fatalf("resolver = %#v", callouts.requests[1])
	}
	if callouts.policies[0].MaxAttempts != 1 || callouts.policies[1].MaxAttempts != 1 {
		t.Fatalf("conflict discovery policies = %#v", callouts.policies)
	}

	// After the worker durably commits the returned link, normal preparation
	// selects the update lane; only then may the frozen PATCH be dispatched.
	patchCallouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{{StatusCode: http.StatusOK}}}
	followup := providerEffect{
		callouts: patchCallouts, profile: &connector_callout.ConnectorCalloutProfile{RetryPolicy: connector_callout.RetryPolicy{MaxAttempts: 5}},
		main:  connector_callout.CalloutRequest{Method: http.MethodPatch, RelativePath: "/targets/target-1"},
		phase: "update", remoteID: "target-1", linkFinalizationBase: RemoteLinkFinalization{RemoteID: "target-1"},
	}
	patched := followup.Dispatch(context.Background())
	if patched.Outcome != EffectSucceeded || len(patchCallouts.requests) != 1 ||
		patchCallouts.requests[0].Method != http.MethodPatch || patchCallouts.policies[0].MaxAttempts != 1 {
		t.Fatalf("checkpointed patch = %#v requests=%#v policies=%#v", patched, patchCallouts.requests, patchCallouts.policies)
	}
}

func TestProviderCreateConflictRequiresExplicitFrozenPayloadPolicy(t *testing.T) {
	for _, test := range []struct {
		name       string
		policy     string
		want       EffectOutcome
		wantMethod string
	}{
		{name: "missing policy fails closed", want: EffectRejected},
		{name: "adopt only", policy: connectorv1.ConnectorPushConflictNoOldSnapshotAdoptLinkOnly, want: EffectSucceeded, wantMethod: http.MethodGet},
		{name: "authorized frozen payload", policy: connectorv1.ConnectorPushConflictNoOldSnapshotAdoptLinkThenPatchAuthorizedFrozenCreatePayload, want: EffectRetryableAfterSuccess, wantMethod: http.MethodGet},
	} {
		t.Run(test.name, func(t *testing.T) {
			callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{
				{StatusCode: http.StatusConflict, Body: []byte(`{"category":"CONFLICT"}`)},
				{StatusCode: http.StatusOK, Body: []byte(`{"id":"target-1"}`)},
				{StatusCode: http.StatusOK},
			}}
			effect := providerEffect{
				callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
				body: []byte(`{"properties":{"email":"a@example.com"}}`),
				contract: connectorv1.ConnectorPushDurableMetadataContract{
					UpdatePathTemplate: "/targets/{remoteId}", ResponseIDPath: "id",
					CreateConflictResolution: &connectorv1.ConnectorPushCreateConflictResolutionContract{
						StatusCodes: []int{http.StatusConflict}, DiagnosticMatch: map[string]string{"category": "CONFLICT"},
						Identity: connectorv1.ConnectorPushConflictIdentityContract{RequestBodyPath: "properties.email"},
						Resolve: connectorv1.ConnectorPushConflictResolverContract{
							Method: http.MethodGet, PathTemplate: "/targets/{identity}",
							ResponseIDPath: "id", RequireExactlyOne: true,
						},
						OnResolved:          connectorv1.ConnectorPushConflictOnResolvedAdoptLinkThenPatch,
						NoOldSnapshotPolicy: test.policy,
					},
				},
				main: connector_callout.CalloutRequest{Method: http.MethodPost}, phase: "create",
				linkFinalizationBase: RemoteLinkFinalization{Create: true},
			}
			result := effect.Dispatch(context.Background())
			if result.Outcome != test.want {
				t.Fatalf("result = %#v", result)
			}
			lastMethod := callouts.requests[len(callouts.requests)-1].Method
			if test.wantMethod != "" && lastMethod != test.wantMethod {
				t.Fatalf("last method = %q, want %q", lastMethod, test.wantMethod)
			}
			if test.policy == "" && len(callouts.requests) != 2 {
				t.Fatalf("missing policy issued %d requests, want POST + resolver only", len(callouts.requests))
			}
		})
	}
}

func TestProviderAssociationPermanentFailureStopsAfterMainSuccess(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{
		{StatusCode: http.StatusCreated, Body: []byte(`{"id":"target-1"}`)},
		{StatusCode: http.StatusBadRequest},
	}}
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
		contract: connectorv1.ConnectorPushDurableMetadataContract{TargetObject: "targets", ResponseIDPath: "id"},
		main:     connector_callout.CalloutRequest{Method: http.MethodPost}, phase: "create",
		attemptCount: 1, maxAttempts: 3,
		associations: []resolvedAssociation{{
			contract: connectorv1.ConnectorPushAssociationContract{
				TargetObject: "related", AssociationMethod: http.MethodPut,
				AssociationPath: "/targets/{fromId}/related/{toId}", CalloutPhases: []string{"create"},
			},
			remoteID: "related-1",
		}},
		linkFinalizationBase: RemoteLinkFinalization{Create: true, LinkKey: "link-1"},
	}
	result := effect.Dispatch(context.Background())
	if result.Outcome != EffectTerminalAfterSuccess || result.FailureCode != "association_create_rejected" ||
		result.RemoteID != "target-1" || result.LinkFinalization == nil {
		t.Fatalf("result = %#v", result)
	}
}

func TestProviderAssociationRetryBudgetIsDurablyBounded(t *testing.T) {
	callouts := &scriptedCallouts{results: []connector_callout.CalloutResponse{{StatusCode: http.StatusServiceUnavailable}}}
	effect := providerEffect{
		callouts: callouts, profile: &connector_callout.ConnectorCalloutProfile{},
		contract: connectorv1.ConnectorPushDurableMetadataContract{TargetObject: "targets"},
		phase:    "create", associationOnly: true, checkpointStatus: http.StatusCreated,
		attemptCount: 3, maxAttempts: 3, remoteID: "target-1",
		associations: []resolvedAssociation{{
			contract: connectorv1.ConnectorPushAssociationContract{
				TargetObject: "related", AssociationMethod: http.MethodPut,
				AssociationPath: "/targets/{fromId}/related/{toId}", CalloutPhases: []string{"create"},
			},
			remoteID: "related-1",
		}},
		linkFinalizationBase: RemoteLinkFinalization{LinkKey: "link-1"},
	}
	result := effect.Dispatch(context.Background())
	if result.Outcome != EffectTerminalAfterSuccess || result.FailureCode != "association_create_retry_exhausted" ||
		result.LinkFinalization == nil || result.LinkFinalization.Create {
		t.Fatalf("result = %#v", result)
	}
	if len(callouts.policies) != 1 || callouts.policies[0].MaxAttempts != 1 {
		t.Fatalf("association policy = %#v", callouts.policies)
	}
}

func TestAssociationTargetActionSlotLineageIsOrderedAndExact(t *testing.T) {
	contract := connectorv1.ConnectorPushAssociationContract{
		TargetActionSlotID:          "z-current-slot",
		TargetPreviousActionSlotIDs: []string{"a-old-slot", "z-current-slot", "", "A-OLD-SLOT", "a-old-slot"},
	}
	got := associationTargetActionSlotIDs(contract)
	want := []string{"z-current-slot", "a-old-slot", "A-OLD-SLOT"}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("slots = %#v, want %#v", got, want)
	}
}

func TestRemoteLinkExecutionSnapshotFailsClosedOnAuthorityDrift(t *testing.T) {
	scope := outboundIntentTestScope()
	intent := outboundIntentTestIntent(scope.CatalogRevision, "intent-1", "record-1")
	command := Command{Scope: scope, Intent: intent, LinkKey: outboundLinkKey(scope, intent)}
	profile := &connector_callout.ConnectorCalloutProfile{
		OwnerPack: "profile-owner", ProfileName: "profile", InstalledPackID: "install-2",
		CredentialRef: "credential", EndpointClass: "endpoint",
	}
	contract := connectorv1.ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey: intent.ConnectorOwnerKey, FlowDeveloperName: intent.FlowDeveloperName,
		FlowActionName: intent.FlowActionName, TargetObject: "target", CreatePathTemplate: "/objects",
		UpdatePathTemplate: "/objects/{remoteId}", ResponseIDPath: "id", RemoteIDField: "RemoteId",
		RemoteIDProvenanceField: "RemoteIdSource",
	}
	resolved := ResolvedAuthority{
		Profile: profile, RetryPolicyJSON: `{"maxAttempts":3}`, ProfileHash: "profile-hash",
		MappingFingerprint: "mapping-hash", BodyFingerprint: "body-hash",
	}
	base := remoteLinkIdentity{
		LinkKey: command.LinkKey, Status: remoteLinkStatusFinalized, RemoteID: "remote-1",
		ConnectorOwnerKey: intent.ConnectorOwnerKey, ActionSlotID: intent.ActionSlotID,
		ActionType: connectorPushActionType, SourceObject: intent.SourceObject,
		SourceRecordID: intent.SourceRecordID, DestinationSlotID: intent.DestinationSlotID,
		RequesterOwnerPack: contract.ConnectorOwnerKey, ProfileOwnerPack: profile.OwnerPack,
		FlowDeveloperName: contract.FlowDeveloperName, FlowActionName: contract.FlowActionName,
		TargetObject: contract.TargetObject, ProfileName: profile.ProfileName,
		LastValidatedPackInstallationID: profile.InstalledPackID,
		CredentialRef:                   profile.CredentialRef, EndpointClass: profile.EndpointClass,
		RetryPolicyJSON: resolved.RetryPolicyJSON, ProfileContractHash: resolved.ProfileHash,
		CreatePathTemplate: contract.CreatePathTemplate, UpdatePathTemplate: contract.UpdatePathTemplate,
		ResponseIDPath: contract.ResponseIDPath, RemoteIDField: contract.RemoteIDField,
		RemoteIDProvenanceField: contract.RemoteIDProvenanceField,
		MappingFingerprint:      resolved.MappingFingerprint, BodyFingerprint: resolved.BodyFingerprint,
	}
	if err := validateRemoteLinkExecutionSnapshot(base, command, contract, resolved); err != nil {
		t.Fatalf("valid snapshot rejected: %v", err)
	}
	for name, mutate := range map[string]func(*remoteLinkIdentity){
		"requester owner": func(value *remoteLinkIdentity) { value.RequesterOwnerPack = "other" },
		"profile owner":   func(value *remoteLinkIdentity) { value.ProfileOwnerPack = "other" },
		"profile":         func(value *remoteLinkIdentity) { value.ProfileName = "other" },
		"credential":      func(value *remoteLinkIdentity) { value.CredentialRef = "other" },
		"endpoint":        func(value *remoteLinkIdentity) { value.EndpointClass = "other" },
		"retry policy":    func(value *remoteLinkIdentity) { value.RetryPolicyJSON = `{}` },
		"profile hash":    func(value *remoteLinkIdentity) { value.ProfileContractHash = "other" },
		"target":          func(value *remoteLinkIdentity) { value.TargetObject = "other" },
		"create path":     func(value *remoteLinkIdentity) { value.CreatePathTemplate = "/other" },
		"update path":     func(value *remoteLinkIdentity) { value.UpdatePathTemplate = "/other/{remoteId}" },
		"response path":   func(value *remoteLinkIdentity) { value.ResponseIDPath = "other" },
		"remote field":    func(value *remoteLinkIdentity) { value.RemoteIDField = "Other" },
		"provenance field": func(value *remoteLinkIdentity) {
			value.RemoteIDProvenanceField = "OtherSource"
		},
		"mapping": func(value *remoteLinkIdentity) { value.MappingFingerprint = "other" },
		"body":    func(value *remoteLinkIdentity) { value.BodyFingerprint = "other" },
	} {
		t.Run(name, func(t *testing.T) {
			value := base
			mutate(&value)
			if err := validateRemoteLinkExecutionSnapshot(value, command, contract, resolved); err == nil {
				t.Fatal("drifted snapshot accepted")
			}
		})
	}
}

func TestAssociationRemoteLinkIdentityAcceptsOnlyAuthorizedLineage(t *testing.T) {
	binding := connectorv1.ConnectorPushAssociationContract{
		SourceObject: "source", TargetObject: "target", TargetDestinationSlotID: "destination",
		TargetProfileOwnerPack: "profile-owner", TargetProfileName: "profile", RemoteIDField: "RemoteId",
	}
	base := remoteLinkIdentity{
		Status: remoteLinkStatusFinalized, RemoteID: "remote-1", ConnectorOwnerKey: "requester",
		ActionSlotID: "a-old-slot", ActionType: connectorPushActionType, SourceObject: binding.SourceObject,
		SourceRecordID: "record-1", DestinationSlotID: binding.TargetDestinationSlotID,
		RequesterOwnerPack: "requester", ProfileOwnerPack: binding.TargetProfileOwnerPack,
		ProfileName: binding.TargetProfileName, TargetObject: binding.TargetObject,
		RemoteIDField: binding.RemoteIDField,
	}
	slots := []string{"z-current-slot", "a-old-slot"}
	if err := validateAssociationRemoteLinkIdentity(base, "requester", binding, "record-1", slots); err != nil {
		t.Fatalf("previous lineage rejected: %v", err)
	}
	for name, mutate := range map[string]func(*remoteLinkIdentity){
		"unknown slot":    func(value *remoteLinkIdentity) { value.ActionSlotID = "unknown" },
		"requester":       func(value *remoteLinkIdentity) { value.RequesterOwnerPack = "other" },
		"profile owner":   func(value *remoteLinkIdentity) { value.ProfileOwnerPack = "other" },
		"profile":         func(value *remoteLinkIdentity) { value.ProfileName = "other" },
		"target":          func(value *remoteLinkIdentity) { value.TargetObject = "other" },
		"remote id field": func(value *remoteLinkIdentity) { value.RemoteIDField = "Other" },
	} {
		t.Run(name, func(t *testing.T) {
			value := base
			mutate(&value)
			if err := validateAssociationRemoteLinkIdentity(value, "requester", binding, "record-1", slots); err == nil {
				t.Fatal("drifted association link accepted")
			}
		})
	}
}

func TestBuildProviderBodyMatchesLaunchAssociationShape(t *testing.T) {
	payload := testDesiredState(t)
	body, err := buildProviderBody(payload, []resolvedAssociation{{
		contract: connectorv1.ConnectorPushAssociationContract{
			TargetObject: "related", PayloadField: "relationships", PayloadMergeMode: "append",
			InlinePhases:    []string{"create"},
			PayloadTemplate: map[string]any{"target": map[string]any{"id": "{toId}"}, "types": []any{map[string]any{"relationshipType": json.Number("5")}}},
		}, remoteID: "related-1",
	}}, "create", "targets", "")
	if err != nil {
		t.Fatal(err)
	}
	var got map[string]any
	if err := json.Unmarshal(body, &got); err != nil {
		t.Fatal(err)
	}
	want := map[string]any{
		"properties":    map[string]any{"title": "Example"},
		"relationships": []any{map[string]any{"target": map[string]any{"id": "related-1"}, "types": []any{map[string]any{"relationshipType": float64(5)}}}},
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("body = %#v, want %#v", got, want)
	}
}

func testDesiredState(t *testing.T) outboundintentv1.UpsertDesiredStateV1 {
	t.Helper()
	return outboundintentv1.UpsertDesiredStateV1{
		Version: 1, TargetObject: "targets", BodyRoot: "properties",
		MappedFields: map[string]json.RawMessage{"title": json.RawMessage(`"Example"`)},
	}
}

func TestProviderNoEffectCertaintyRequiresLocalRefusal(t *testing.T) {
	for _, tc := range []struct {
		name   string
		status int
		err    error
		want   EffectOutcome
	}{
		{"local-invalid-request", 0, connector_callout.ErrInvalidRequest, EffectRejectedNoEffect},
		{"local-insecure-endpoint", 0, connector_callout.ErrCredentialedEndpointMustBeHTTPS, EffectRejectedNoEffect},
		{"local-unsupported-auth", 0, connector_callout.ErrUnsupportedAuthType, EffectRejectedNoEffect},
		{"http-refusal-is-not-proof", http.StatusBadRequest, nil, EffectRejected},
		{"http-conflict-is-not-proof", http.StatusConflict, nil, EffectRejected},
		{"response-precedes-local-error", http.StatusBadRequest, connector_callout.ErrInvalidRequest, EffectRejected},
		{"rate-limit", http.StatusTooManyRequests, nil, EffectUncertain},
		{"server-failure", http.StatusInternalServerError, nil, EffectUncertain},
		{"transport-timeout", 0, context.DeadlineExceeded, EffectUncertain},
	} {
		t.Run(tc.name, func(t *testing.T) {
			result, terminal := classifyProviderResult(connector_callout.CalloutResponse{StatusCode: tc.status}, tc.err)
			if !terminal || result.Outcome != tc.want {
				t.Fatalf("outcome=%s terminal=%t want=%s", result.Outcome, terminal, tc.want)
			}
		})
	}
}
