package automation

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

func TestRemoteIdentityTransferScopeComesFromExactCatalogRoute(t *testing.T) {
	view := runtimeOutboundIntentTestView(t)
	contract := defaultPushConnectorContract()
	binding := connectorInboundRegisterInstalledBinding{
		Identity:          connectorPushDurableIdentityFromContract(contract, "source-record"),
		FlowDeveloperName: contract.FlowDeveloperName, FlowActionName: contract.FlowActionName,
	}
	ctx := metadata.WithRuntimeMetadataView(context.Background(), view)
	scope, err := resolveConnectorIntegrationOutboundScope(ctx, binding)
	if err != nil {
		t.Fatal(err)
	}
	if scope != (connectorIntegrationOutboundScope{
		orgID: "org-runtime-outbound", appInstallationID: "app-installation", authorizedWorkloadID: "integration-service",
	}) {
		t.Fatalf("transfer scope = %#v", scope)
	}
	// No caller-supplied or persisted identity can provide missing Catalog scope.
	if scope, err := resolveConnectorIntegrationOutboundScope(context.Background(), binding); err != nil || scope != (connectorIntegrationOutboundScope{}) {
		t.Fatalf("legacy context granted Integration scope: %#v, %v", scope, err)
	}
	for _, axis := range []string{"owner", "action", "object", "destination", "flow", "flow action", "action type"} {
		t.Run(axis, func(t *testing.T) {
			other := binding
			switch axis {
			case "owner":
				other.Identity.ConnectorOwnerKey = "other-owner"
			case "action":
				other.Identity.ActionSlotID = "other-slot"
			case "object":
				other.Identity.SourceObject = "OtherObject"
			case "destination":
				other.Identity.DestinationSlotID = "other-destination"
			case "flow":
				other.FlowDeveloperName = "OtherFlow"
			case "flow action":
				other.FlowActionName = "OtherAction"
			case "action type":
				other.Identity.ActionType = "OtherActionType"
			}
			if scope, err := resolveConnectorIntegrationOutboundScope(ctx, other); !errors.Is(err, ErrConnectorPushDurableConflict) || scope != (connectorIntegrationOutboundScope{}) {
				t.Fatalf("different %s gained scope: %#v, %v", axis, scope, err)
			}
		})
	}
}

func TestFlowExecutorTransferTargetSnapshotDoesNotInventInboundAuthorship(t *testing.T) {
	exec := connectorInboundInstalledBindingsExecutor([]connector.ConnectorPushDurableMetadataContract{
		connectorInboundInstalledTestContract("Lead", "External_Id__c", "lead-push"),
		connectorInboundInstalledTestContract("Contact", "External_Id__c", "contact-push"),
	})
	transfer := ConnectorRemoteIdentityTransfer{
		SourceObject: "Lead", SourceRecordId: "source", TargetObject: "Contact", TargetRecordId: "target", RemoteIDField: "External_Id__c",
	}
	hints := []ConnectorRemoteIdentityTransferHint{{ConnectorOwnerKey: "connector_a", ConnectorNamespace: "connector_a", RemoteID: "remote"}}
	admissions, err := exec.resolveConnectorRemoteIdentityTransferAdmissions(context.Background(), transfer, hints)
	if err != nil || len(admissions) != 1 {
		t.Fatalf("async transfer without inbound origin: %v, %v", admissions, err)
	}
	snapshot := admissions[0].TargetSnapshot
	if snapshot.ConnectorNamespace != "connector_a" || snapshot.RequesterOwnerPack != "connector_a" ||
		snapshot.FlowDeveloperName != "Connector_Push_Contact" || snapshot.RemoteIDField != "External_Id__c" ||
		snapshot.CreatedPackInstallID != "install-1" || snapshot.MappingFingerprint == "" || snapshot.ProfileContractHash == "" {
		t.Fatalf("target snapshot was not derived from installed Contact authority: %#v", snapshot)
	}
	ctx := context.WithValue(packOwnedPushConnectorContext(), constants.ContextKeyConnectorSyncOrigin, "inbound-connector")
	later, err := exec.resolveConnectorRemoteIdentityTransferAdmissions(ctx, transfer, hints)
	if err != nil || len(later) != 1 || !connectorPushPersistedRemoteLinkSnapshotEqual(snapshot, later[0].TargetSnapshot) {
		t.Fatalf("inbound origin changed durable target authority: %v, %v", later, err)
	}
}

func TestTransferTargetSnapshotUsesExactImmutableFinalizationAuthority(t *testing.T) {
	contract := connectorInboundInstalledTestContract("Contact", "External_Id__c", "contact-push")
	exec := connectorInboundInstalledBindingsExecutor([]connector.ConnectorPushDurableMetadataContract{contract})
	set := exec.connectorInboundInstalled.(connectorInboundInstalledContractResolverStub).set
	target, err := selectConnectorInboundInstalledBinding(set, "connector_a", "Contact", "External_Id__c")
	if err != nil {
		t.Fatal(err)
	}
	target.Identity.SourceRecordID = "target-contact"
	profile := *exec.connectorPushProfiles.(staticPushProfileResolver).profile
	exec.connectorPushProfiles = nil // An immutable closure must not fall back to split reads.
	for _, variant := range []string{"exact", "different source", "different field", "different generation", "missing generation", "different catalog"} {
		t.Run(variant, func(t *testing.T) {
			authority := &ConnectorInboundRemoteIdentityFinalizationAuthority{
				Contract: contract, Profile: profile, OutboundMappings: set.Mappings,
			}
			ctx := context.Background()
			switch variant {
			case "different source":
				authority.Contract.SourceObject = "Lead"
			case "different field":
				authority.Contract.RemoteIDField = "Other_Id__c"
			case "different generation":
				authority.Profile.CreatedPackInstallID = "replacement-install"
			case "missing generation":
				authority.Profile.CreatedPackInstallID = ""
			case "different catalog":
				ctx = metadata.WithRuntimeMetadataView(ctx, runtimeOutboundIntentTestView(t))
			}
			exec.SetConnectorInboundRemoteIdentityFinalizationAuthorityResolver(&captureInboundFinalizeAuthorityResolver{authority: authority})
			snapshot, err := exec.resolveConnectorRemoteIdentityTransferTargetSnapshot(ctx, set, target, "External_Id__c")
			if variant != "exact" {
				if err == nil {
					t.Fatalf("mismatched %s supplied target authority: %#v", variant, snapshot)
				}
				return
			}
			if err != nil || snapshot.FlowDeveloperName != contract.FlowDeveloperName || snapshot.CreatedPackInstallID != "install-1" {
				t.Fatalf("exact immutable snapshot = %#v, error = %v", snapshot, err)
			}
		})
	}
}

type connectorInboundInstalledContractResolverStub struct {
	set ConnectorInboundRemoteIdentityInstalledContractSet
	err error
}

type connectorInboundInstalledBatchResolverStub struct {
	sets         map[string]ConnectorInboundRemoteIdentityInstalledContractSet
	batchOwners  []string
	singleLocked int
}

func (s *connectorInboundInstalledBatchResolverStub) ResolveConnectorInboundRemoteIdentityInstalledContracts(context.Context, string) (ConnectorInboundRemoteIdentityInstalledContractSet, error) {
	return ConnectorInboundRemoteIdentityInstalledContractSet{}, nil
}

func (s *connectorInboundInstalledBatchResolverStub) ResolveConnectorInboundRemoteIdentityInstalledContractsInAmbientTx(context.Context, string) (ConnectorInboundRemoteIdentityInstalledContractSet, error) {
	s.singleLocked++
	return ConnectorInboundRemoteIdentityInstalledContractSet{}, nil
}

func (s *connectorInboundInstalledBatchResolverStub) ResolveConnectorInboundRemoteIdentityInstalledContractSetsInAmbientTx(_ context.Context, owners []string) (map[string]ConnectorInboundRemoteIdentityInstalledContractSet, error) {
	s.batchOwners = append([]string(nil), owners...)
	return s.sets, nil
}

func (s connectorInboundInstalledContractResolverStub) ResolveConnectorInboundRemoteIdentityInstalledContractsInAmbientTx(context.Context, string) (ConnectorInboundRemoteIdentityInstalledContractSet, error) {
	return s.set, s.err
}

func (s connectorInboundInstalledContractResolverStub) ResolveConnectorInboundRemoteIdentityInstalledContracts(context.Context, string) (ConnectorInboundRemoteIdentityInstalledContractSet, error) {
	return s.set, s.err
}

func (s connectorInboundInstalledContractResolverStub) ResolveConnectorInboundRemoteIdentityInstalledContractSetsInAmbientTx(_ context.Context, owners []string) (map[string]ConnectorInboundRemoteIdentityInstalledContractSet, error) {
	sets := make(map[string]ConnectorInboundRemoteIdentityInstalledContractSet, len(owners))
	for _, owner := range owners {
		sets[owner] = s.set
	}
	return sets, s.err
}

func TestFlowExecutorResolvesRecoveryBindingsFromLockedInstalledContracts(t *testing.T) {
	exec := connectorInboundInstalledBindingsExecutor([]connector.ConnectorPushDurableMetadataContract{
		connectorInboundInstalledTestContract("Lead", "HubSpot_Lead_Id__c", "lead-push"),
		connectorInboundInstalledTestContract("Contact", "HubSpot_Contact_Id__c", "contact-push"),
	})
	resolved, err := exec.ResolveConnectorInboundRemoteIdentityRecoveryInstalledBindings(context.Background(), ConnectorInboundRemoteIdentityRecoverySelector{
		ConnectorOwnerKey: "connector_a", ConnectorNamespace: "hubspot-routing",
		DestinationSlotID: "hubspot-contact", RemoteID: "remote-1",
	})
	if err != nil {
		t.Fatalf("ResolveConnectorInboundRemoteIdentityRecoveryInstalledBindings: %v", err)
	}
	if resolved.Generation != "generation-1" || len(resolved.Bindings) != 2 {
		t.Fatalf("resolved bindings = %#v", resolved)
	}
	for _, binding := range resolved.Bindings {
		if binding.Identity.SourceRecordID != "" || binding.Identity.DestinationSlotID != "hubspot-contact" {
			t.Fatalf("derived identity = %#v", binding.Identity)
		}
		if binding.Snapshot.RemoteIDField != binding.EvidenceBinding.RemoteIDField || binding.Snapshot.ProfileContractHash == "" {
			t.Fatalf("derived snapshot = %#v", binding.Snapshot)
		}
	}
}

func TestFlowExecutorRegisterBindingRequiresUniqueInstalledDestination(t *testing.T) {
	tests := []struct {
		name      string
		contracts []connector.ConnectorPushDurableMetadataContract
		wantErr   string
	}{
		{name: "missing", contracts: []connector.ConnectorPushDurableMetadataContract{connectorInboundInstalledTestContract("Contact", "Other_Id__c", "other")}, wantErr: "not found"},
		{name: "ambiguous", contracts: []connector.ConnectorPushDurableMetadataContract{
			connectorInboundInstalledTestContract("Contact", "HubSpot_Id__c", "one"),
			connectorInboundInstalledTestContract("Contact", "HubSpot_Id__c", "two"),
		}, wantErr: "ambiguous"},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			exec := connectorInboundInstalledBindingsExecutor(tc.contracts)
			_, err := exec.resolveConnectorInboundRegisterInstalledBinding(context.Background(), "connector_a", "Contact", "HubSpot_Id__c")
			if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
				t.Fatalf("register binding error = %v, want %q", err, tc.wantErr)
			}
		})
	}
}

func TestFlowExecutorTransferAdmissionsResolveAllOwnersInOneLockedBatch(t *testing.T) {
	sets := make(map[string]ConnectorInboundRemoteIdentityInstalledContractSet)
	for _, owner := range []string{"owner-a", "owner-z"} {
		lead := connectorInboundInstalledTestContract("Lead", "HubSpot_Id__c", owner+"-lead")
		contact := connectorInboundInstalledTestContract("Contact", "HubSpot_Id__c", owner+"-contact")
		lead.ConnectorOwnerKey = owner
		contact.ConnectorOwnerKey = owner
		lead.ProfileOwnerPack = "connector_a"
		contact.ProfileOwnerPack = "connector_a"
		sets[owner] = ConnectorInboundRemoteIdentityInstalledContractSet{
			Generation:           owner + "-generation",
			Contracts:            []connector.ConnectorPushDurableMetadataContract{lead, contact},
			LockedPackInstallIDs: map[string]string{"connector_a": "install-1"},
			Mappings: []connector.ConnectorFieldMappingInput{{
				Source: owner, SourceObject: contact.SourceObject, TargetObject: contact.TargetObject,
				ActionType: contact.ActionType, Direction: connector.DirectionOutbound,
				SourceField: "Email", TargetField: "email",
			}},
		}
	}
	resolver := &connectorInboundInstalledBatchResolverStub{sets: sets}
	exec := connectorInboundInstalledBindingsExecutor(nil)
	exec.SetConnectorInboundRemoteIdentityInstalledContractResolver(resolver)
	admissions, err := exec.resolveConnectorRemoteIdentityTransferAdmissions(
		context.Background(),
		ConnectorRemoteIdentityTransfer{
			SourceObject: "Lead", SourceRecordId: "00Q-source",
			TargetObject: "Contact", TargetRecordId: "003-target",
			RemoteIDField: "HubSpot_Id__c",
		},
		[]ConnectorRemoteIdentityTransferHint{
			{ConnectorOwnerKey: "owner-z", ConnectorNamespace: "z", RemoteID: "remote-1", DestinationSlotID: "hubspot-contact"},
			{ConnectorOwnerKey: "owner-a", ConnectorNamespace: "a", RemoteID: "remote-1", DestinationSlotID: "hubspot-contact"},
		},
	)
	if err != nil {
		t.Fatalf("resolveConnectorRemoteIdentityTransferAdmissions: %v", err)
	}
	if len(admissions) != 2 || strings.Join(resolver.batchOwners, ",") != "owner-a,owner-z" || resolver.singleLocked != 0 {
		t.Fatalf("admissions/batch/single = %d/%v/%d", len(admissions), resolver.batchOwners, resolver.singleLocked)
	}
}

func TestFlowExecutorRecoveryBindingsRejectProfileInstallIncarnationDrift(t *testing.T) {
	contract := connectorInboundInstalledTestContract("Lead", "HubSpot_Lead_Id__c", "lead-push")
	exec := connectorInboundInstalledBindingsExecutor([]connector.ConnectorPushDurableMetadataContract{contract})
	resolver := exec.connectorInboundInstalled.(connectorInboundInstalledContractResolverStub)
	resolver.set.LockedPackInstallIDs = map[string]string{"connector_a": "install-replaced"}
	exec.SetConnectorInboundRemoteIdentityInstalledContractResolver(resolver)
	_, err := exec.ResolveConnectorInboundRemoteIdentityRecoveryInstalledBindings(context.Background(), ConnectorInboundRemoteIdentityRecoverySelector{
		ConnectorOwnerKey: "connector_a", ConnectorNamespace: "hubspot-routing",
		DestinationSlotID: "hubspot-contact", RemoteID: "remote-1",
	})
	if err == nil || !strings.Contains(err.Error(), "incarnation changed") {
		t.Fatalf("profile install drift error = %v", err)
	}
}

func connectorInboundInstalledBindingsExecutor(contracts []connector.ConnectorPushDurableMetadataContract) *FlowExecutor {
	mappings := make([]connector.ConnectorFieldMappingInput, 0, len(contracts))
	for _, contract := range contracts {
		mappings = append(mappings, connector.ConnectorFieldMappingInput{
			Source:       contract.ConnectorOwnerKey,
			SourceObject: contract.SourceObject,
			TargetObject: contract.TargetObject,
			ActionType:   contract.ActionType,
			Direction:    connector.DirectionOutbound,
			SourceField:  "Email",
			TargetField:  "email",
		})
	}
	exec := &FlowExecutor{
		connectorPushProfiles: staticPushProfileResolver{profile: &ConnectorPushProfile{
			OwnerPack: "connector_a", ProfileName: "default", CreatedPackInstallID: "install-1",
			CredentialRef: "nc-main", EndpointClass: "crm.objects.write",
			RetryPolicy: ConnectorPushRetryPolicy{MaxAttempts: 1},
		}},
	}
	exec.SetConnectorInboundRemoteIdentityInstalledContractResolver(connectorInboundInstalledContractResolverStub{set: ConnectorInboundRemoteIdentityInstalledContractSet{
		Generation: "generation-1", Contracts: contracts, Mappings: mappings,
		LockedPackInstallIDs: map[string]string{"connector_a": "install-1"},
	}})
	return exec
}

func connectorInboundInstalledTestContract(sourceObject, remoteIDField, actionSlotID string) connector.ConnectorPushDurableMetadataContract {
	return connector.ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey:  "connector_a",
		ActionSlotID:       actionSlotID,
		ActionType:         constants.ActionTypePushToConnector,
		FlowDeveloperName:  "Connector_Push_" + sourceObject,
		FlowActionName:     "Push",
		SourceObject:       sourceObject,
		DestinationSlotID:  "hubspot-contact",
		TargetObject:       "remoteContact",
		ProfileName:        "default",
		CreatePathTemplate: "/objects",
		UpdatePathTemplate: "/objects/{remoteId}",
		ResponseIDPath:     "id",
		RemoteIDField:      remoteIDField,
	}
}
