package automation

import (
	"context"
	"errors"
	"testing"

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

func TestResolveConnectorRemoteIdentityRecordReadsActiveOwnerAndMirror(t *testing.T) {
	authority := connectorRemoteIdentityTestAuthority()
	store := &connectorRemoteIdentityAuthorityStoreStub{authority: authority}
	query := &connectorRemoteIdentityQueryStub{records: []models.SObject{{
		"Id":             authority.Identity.SourceRecordID,
		"IsDeleted":      false,
		"External_Id__c": authority.RemoteID,
	}}}

	resolution, err := resolveConnectorRemoteIdentityRecord(
		context.Background(), store, query, &models.UserSession{ID: "005-user"},
		ConnectorRemoteLocatorQuery{ConnectorOwnerKey: "owner-key", DestinationSlotID: "dest-slot", RemoteID: "remote-1"},
	)
	if err != nil {
		t.Fatalf("resolveConnectorRemoteIdentityRecord: %v", err)
	}
	if resolution.AuthorityState != ConnectorRemoteIdentityAuthorityFinalized ||
		resolution.OwnerLifecycle != ConnectorRemoteIdentityOwnerActive ||
		resolution.MirrorStatus != ConnectorPushCompatibilityMirrorSynced {
		t.Fatalf("resolution = %#v, want finalized/active/synced", resolution)
	}
	if len(query.calls) != 1 || !query.calls[0].ScanAll || query.calls[0].Limit != 2 {
		t.Fatalf("query calls = %#v, want one bounded ScanAll read", query.calls)
	}
	if len(store.readbacks) != 1 || store.readbacks[0].status != ConnectorPushCompatibilityMirrorSynced {
		t.Fatalf("readbacks = %#v, want synced", store.readbacks)
	}
}

func TestResolveConnectorRemoteIdentityRecordInvalidatesDriftedMirror(t *testing.T) {
	authority := connectorRemoteIdentityTestAuthority()
	store := &connectorRemoteIdentityAuthorityStoreStub{authority: authority}
	query := &connectorRemoteIdentityQueryStub{records: []models.SObject{{
		"Id":             authority.Identity.SourceRecordID,
		"IsDeleted":      false,
		"External_Id__c": "",
	}}}

	resolution, err := resolveConnectorRemoteIdentityRecord(context.Background(), store, query, &models.UserSession{}, ConnectorRemoteLocatorQuery{
		ConnectorOwnerKey: "owner-key", DestinationSlotID: "dest-slot", RemoteID: "remote-1",
	})
	if err != nil {
		t.Fatalf("resolveConnectorRemoteIdentityRecord: %v", err)
	}
	if resolution.OwnerLifecycle != ConnectorRemoteIdentityOwnerActive || resolution.MirrorStatus != ConnectorPushCompatibilityMirrorDrifted {
		t.Fatalf("resolution = %#v, want active/drifted", resolution)
	}
	if len(store.readbacks) != 1 || store.readbacks[0].status != ConnectorPushCompatibilityMirrorDrifted {
		t.Fatalf("readbacks = %#v, want drifted", store.readbacks)
	}
}

func TestResolveConnectorRemoteIdentityRecordReturnsDeletedOwner(t *testing.T) {
	authority := connectorRemoteIdentityTestAuthority()
	store := &connectorRemoteIdentityAuthorityStoreStub{authority: authority}
	query := &connectorRemoteIdentityQueryStub{records: []models.SObject{{
		"Id":             authority.Identity.SourceRecordID,
		"IsDeleted":      true,
		"External_Id__c": authority.RemoteID,
	}}}

	resolution, err := resolveConnectorRemoteIdentityRecord(context.Background(), store, query, &models.UserSession{}, ConnectorRemoteLocatorQuery{
		ConnectorOwnerKey: "owner-key", DestinationSlotID: "dest-slot", RemoteID: "remote-1",
	})
	if err != nil {
		t.Fatalf("resolveConnectorRemoteIdentityRecord: %v", err)
	}
	if resolution.OwnerLifecycle != ConnectorRemoteIdentityOwnerDeleted {
		t.Fatalf("owner lifecycle = %q, want deleted", resolution.OwnerLifecycle)
	}
}

func TestResolveConnectorRemoteIdentityRecordReturnsPendingRecoveryWithoutMirrorWriteback(t *testing.T) {
	authority := connectorRemoteIdentityTestAuthority()
	authority.LinkStatus = ConnectorPushLinkStatusOwnerDeletedPendingRecovery
	store := &connectorRemoteIdentityAuthorityStoreStub{authority: authority}

	resolution, err := resolveConnectorRemoteIdentityRecord(context.Background(), store, nil, &models.UserSession{}, ConnectorRemoteLocatorQuery{
		ConnectorOwnerKey: "owner-key", DestinationSlotID: "dest-slot", RemoteID: "remote-1",
	})
	if err != nil {
		t.Fatalf("resolveConnectorRemoteIdentityRecord: %v", err)
	}
	if resolution.AuthorityState != ConnectorRemoteIdentityAuthorityOwnerDeletedPendingRecovery ||
		resolution.OwnerLifecycle != ConnectorRemoteIdentityOwnerDeleted {
		t.Fatalf("resolution = %#v, want pending-recovery/deleted", resolution)
	}
	if len(store.readbacks) != 0 {
		t.Fatalf("mirror readbacks = %#v, want none for pending recovery", store.readbacks)
	}
}

func TestResolveConnectorRemoteIdentityRecordReturnsMissingWithoutFallbackAuthority(t *testing.T) {
	store := &connectorRemoteIdentityAuthorityStoreStub{}
	query := &connectorRemoteIdentityQueryStub{err: errors.New("must not query record without durable authority")}

	resolution, err := resolveConnectorRemoteIdentityRecord(context.Background(), store, query, &models.UserSession{}, ConnectorRemoteLocatorQuery{
		ConnectorOwnerKey: "owner-key", DestinationSlotID: "dest-slot", RemoteID: "missing",
	})
	if err != nil {
		t.Fatalf("resolveConnectorRemoteIdentityRecord: %v", err)
	}
	if resolution.AuthorityState != ConnectorRemoteIdentityAuthorityMissing || resolution.OwnerLifecycle != ConnectorRemoteIdentityOwnerMissing {
		t.Fatalf("resolution = %#v, want missing/missing", resolution)
	}
	if len(query.calls) != 0 {
		t.Fatalf("record query calls = %d, want 0", len(query.calls))
	}
}

func TestResolveConnectorRemoteIdentityRecordFailsClosedOnOrphanedAuthority(t *testing.T) {
	store := &connectorRemoteIdentityAuthorityStoreStub{authority: connectorRemoteIdentityTestAuthority()}
	query := &connectorRemoteIdentityQueryStub{}

	_, err := resolveConnectorRemoteIdentityRecord(context.Background(), store, query, &models.UserSession{}, ConnectorRemoteLocatorQuery{
		ConnectorOwnerKey: "owner-key", DestinationSlotID: "dest-slot", RemoteID: "remote-1",
	})
	if !errors.Is(err, ErrConnectorPushDurableConflict) {
		t.Fatalf("resolveConnectorRemoteIdentityRecord error = %v, want durable conflict", err)
	}
}

func TestExecuteResolveConnectorRemoteIdentityPublishesTypedOutputs(t *testing.T) {
	authorityStore := &connectorRemoteIdentityAuthorityStoreStub{authority: connectorRemoteIdentityTestAuthority()}
	store := &connectorRemoteIdentityActionStore{
		inboundRegisterDurableStore:               newInboundRegisterDurableStore(),
		connectorRemoteIdentityAuthorityStoreStub: authorityStore,
	}
	query := &connectorRemoteIdentityQueryStub{records: []models.SObject{{
		"Id":             "003-local",
		"IsDeleted":      false,
		"External_Id__c": "remote-1",
	}}}
	exec := &FlowExecutor{connectorPushDurable: store, query: query}
	interview := &models.FlowInterview{ContextData: map[string]interface{}{}}
	action := &models.FlowActionCall{
		ActionType: "resolveConnectorRemoteIdentity",
		InputParameters: []models.FlowActionParameter{
			{Name: "connectorOwnerKey", Value: "owner-key"},
			{Name: "destinationSlotId", Value: "dest-slot"},
			{Name: "remoteId", Value: "remote-1"},
		},
		OutputParameters: []models.FlowActionParameter{
			{Name: "authorityState", Value: map[string]interface{}{"elementReference": "authorityState"}},
			{Name: "sourceObject", Value: map[string]interface{}{"elementReference": "ownerObject"}},
			{Name: "sourceRecordId", Value: map[string]interface{}{"elementReference": "ownerId"}},
			{Name: "ownerLifecycle", Value: map[string]interface{}{"elementReference": "ownerLifecycle"}},
			{Name: "mirrorStatus", Value: map[string]interface{}{"elementReference": "mirrorStatus"}},
		},
		Connector: &models.FlowConnector{TargetReference: "Next"},
	}
	ctx := constants.WithStandardActionExecutionRootFlow(context.Background(), "standard-action-1", "Inbound_Upsert")
	ctx = constants.WithStandardActionProtectedFlowExecution(ctx, "Inbound_Upsert")
	ctx = withFlowRuntimeContext(ctx, flowRuntimeContextFromCtx(ctx).WithCurrentFlow(FlowExecutionIdentity{
		APIName: "Inbound_Upsert", OwnerPack: "owner-key", VersionSource: flowSourceStandard,
	}))

	next, err := exec.executeResolveConnectorRemoteIdentity(ctx, interview, action, &models.UserSession{ID: "005-user"})
	if err != nil {
		t.Fatalf("executeResolveConnectorRemoteIdentity: %v", err)
	}
	if next != "Next" {
		t.Fatalf("next = %q, want Next", next)
	}
	if interview.ContextData["authorityState"] != ConnectorRemoteIdentityAuthorityFinalized ||
		interview.ContextData["ownerObject"] != "Contact" ||
		interview.ContextData["ownerId"] != "003-local" ||
		interview.ContextData["ownerLifecycle"] != ConnectorRemoteIdentityOwnerActive ||
		interview.ContextData["mirrorStatus"] != ConnectorPushCompatibilityMirrorSynced {
		t.Fatalf("action outputs = %#v", interview.ContextData)
	}
}

func TestExecuteResolveConnectorRemoteIdentityAcceptsAuthorizedInboundRootAndRejectsOwnerSpoof(t *testing.T) {
	for _, tc := range []struct {
		name       string
		ownerParam string
		wantErr    bool
	}{
		{name: "authorized owner", ownerParam: "owner-key"},
		{name: "spoofed owner", ownerParam: "other-owner", wantErr: true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			authorityStore := &connectorRemoteIdentityAuthorityStoreStub{}
			store := &connectorRemoteIdentityActionStore{
				inboundRegisterDurableStore:               newInboundRegisterDurableStore(),
				connectorRemoteIdentityAuthorityStoreStub: authorityStore,
			}
			exec := &FlowExecutor{connectorPushDurable: store, query: &connectorRemoteIdentityQueryStub{}}
			action := &models.FlowActionCall{
				ActionType: "resolveConnectorRemoteIdentity",
				InputParameters: []models.FlowActionParameter{
					{Name: "connectorOwnerKey", Value: tc.ownerParam},
					{Name: "destinationSlotId", Value: "dest-slot"},
					{Name: "remoteId", Value: "remote-1"},
				},
			}
			ctx := constants.WithConnectorInboundFlowExecution(context.Background(), "Inbound_Upsert", "owner-key")
			ctx = withFlowRuntimeContext(ctx, flowRuntimeContextFromCtx(ctx).WithCurrentFlow(FlowExecutionIdentity{
				APIName: "Inbound_Upsert", OwnerPack: "owner-key", VersionSource: flowSourceStandard,
			}))
			_, err := exec.executeResolveConnectorRemoteIdentity(ctx, &models.FlowInterview{}, action, &models.UserSession{})
			if tc.wantErr && err == nil {
				t.Fatal("executeResolveConnectorRemoteIdentity error = nil, want owner-binding failure")
			}
			if !tc.wantErr && err != nil {
				t.Fatalf("executeResolveConnectorRemoteIdentity: %v", err)
			}
		})
	}
}

func TestExecuteResolveConnectorRemoteIdentityAcceptsExactAuthorizedCompanionVersion(t *testing.T) {
	for _, tc := range []struct {
		name                    string
		authorizedFlowVersionID string
		ownerParam              string
		wantErr                 bool
	}{
		{name: "exact companion version", authorizedFlowVersionID: "flow-version-1", ownerParam: "companion-pack"},
		{name: "different companion version", authorizedFlowVersionID: "flow-version-other", ownerParam: "companion-pack", wantErr: true},
		{name: "spoofed owner", authorizedFlowVersionID: "flow-version-1", ownerParam: "other-pack", wantErr: true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			store := &connectorRemoteIdentityActionStore{
				inboundRegisterDurableStore:               newInboundRegisterDurableStore(),
				connectorRemoteIdentityAuthorityStoreStub: &connectorRemoteIdentityAuthorityStoreStub{},
			}
			exec := &FlowExecutor{connectorPushDurable: store, query: &connectorRemoteIdentityQueryStub{}}
			action := &models.FlowActionCall{
				ActionType: "resolveConnectorRemoteIdentity",
				InputParameters: []models.FlowActionParameter{
					{Name: "connectorOwnerKey", Value: tc.ownerParam},
					{Name: "destinationSlotId", Value: "dest-slot"},
					{Name: "remoteId", Value: "remote-1"},
				},
			}
			ctx := context.WithValue(
				context.Background(),
				constants.ContextKeyConnectorSyncOrigin,
				"connector-route-pack",
			)
			ctx = constants.WithConnectorInboundCompanionFlowExecution(ctx, tc.authorizedFlowVersionID)
			ctx = withFlowRuntimeContext(ctx, flowRuntimeContextFromCtx(ctx).WithCurrentFlow(FlowExecutionIdentity{
				APIName:       "Inbound_Companion_Upsert",
				VersionID:     "flow-version-1",
				OwnerPack:     "companion-pack",
				VersionSource: flowSourceStandard,
			}))

			_, err := exec.executeResolveConnectorRemoteIdentity(ctx, &models.FlowInterview{}, action, &models.UserSession{})
			if tc.wantErr && err == nil {
				t.Fatal("executeResolveConnectorRemoteIdentity error = nil, want companion version authorization failure")
			}
			if !tc.wantErr && err != nil {
				t.Fatalf("executeResolveConnectorRemoteIdentity: %v", err)
			}
		})
	}
}

func connectorRemoteIdentityTestAuthority() *ConnectorRemoteIdentityAuthority {
	identity := connectorPushTestIdentity()
	identity.DestinationSlotID = "dest-slot"
	return &ConnectorRemoteIdentityAuthority{
		LinkID:            "link-id",
		LinkKey:           mustConnectorPushTestLinkKey(identity),
		LinkStatus:        ConnectorPushLinkStatusFinalized,
		RemoteID:          "remote-1",
		RemoteLocatorHash: ConnectorPushRemoteLocatorHash(identity, "remote-1"),
		Identity:          identity,
		RemoteIDField:     "External_Id__c",
	}
}

type connectorRemoteIdentityAuthorityStoreStub struct {
	authority *ConnectorRemoteIdentityAuthority
	err       error
	readbacks []connectorRemoteIdentityReadback
}

type connectorRemoteIdentityActionStore struct {
	*inboundRegisterDurableStore
	*connectorRemoteIdentityAuthorityStoreStub
}

type connectorRemoteIdentityReadback struct {
	linkKey string
	status  string
}

func (s *connectorRemoteIdentityAuthorityStoreStub) ResolveFinalizedRemoteLocator(context.Context, ConnectorRemoteLocatorQuery) (*ConnectorRemoteIdentityAuthority, error) {
	return s.authority, s.err
}

func (s *connectorRemoteIdentityAuthorityStoreStub) RecordCompatibilityMirrorReadback(_ context.Context, linkKey, status string) error {
	s.readbacks = append(s.readbacks, connectorRemoteIdentityReadback{linkKey: linkKey, status: status})
	return nil
}

type connectorRemoteIdentityQueryStub struct {
	records []models.SObject
	err     error
	calls   []queryspec.QuerySpec
}

func (q *connectorRemoteIdentityQueryStub) QueryWithSpec(_ context.Context, spec queryspec.QuerySpec, _ *models.UserSession) (*models.QueryResult, error) {
	q.calls = append(q.calls, spec)
	if q.err != nil {
		return nil, q.err
	}
	return &models.QueryResult{TotalSize: len(q.records), Done: true, Records: q.records}, nil
}

func (*connectorRemoteIdentityQueryStub) QueryWithFilter(context.Context, string, string, *models.UserSession, string, string, int) (*models.QueryResult, error) {
	return nil, errors.New("unexpected QueryWithFilter")
}

func (*connectorRemoteIdentityQueryStub) RunAnalytics(context.Context, models.AnalyticsQuery, *models.UserSession) (interface{}, error) {
	return nil, errors.New("unexpected RunAnalytics")
}

func (*connectorRemoteIdentityQueryStub) SearchSingleObject(context.Context, string, string, []string, []string, int, int, string, string, string, *models.UserSession) ([]models.SObject, error) {
	return nil, errors.New("unexpected SearchSingleObject")
}

func (*connectorRemoteIdentityQueryStub) SearchSuggestions(context.Context, string, string, []string, []string, int, *models.UserSession) ([]models.SObject, error) {
	return nil, errors.New("unexpected SearchSuggestions")
}

func (*connectorRemoteIdentityQueryStub) GlobalSearch(context.Context, string, *models.UserSession) ([]models.SearchResult, error) {
	return nil, errors.New("unexpected GlobalSearch")
}

func (*connectorRemoteIdentityQueryStub) Calculate(context.Context, string, models.SObject, *models.UserSession) (models.SObject, error) {
	return nil, errors.New("unexpected Calculate")
}

func (*connectorRemoteIdentityQueryStub) ExecuteRawSQL(context.Context, string, []interface{}, *models.UserSession) ([]models.SObject, error) {
	return nil, errors.New("unexpected ExecuteRawSQL")
}
