package automation

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

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

const (
	recoveryActionType      = "recoverConnectorRemoteIdentity"
	recoveryActionOwnerPack = "PingCAP-HubSpot-Companion"
	recoveryActionNamespace = "hubspot-crm-connector"
	recoveryActionFlowName  = "HubSpot_Workflow_Action_CampaignMember_Upsert"
	recoveryActionVersionID = "flow-version-recovery"
)

type recoveryActionPersistenceStub struct {
	PersistenceProvider
	active     bool
	locked     models.SObject
	lockErr    error
	lockCalls  int
	lockObject string
	lockID     string
}

func (s *recoveryActionPersistenceStub) IsTransactionActive(context.Context) bool {
	return s != nil && s.active
}

func (s *recoveryActionPersistenceStub) GetLock(_ context.Context, objectName, recordID string) (models.SObject, error) {
	s.lockCalls++
	s.lockObject = objectName
	s.lockID = recordID
	return s.locked, s.lockErr
}

type recoveryActionRecovererStub struct {
	result  *ConnectorInboundRemoteIdentityRecoveryResult
	err     error
	calls   int
	request ConnectorInboundRemoteIdentityRecoveryRequest
}

func (s *recoveryActionRecovererStub) Recover(_ context.Context, request ConnectorInboundRemoteIdentityRecoveryRequest) (*ConnectorInboundRemoteIdentityRecoveryResult, error) {
	s.calls++
	s.request = request
	return s.result, s.err
}

type recoveryActionLockedRecordValidatorStub struct {
	status ConnectorPushCompatibilityMirrorStatus
	err    error
	calls  int
	input  ConnectorInboundRemoteIdentityRecoveryLockedRecordInput
}

func (s *recoveryActionLockedRecordValidatorStub) ValidateConnectorInboundRemoteIdentityRecoveryLockedRecord(
	input ConnectorInboundRemoteIdentityRecoveryLockedRecordInput,
) (ConnectorPushCompatibilityMirrorStatus, error) {
	s.calls++
	s.input = input
	return s.status, s.err
}

func TestRecoverConnectorRemoteIdentityActionBuildsSelectorOnlyRequest(t *testing.T) {
	for _, test := range []struct {
		name       string
		ctx        context.Context
		result     *ConnectorInboundRemoteIdentityRecoveryResult
		wantState  string
		wantOwner  string
		wantOrigin string
	}{
		{
			name:       "authenticated root recovered",
			ctx:        recoveryActionRootContext(),
			result:     recoveryActionRecoveredResult("evidence-root"),
			wantState:  ConnectorInboundRemoteIdentityRecoveryStateRecovered,
			wantOwner:  recoveryActionNamespace,
			wantOrigin: recoveryActionNamespace,
		},
		{
			name:       "exact companion recovered",
			ctx:        recoveryActionCompanionContext(recoveryActionVersionID),
			result:     recoveryActionRecoveredResult("evidence-1"),
			wantState:  ConnectorInboundRemoteIdentityRecoveryStateRecovered,
			wantOwner:  recoveryActionOwnerPack,
			wantOrigin: recoveryActionNamespace,
		},
		{
			name:       "protected standard action not found",
			ctx:        recoveryActionProtectedStandardContext(),
			wantState:  ConnectorInboundRemoteIdentityRecoveryStateNotFound,
			wantOwner:  recoveryActionOwnerPack,
			wantOrigin: recoveryActionNamespace,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			stub := &recoveryActionRecovererStub{result: test.result}
			executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{}}
			interview := recoveryActionInterview()
			next, err := executor.executeRecoverConnectorRemoteIdentityWithFactory(
				test.ctx,
				interview,
				recoveryActionCall(),
				nil,
				recoveryActionType,
				func() (connectorInboundRemoteIdentityRecoverer, error) { return stub, nil },
			)
			if err != nil {
				t.Fatalf("execute recovery action: %v", err)
			}
			if next != "RouteDurableRecovery" {
				t.Fatalf("next = %q, want RouteDurableRecovery", next)
			}
			if stub.calls != 1 {
				t.Fatalf("Recover calls = %d, want 1", stub.calls)
			}
			selector := stub.request.Selector
			if selector.ConnectorOwnerKey != test.wantOwner || selector.ConnectorNamespace != test.wantOrigin ||
				selector.DestinationSlotID != "hubspot-contact" || selector.RemoteID != "12345" {
				t.Fatalf("selector = %#v", selector)
			}
			if got := interview.ContextData["durableRecoveryState"]; got != test.wantState {
				t.Fatalf("durableRecoveryState = %#v, want %q", got, test.wantState)
			}
		})
	}
}

func TestRecoverConnectorRemoteIdentityActionRejectsUntrustedExecutionBeforeFactory(t *testing.T) {
	valid := recoveryActionCompanionContext(recoveryActionVersionID)
	for _, test := range []struct {
		name    string
		ctx     context.Context
		active  bool
		wantErr string
	}{
		{name: "missing authenticated execution marker", ctx: recoveryActionFlowContext(context.Background()), wantErr: "requires the authenticated connector root"},
		{name: "root marker cannot authorize foreign owner pack", ctx: recoveryActionForeignOwnerRootContext(), wantErr: "requires the authenticated connector root"},
		{name: "root marker has wrong flow", ctx: recoveryActionRootContextWithMarker("Other_Inbound_Flow", recoveryActionNamespace), wantErr: "requires the authenticated connector root"},
		{name: "root marker has wrong namespace", ctx: recoveryActionRootContextWithMarker(recoveryActionFlowName, "other-connector"), wantErr: "requires the authenticated connector root"},
		{name: "wrong companion version", ctx: recoveryActionCompanionContext("other-version"), wantErr: "requires the authenticated connector root"},
		{name: "missing namespace", ctx: recoveryActionContextWithoutNamespace(), wantErr: "namespace is required"},
		{name: "preview", ctx: WithPreviewMode(valid), wantErr: "preview execution is not allowed"},
		{name: "atomic marker", ctx: withAtomicFlowBoundary(valid), wantErr: "atomic Flow execution is not allowed"},
		{name: "ambient transaction", ctx: valid, active: true, wantErr: "ambient transaction is not allowed"},
	} {
		t.Run(test.name, func(t *testing.T) {
			factoryCalls := 0
			executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{active: test.active}}
			_, err := executor.executeRecoverConnectorRemoteIdentityWithFactory(
				test.ctx,
				recoveryActionInterview(),
				recoveryActionCall(),
				nil,
				recoveryActionType,
				func() (connectorInboundRemoteIdentityRecoverer, error) {
					factoryCalls++
					return &recoveryActionRecovererStub{}, nil
				},
			)
			if err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Fatalf("error = %v, want containing %q", err, test.wantErr)
			}
			if factoryCalls != 0 {
				t.Fatalf("factory calls = %d, want 0", factoryCalls)
			}
		})
	}
}

func TestRecoverConnectorRemoteIdentityActionRejectsAmbientTransactionBeforeDynamicResolutionOrFactory(t *testing.T) {
	resolverCalls := 0
	factoryCalls := 0
	executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{active: true}}
	_, err := executor.executeRecoverConnectorRemoteIdentityWithDependencies(
		recoveryActionCompanionContext(recoveryActionVersionID),
		recoveryActionInterview(),
		recoveryActionCall(),
		nil,
		recoveryActionType,
		func() (connectorInboundRemoteIdentityRecoverer, error) {
			factoryCalls++
			return &recoveryActionRecovererStub{}, nil
		},
		func(interface{}) (interface{}, error) {
			resolverCalls++
			return "12345", nil
		},
	)
	if err == nil || !strings.Contains(err.Error(), "ambient transaction is not allowed") {
		t.Fatalf("error = %v, want ambient transaction rejection", err)
	}
	if resolverCalls != 0 || factoryCalls != 0 {
		t.Fatalf("dynamic resolver/factory calls = %d/%d, want 0/0", resolverCalls, factoryCalls)
	}
}

func TestRecoverConnectorRemoteIdentityActionRejectsAuthorityShapedInputs(t *testing.T) {
	for _, test := range []struct {
		name    string
		mutate  func(*models.FlowActionCall, *models.FlowInterview)
		wantErr string
	}{
		{
			name: "caller owner",
			mutate: func(action *models.FlowActionCall, _ *models.FlowInterview) {
				action.InputParameters = append(action.InputParameters, models.FlowActionParameter{Name: "connectorOwnerKey", Value: map[string]interface{}{"stringValue": "attacker"}})
			},
			wantErr: "exactly destinationSlotId and remoteId",
		},
		{
			name: "dynamic destination",
			mutate: func(action *models.FlowActionCall, _ *models.FlowInterview) {
				action.InputParameters[0].Value = map[string]interface{}{"elementReference": "destination"}
			},
			wantErr: "pack-declared stringValue literal",
		},
		{
			name: "literal remote id",
			mutate: func(action *models.FlowActionCall, _ *models.FlowInterview) {
				action.InputParameters[1].Value = map[string]interface{}{"stringValue": "12345"}
			},
			wantErr: "dynamic elementReference",
		},
		{
			name: "blank resolved remote id",
			mutate: func(_ *models.FlowActionCall, interview *models.FlowInterview) {
				interview.ContextData["resolvedHubSpotId"] = ""
			},
			wantErr: "canonical nonblank string",
		},
		{
			name: "wrong action type",
			mutate: func(action *models.FlowActionCall, _ *models.FlowInterview) {
				action.ActionType = "otherAction"
			},
			wantErr: "action type must be",
		},
		{
			name: "extra output",
			mutate: func(action *models.FlowActionCall, _ *models.FlowInterview) {
				action.OutputParameters = append(action.OutputParameters, models.FlowActionParameter{Name: "sourceRecordId", Value: map[string]interface{}{"elementReference": "leaked"}})
			},
			wantErr: "exactly one recoveryState output",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			action := recoveryActionCall()
			interview := recoveryActionInterview()
			test.mutate(action, interview)
			factoryCalls := 0
			executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{}}
			_, err := executor.executeRecoverConnectorRemoteIdentityWithFactory(
				recoveryActionCompanionContext(recoveryActionVersionID),
				interview,
				action,
				nil,
				recoveryActionType,
				func() (connectorInboundRemoteIdentityRecoverer, error) {
					factoryCalls++
					return &recoveryActionRecovererStub{}, nil
				},
			)
			if err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Fatalf("error = %v, want containing %q", err, test.wantErr)
			}
			if factoryCalls != 0 {
				t.Fatalf("factory calls = %d, want 0", factoryCalls)
			}
		})
	}
}

func TestRecoverConnectorRemoteIdentityActionAllowsIndependentElementAndActionName(t *testing.T) {
	action := recoveryActionCall()
	action.Name = "Element_With_An_Independent_Name"
	action.ActionName = "Installed_Action_Alias"
	stub := &recoveryActionRecovererStub{}
	executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{}}
	_, err := executor.executeRecoverConnectorRemoteIdentityWithFactory(
		recoveryActionCompanionContext(recoveryActionVersionID),
		recoveryActionInterview(),
		action,
		nil,
		recoveryActionType,
		func() (connectorInboundRemoteIdentityRecoverer, error) { return stub, nil },
	)
	if err != nil {
		t.Fatalf("execute action with independent names: %v", err)
	}
	if stub.calls != 1 {
		t.Fatalf("Recover calls = %d, want 1", stub.calls)
	}
}

func TestRecoverConnectorRemoteIdentityActionMatchesNormalizedDescriptorType(t *testing.T) {
	stub := &recoveryActionRecovererStub{}
	executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{}}
	_, err := executor.executeRecoverConnectorRemoteIdentityWithFactory(
		recoveryActionCompanionContext(recoveryActionVersionID),
		recoveryActionInterview(),
		recoveryActionCall(),
		nil,
		normalizeFlowActionType(recoveryActionType),
		func() (connectorInboundRemoteIdentityRecoverer, error) { return stub, nil },
	)
	if err != nil {
		t.Fatalf("execute action from normalized descriptor: %v", err)
	}
	if stub.calls != 1 {
		t.Fatalf("Recover calls = %d, want 1", stub.calls)
	}
}

func TestRecoverConnectorRemoteIdentityActionRejectsIncompleteRecoveredResult(t *testing.T) {
	stub := &recoveryActionRecovererStub{result: &ConnectorInboundRemoteIdentityRecoveryResult{EvidenceID: "evidence-1"}}
	executor := &FlowExecutor{persistence: &recoveryActionPersistenceStub{}}
	interview := recoveryActionInterview()
	_, err := executor.executeRecoverConnectorRemoteIdentityWithFactory(
		recoveryActionCompanionContext(recoveryActionVersionID),
		interview,
		recoveryActionCall(),
		nil,
		recoveryActionType,
		func() (connectorInboundRemoteIdentityRecoverer, error) { return stub, nil },
	)
	if err == nil || !strings.Contains(err.Error(), "incomplete recovered result") {
		t.Fatalf("error = %v, want incomplete recovered result", err)
	}
	if _, exists := interview.ContextData["durableRecoveryState"]; exists {
		t.Fatal("incomplete recovered result published recoveryState")
	}
}

func TestRecoverySourceValidatorLocksExactEvidenceOwnerBeforeProductValidation(t *testing.T) {
	locked := models.SObject{constants.FieldID: "00Q-lead", constants.FieldIsDeleted: false, "IsConverted": false, "HubSpot_Id__c": nil}
	persistence := &recoveryActionPersistenceStub{locked: locked}
	validator := &recoveryActionLockedRecordValidatorStub{status: ConnectorPushCompatibilityMirrorDrifted}
	sourceValidator := connectorInboundRemoteIdentityRecoverySourceRecordValidator{
		persistence: persistence,
		validator:   validator,
	}
	result, err := sourceValidator.ValidateConnectorInboundRemoteIdentityRecoverySource(
		context.Background(),
		ConnectorInboundRemoteIDEvidence{
			SourceObject:   "Lead",
			SourceRecordId: "00Q-lead",
			RemoteIDField:  "HubSpot_Id__c",
			RemoteId:       "12345",
		},
		ConnectorInboundRemoteIdentityRecoveryBinding{},
	)
	if err != nil {
		t.Fatalf("Validate source: %v", err)
	}
	if persistence.lockCalls != 1 || persistence.lockObject != "Lead" || persistence.lockID != "00Q-lead" {
		t.Fatalf("GetLock calls=%d object=%q id=%q", persistence.lockCalls, persistence.lockObject, persistence.lockID)
	}
	if validator.calls != 1 || validator.input.RecordID != "00Q-lead" || validator.input.Record[constants.FieldID] != "00Q-lead" {
		t.Fatalf("validator calls=%d input=%#v", validator.calls, validator.input)
	}
	if result.CompatibilityMirrorStatus != ConnectorPushCompatibilityMirrorDrifted {
		t.Fatalf("mirror status = %q", result.CompatibilityMirrorStatus)
	}
}

func TestRecoverySourceValidatorFailsBeforeProductValidationWhenLockFails(t *testing.T) {
	for _, test := range []struct {
		name    string
		locked  models.SObject
		lockErr error
		wantErr string
	}{
		{name: "missing", wantErr: "not found"},
		{name: "lock error", lockErr: errors.New("lock failed"), wantErr: "lock failed"},
	} {
		t.Run(test.name, func(t *testing.T) {
			validator := &recoveryActionLockedRecordValidatorStub{}
			sourceValidator := connectorInboundRemoteIdentityRecoverySourceRecordValidator{
				persistence: &recoveryActionPersistenceStub{locked: test.locked, lockErr: test.lockErr},
				validator:   validator,
			}
			_, err := sourceValidator.ValidateConnectorInboundRemoteIdentityRecoverySource(
				context.Background(),
				ConnectorInboundRemoteIDEvidence{SourceObject: "Lead", SourceRecordId: "00Q-lead"},
				ConnectorInboundRemoteIdentityRecoveryBinding{},
			)
			if err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Fatalf("error = %v, want containing %q", err, test.wantErr)
			}
			if validator.calls != 0 {
				t.Fatalf("validator calls = %d, want 0", validator.calls)
			}
		})
	}
}

func recoveryActionCall() *models.FlowActionCall {
	return &models.FlowActionCall{
		Name:       "RecoverDurableRemoteIdentity",
		ActionName: recoveryActionType,
		ActionType: recoveryActionType,
		InputParameters: []models.FlowActionParameter{
			{Name: "destinationSlotId", Value: map[string]interface{}{"stringValue": "hubspot-contact"}},
			{Name: "remoteId", Value: map[string]interface{}{"elementReference": "resolvedHubSpotId"}},
		},
		OutputParameters: []models.FlowActionParameter{
			{Name: "recoveryState", Value: map[string]interface{}{"elementReference": "durableRecoveryState"}},
		},
		Connector: &models.FlowConnector{TargetReference: "RouteDurableRecovery"},
	}
}

func recoveryActionInterview() *models.FlowInterview {
	return &models.FlowInterview{ContextData: map[string]interface{}{"resolvedHubSpotId": "12345"}}
}

func recoveryActionRecoveredResult(evidenceID string) *ConnectorInboundRemoteIdentityRecoveryResult {
	return &ConnectorInboundRemoteIdentityRecoveryResult{
		EvidenceID:     evidenceID,
		LinkKey:        "link-key-1",
		SourceObject:   "Lead",
		SourceRecordID: "00Q-lead",
	}
}

func recoveryActionFlowContext(base context.Context) context.Context {
	base = context.WithValue(base, constants.ContextKeyConnectorSyncOrigin, recoveryActionNamespace)
	return withFlowRuntimeContext(base, flowRuntimeContextFromCtx(base).WithCurrentFlow(FlowExecutionIdentity{
		APIName:       recoveryActionFlowName,
		DefinitionID:  "flow-definition-recovery",
		VersionID:     recoveryActionVersionID,
		OwnerPack:     recoveryActionOwnerPack,
		VersionSource: flowSourceStandard,
	}))
}

func recoveryActionCompanionContext(versionID string) context.Context {
	ctx := recoveryActionFlowContext(context.Background())
	return constants.WithConnectorInboundCompanionFlowExecution(ctx, versionID)
}

func recoveryActionRootContext() context.Context {
	return recoveryActionRootContextWithMarker(recoveryActionFlowName, recoveryActionNamespace)
}

func recoveryActionRootContextWithMarker(markerFlow, markerNamespace string) context.Context {
	ctx := context.WithValue(context.Background(), constants.ContextKeyConnectorSyncOrigin, recoveryActionNamespace)
	ctx = withFlowRuntimeContext(ctx, flowRuntimeContextFromCtx(ctx).WithCurrentFlow(FlowExecutionIdentity{
		APIName:       recoveryActionFlowName,
		DefinitionID:  "flow-definition-recovery-root",
		VersionID:     "flow-version-recovery-root",
		OwnerPack:     recoveryActionNamespace,
		VersionSource: flowSourceStandard,
	}))
	return constants.WithConnectorInboundFlowExecution(ctx, markerFlow, markerNamespace)
}

func recoveryActionForeignOwnerRootContext() context.Context {
	ctx := recoveryActionFlowContext(context.Background())
	return constants.WithConnectorInboundFlowExecution(ctx, recoveryActionFlowName, recoveryActionNamespace)
}

func recoveryActionProtectedStandardContext() context.Context {
	ctx := recoveryActionFlowContext(context.Background())
	ctx = constants.WithStandardActionExecutionRootFlow(ctx, "standard-action-recovery", recoveryActionFlowName)
	return constants.WithStandardActionProtectedFlowExecution(ctx, recoveryActionFlowName)
}

func recoveryActionContextWithoutNamespace() context.Context {
	ctx := withFlowRuntimeContext(context.Background(), flowRuntimeContextFromCtx(context.Background()).WithCurrentFlow(FlowExecutionIdentity{
		APIName:       recoveryActionFlowName,
		DefinitionID:  "flow-definition-recovery",
		VersionID:     recoveryActionVersionID,
		OwnerPack:     recoveryActionOwnerPack,
		VersionSource: flowSourceStandard,
	}))
	return constants.WithConnectorInboundCompanionFlowExecution(ctx, recoveryActionVersionID)
}
