package automation

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	connector "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
)

type staticPushProfileResolver struct {
	profile *ConnectorPushProfile
	err     error
	capture func(requesterOwnerPack, ownerPack, profileName string)
}

func (r staticPushProfileResolver) ResolveConnectorPushProfile(_ context.Context, requesterOwnerPack, ownerPack, profileName string) (*ConnectorPushProfile, error) {
	if r.capture != nil {
		r.capture(requesterOwnerPack, ownerPack, profileName)
	}
	if r.err != nil {
		return nil, r.err
	}
	return r.profile, nil
}

type memoryConnectorPushDurableStore struct {
	ConnectorPushDurableStore
	links    map[string]*ConnectorPushRemoteLink
	attempts map[string]*ConnectorPushAttempt
}

func newMemoryConnectorPushDurableStore() *memoryConnectorPushDurableStore {
	return &memoryConnectorPushDurableStore{
		links:    map[string]*ConnectorPushRemoteLink{},
		attempts: map[string]*ConnectorPushAttempt{},
	}
}

func (s *memoryConnectorPushDurableStore) FindLink(_ context.Context, linkKey string) (*ConnectorPushRemoteLink, error) {
	return s.links[linkKey], nil
}

func (s *memoryConnectorPushDurableStore) FindFinalizedLink(_ context.Context, identity ConnectorPushDurableIdentity) (*ConnectorPushRemoteLink, error) {
	linkKey := mustConnectorPushTestLinkKey(identity)
	link := s.links[linkKey]
	if link == nil || link.Status != ConnectorPushLinkStatusFinalized {
		return nil, nil
	}
	return link, nil
}

func (s *memoryConnectorPushDurableStore) FindBlockingAttempt(_ context.Context, linkKey string) (*ConnectorPushAttempt, error) {
	for _, attempt := range s.attempts {
		if strings.TrimSpace(attempt.Snapshot.RequestMethod) != http.MethodPost {
			continue
		}
		if attempt.LinkKey == linkKey &&
			attempt.Status != ConnectorPushAttemptStatusFinalized &&
			attempt.Status != ConnectorPushAttemptStatusTerminalFailed &&
			attempt.Status != ConnectorPushAttemptStatusAmbiguousRemoteEffect &&
			attempt.Status != ConnectorPushAttemptStatusSuperseded {
			return attempt, nil
		}
	}
	return nil, nil
}

type mapFieldReadAuthorizer struct {
	allowed map[string]bool
}

func (a mapFieldReadAuthorizer) CheckFieldVisibilityWithUser(ctx context.Context, objectAPIName, fieldAPIName string, _ *models.UserSession) (bool, error) {
	_ = constants.GetSecurityContext(ctx)
	if a.allowed == nil {
		return false, nil
	}
	return a.allowed[strings.ToLower(objectAPIName+"."+fieldAPIName)], nil
}

type staticRecordReadAuthorizer struct {
	allowed bool
}

func (a staticRecordReadAuthorizer) CheckRecordReadAccessWithUser(context.Context, string, models.SObject, *models.UserSession) bool {
	return a.allowed
}

type staticCommittedSourceRecordLoader struct {
	records map[string]models.SObject
	err     error
}

func (l staticCommittedSourceRecordLoader) load(_ context.Context, objectName string, id string) (models.SObject, error) {
	if l.err != nil {
		return nil, l.err
	}
	key := strings.ToLower(strings.TrimSpace(objectName)) + "/" + strings.TrimSpace(id)
	record := l.records[key]
	if record == nil {
		return nil, fmt.Errorf("missing committed source row %s", key)
	}
	return record, nil
}

type pushDetachedCtxKey struct{}

func defaultPushConnectorMappings() []connector.ConnectorFieldMappingInput {
	return []connector.ConnectorFieldMappingInput{{
		Source:       "connector_a",
		SourceObject: "Contact",
		TargetObject: "remoteContact",
		ActionType:   constants.ActionTypePushToConnector,
		Direction:    connector.DirectionOutbound,
		SourceField:  "Email",
		TargetField:  "email",
	}}
}

func defaultPushConnectorContract() *connector.ConnectorPushDurableMetadataContract {
	return pushConnectorContract("connector_a", "connector_a", "default", "remoteContact")
}

func pushConnectorContract(ownerPack, profileOwnerPack, profileName, targetObject string) *connector.ConnectorPushDurableMetadataContract {
	return &connector.ConnectorPushDurableMetadataContract{
		ConnectorOwnerKey:  ownerPack,
		ActionSlotID:       "contact-remote-sync",
		ActionType:         constants.ActionTypePushToConnector,
		FlowDeveloperName:  "Connector_Push",
		FlowActionName:     "Push",
		SourceObject:       "Contact",
		DestinationSlotID:  "remote-contact",
		ProfileOwnerPack:   profileOwnerPack,
		ProfileName:        profileName,
		TargetObject:       targetObject,
		CreatePathTemplate: "/objects",
		UpdatePathTemplate: "/objects/{remoteId}",
		ResponseIDPath:     "id",
		RemoteIDField:      "RemoteKey__c",
	}
}

func pushConnectorContractWithBodyRoot(bodyRoot string) *connector.ConnectorPushDurableMetadataContract {
	contract := defaultPushConnectorContract()
	contract.BodyRoot = bodyRoot
	return contract
}

func pushConnectorContractWithAssociation() *connector.ConnectorPushDurableMetadataContract {
	contract := pushConnectorContractWithBodyRoot("properties")
	contract.Associations = []connector.ConnectorPushAssociationContract{{
		SourceLookupField:       "ParentRef__c",
		SourceObject:            "account_parent",
		RemoteIDField:           "RemoteKey__c",
		TargetActionSlotID:      "account-remote-sync",
		TargetDestinationSlotID: "remote-account",
		TargetObject:            "remoteRelated",
		PayloadField:            "links",
		PayloadMergeMode:        "append",
		InlinePhases:            []string{"create"},
		PayloadTemplate: map[string]interface{}{
			"to": map[string]interface{}{"id": "{toId}"},
			"types": []interface{}{
				map[string]interface{}{
					"linkCategory": "REMOTE_LINK",
					"linkTypeCode": float64(7),
				},
			},
		},
		AssociationPath:   "/link-api/{fromObject}/{fromId}/to/{toObject}/{toId}",
		AssociationMethod: http.MethodPut,
		CalloutPhases:     []string{"update"},
		CalloutBodyTemplate: []interface{}{
			map[string]interface{}{
				"linkCategory": "REMOTE_LINK",
				"linkTypeCode": float64(7),
			},
		},
	}}
	return contract
}

func formulaEngineForPushTests() *formula.Engine {
	return formula.NewEngine()
}

func pushConnectorContext(identity FlowExecutionIdentity) context.Context {
	frc := flowRuntimeContextFromCtx(context.Background()).WithCurrentFlow(identity)
	frc.SetObjectType("Contact")
	ctx := withFlowRuntimeContext(context.Background(), frc)
	return context.WithValue(
		ctx,
		constants.ContextKeySessionID,
		"session-test",
	)
}

func packOwnedPushConnectorContext() context.Context {
	return packOwnedPushConnectorContextForRecord("003-test")
}

func packOwnedPushConnectorContextForRecord(recordID string) context.Context {
	ctx := pushConnectorContext(FlowExecutionIdentity{
		APIName:       "Connector_Push",
		DefinitionID:  "flow-def",
		VersionID:     "flow-version",
		OwnerPack:     "connector_a",
		VersionSource: flowSourceStandard,
	})
	ctx = withRecordTriggerRootRecordID(ctx, recordID)
	return pushConnectorContextWithTriggerSnapshot(ctx, models.SObject{
		constants.FieldID: recordID,
		"Email":           "a@example.com",
		"ParentRef__c":    "001-test",
		"RemoteKey__c":    "",
		"HiddenPath__c":   "/objects/{remoteId}",
		"FirstName":       "Ada",
	})
}

func pushConnectorContextWithTriggerSnapshot(ctx context.Context, record models.SObject) context.Context {
	frc := flowRuntimeContextFromCtx(ctx).WithRecordTriggerNewSnapshot(record)
	return withFlowRuntimeContext(ctx, frc)
}

func pushConnectorAfterSaveFlowMeta() *models.FlowMetadata {
	return &models.FlowMetadata{Start: &models.FlowStart{
		TriggerType: "RecordAfterSave",
		Object:      "Contact",
	}}
}

func pushConnectorAction(params map[string]interface{}) *models.FlowActionCall {
	if _, ok := params["destinationSlotId"]; !ok {
		params["destinationSlotId"] = "remote-contact"
	}
	inputs := make([]models.FlowActionParameter, 0, len(params))
	for name, value := range params {
		inputs = append(inputs, models.FlowActionParameter{Name: name, Value: value})
	}
	return &models.FlowActionCall{
		Name:            "Push",
		ActionType:      constants.ActionTypePushToConnector,
		InputParameters: inputs,
		Connector:       &models.FlowConnector{TargetReference: "done"},
	}
}

func pushConnectorInterview(record models.SObject) *models.FlowInterview {
	if record != nil {
		if id, _ := record[constants.FieldID].(string); strings.TrimSpace(id) == "" {
			record[constants.FieldID] = "003-test"
		}
	}
	return &models.FlowInterview{
		Id:          "interview-1",
		ContextData: map[string]interface{}{"$Record": record},
	}
}

func TestConnectorPushContractBodyFingerprintIgnoresDerivedAssociationTargetPreviousSlots(t *testing.T) {
	contract := pushConnectorContractWithAssociation()
	withDerivedLineage := pushConnectorContractWithAssociation()
	withDerivedLineage.Associations[0].TargetPreviousActionSlotIDs = []string{"account-remote-sync-v1", "account-remote-sync-v0"}
	withDerivedLineage.Associations[0].TargetProfileOwnerPack = "connector-a"
	withDerivedLineage.Associations[0].TargetProfileName = "default"

	got := ConnectorPushContractBodyFingerprint(withDerivedLineage, defaultPushConnectorMappings())
	want := ConnectorPushContractBodyFingerprint(contract, defaultPushConnectorMappings())
	if got != want {
		t.Fatalf("BodyFingerprint changed for derived association fields: got %q want %q", got, want)
	}

	authoredChange := pushConnectorContractWithAssociation()
	authoredChange.Associations[0].PayloadField = "otherLinks"
	if ConnectorPushContractBodyFingerprint(authoredChange, defaultPushConnectorMappings()) == want {
		t.Fatal("BodyFingerprint ignored source-authored association payload behavior")
	}
}

func TestConnectorPushCreateConflictResolutionFingerprintIncludesNoOldSnapshotPolicy(t *testing.T) {
	policy := connectorPushTestCreateConflictResolution()
	withoutFallback := ConnectorPushCreateConflictResolutionFingerprint(policy)
	policy.NoOldSnapshotPolicy = connector.ConnectorPushConflictNoOldSnapshotAdoptLinkThenPatchAuthorizedFrozenCreatePayload
	withFallback := ConnectorPushCreateConflictResolutionFingerprint(policy)
	if withFallback == withoutFallback {
		t.Fatal("create conflict fingerprint ignored noOldSnapshotPolicy")
	}
}

func TestConnectorPushSameOriginWritePolicyAllowPatchExistingDoesNotSuppress(t *testing.T) {
	ctx := context.WithValue(packOwnedPushConnectorContext(), constants.ContextKeyConnectorSyncOrigin, "connector_a")
	contract := defaultPushConnectorContract()
	contract.SameOriginWritePolicy = connector.ConnectorPushSameOriginWritePolicySuppress
	if !connectorSyncOriginMatchesPushToConnectorContract(ctx, "connector_a", contract) {
		t.Fatal("default same-origin policy should suppress connector echo")
	}

	contract.SameOriginWritePolicy = connector.ConnectorPushSameOriginWritePolicyAllowPatchExisting
	if !connectorSyncOriginMatchesPushToConnectorContract(ctx, "connector_a", contract) {
		t.Fatal("allowPatchExisting still requires same-origin contract match")
	}
	if !connectorPushAllowsSameOriginPatchExisting(contract) {
		t.Fatal("allowPatchExisting policy should enable same-origin patch path")
	}
}

func TestPushToConnectorPayloadSourceRecordAliasesOnlyPayloadSourceFields(t *testing.T) {
	exec := &FlowExecutor{
		objectSchemaLookup: func(_ context.Context, objectAPIName string) *models.ObjectMetadata {
			if objectAPIName != "Contact" {
				return nil
			}
			return &models.ObjectMetadata{
				APIName: "Contact",
				Fields: []models.FieldMetadata{
					{APIName: "HubSpot_Owner_Id__c", PhysicalName: "hubspot_owner_id__c"},
					{APIName: "RemoteKey__c", PhysicalName: "remote_key__c"},
				},
			}
		},
	}
	raw := models.SObject{
		constants.FieldID:     "003-test",
		"hubspot_owner_id__c": "25458948",
		"remote_key__c":       "remote-123",
	}

	record, err := exec.pushToConnectorPayloadSourceRecord(context.Background(), "Contact", raw, []string{"HubSpot_Owner_Id__c"})
	if err != nil {
		t.Fatalf("pushToConnectorPayloadSourceRecord: %v", err)
	}
	if record["HubSpot_Owner_Id__c"] != "25458948" {
		t.Fatalf("HubSpot_Owner_Id__c = %#v, want physical owner alias", record["HubSpot_Owner_Id__c"])
	}
	if _, ok := record["RemoteKey__c"]; ok {
		t.Fatalf("RemoteKey__c was aliased even though it is not a payload source field: %#v", record)
	}
	if _, ok := raw["HubSpot_Owner_Id__c"]; ok {
		t.Fatalf("raw committed row was mutated by payload aliasing: %#v", raw)
	}
}

type capturePushMirrorPersistence struct {
	*stubPersistenceCapture
	calls              []pushMirrorPersistenceCall
	err                error
	denyUserContextFor string
	updated            bool
}

type pushMirrorPersistenceCall struct {
	objectName          string
	id                  string
	updates             models.SObject
	detached            bool
	securityContext     constants.SecurityContext
	standardActionID    string
	ownsRootFlow        bool
	internalScopes      bool
	scopedInternalEdit  bool
	protectedFlowMarker bool
	connectorSyncOrigin string
	suppressedRow       string
	userID              string
	userName            string
	userEmail           string
}

func (p *capturePushMirrorPersistence) Update(ctx context.Context, objectName string, id string, updates models.SObject, user *models.UserSession) error {
	detached, _ := ctx.Value(pushDetachedCtxKey{}).(bool)
	origin, _ := ctx.Value(constants.ContextKeyConnectorSyncOrigin).(string)
	suppressed, _ := ctx.Value(constants.ContextKeySuppressRecordTriggeredAutomation).(string)
	userID := ""
	userName := ""
	userEmail := ""
	if user != nil {
		userID = strings.TrimSpace(user.ID)
		userName = strings.TrimSpace(user.Name)
		if user.Email != nil {
			userEmail = strings.TrimSpace(*user.Email)
		}
	}
	p.calls = append(p.calls, pushMirrorPersistenceCall{
		objectName:          objectName,
		id:                  id,
		updates:             updates,
		detached:            detached,
		securityContext:     constants.GetSecurityContext(ctx),
		standardActionID:    constants.StandardActionExecutionActionID(ctx),
		ownsRootFlow:        constants.StandardActionExecutionOwnsRootFlow(ctx, "Std_PushToConnectorMirror"),
		internalScopes:      constants.HasStandardActionInternalAccessScopes(ctx),
		scopedInternalEdit:  constants.IsScopedInternalAccess(ctx, objectName, constants.PermEdit),
		protectedFlowMarker: constants.HasStandardActionProtectedFlowExecution(ctx),
		connectorSyncOrigin: origin,
		suppressedRow:       suppressed,
		userID:              userID,
		userName:            userName,
		userEmail:           userEmail,
	})
	if p.err != nil {
		return p.err
	}
	if p.denyUserContextFor != "" && userID == p.denyUserContextFor && constants.GetSecurityContext(ctx) == constants.UserContext {
		return errors.New("user-context authorization denied")
	}
	p.updated = true
	return nil
}

func (p *capturePushMirrorPersistence) DetachTx(ctx context.Context) context.Context {
	return context.WithValue(ctx, pushDetachedCtxKey{}, true)
}

func connectorPushTestCreateConflictResolution() *connector.ConnectorPushCreateConflictResolutionContract {
	return &connector.ConnectorPushCreateConflictResolutionContract{
		StatusCodes:     []int{http.StatusConflict},
		DiagnosticMatch: map[string]string{"category": "CONFLICT"},
		Identity: connector.ConnectorPushConflictIdentityContract{
			RequestBodyPath: "email",
			Normalization:   connector.ConnectorPushConflictNormalizationEmail,
		},
		Resolve: connector.ConnectorPushConflictResolverContract{
			Method:            http.MethodGet,
			PathTemplate:      "/objects/{identity}?idProperty=email",
			ResponseIDPath:    "id",
			RequireExactlyOne: true,
		},
		OnResolved: connector.ConnectorPushConflictOnResolvedAdoptLinkThenPatch,
	}
}
