package events

// outbox_active_projection_test.go — verifies that EnqueueEvent lifts
// active-projection context values into InternalMetadata, and that the
// dispatcher strips them before Publish so subscribers never see them
// while pubCtx still carries the snaps for FlowTriggerListener.

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	sharedevents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

// captureEnqueueRepo records the InternalMetadata from the last Enqueue call.
// Implements ports.OutboxRepository.
type captureEnqueueRepo struct {
	lastEnvelope InternalRecordEventEnvelope
	enqueueCalls int
}

func (r *captureEnqueueRepo) Enqueue(_ context.Context, _ string, payload interface{}) (string, error) {
	r.enqueueCalls++
	if env, ok := payload.(InternalRecordEventEnvelope); ok {
		r.lastEnvelope = env
	}
	return "evt-001", nil
}
func (r *captureEnqueueRepo) GetPendingEvents(_ context.Context, _ int) ([]models.OutboxEvent, error) {
	return nil, nil
}
func (r *captureEnqueueRepo) GetPendingEventsWithRecordFIFO(_ context.Context, _ int) ([]models.OutboxEvent, error) {
	return nil, nil
}
func (r *captureEnqueueRepo) ClaimEvent(_ context.Context, id string) (models.OutboxClaim, error) {
	return models.OutboxClaim{EventID: id}, nil
}
func (r *captureEnqueueRepo) CreateOutboxAttemptSavepoint(_ context.Context) error { return nil }
func (r *captureEnqueueRepo) RollbackToOutboxAttemptSavepoint(_ context.Context) error {
	return nil
}
func (r *captureEnqueueRepo) ReleaseOutboxAttemptSavepoint(_ context.Context) error { return nil }
func (r *captureEnqueueRepo) RefreshOutboxProcessingClaim(_ context.Context, _ models.OutboxClaim) error {
	return nil
}
func (r *captureEnqueueRepo) UpdateStatus(_ context.Context, _ models.OutboxClaim, _, _ string) error {
	return nil
}
func (r *captureEnqueueRepo) MarkFailed(_ context.Context, _ models.OutboxClaim, _ string) error {
	return nil
}
func (r *captureEnqueueRepo) IncrementRetry(_ context.Context, _ models.OutboxClaim, _ string) error {
	return nil
}
func (r *captureEnqueueRepo) CleanupProcessed(_ context.Context, _ time.Time) (int64, error) {
	return 0, nil
}

// TestEnqueueEvent_LiftActiveProjectionMetadata — active-projection context
// values stamped before EnqueueEvent are persisted in InternalMetadata.
func TestEnqueueEvent_LiftActiveProjectionMetadata(t *testing.T) {
	repo := &captureEnqueueRepo{}
	svc := NewOutboxService(repo, nil, nil)

	ctx := context.Background()
	ctx = MarkActiveProjectionAttempted(ctx)
	var err error
	ctx, err = WithActiveProjections(ctx,
		models.SObject{"StatusActive__c": "Hot"},
		models.SObject{"StatusActive__c": "Cold"},
	)
	if err != nil {
		t.Fatalf("WithActiveProjections: %v", err)
	}

	if enqErr := svc.EnqueueEvent(ctx, sharedevents.RecordCreated, RecordEventPayload{
		ObjectAPIName: "Widget__c",
		Record:        models.SObject{"Id": "001", "Score__c": float64(99)},
	}); enqErr != nil {
		t.Fatalf("EnqueueEvent: %v", enqErr)
	}

	if repo.enqueueCalls != 1 {
		t.Fatalf("enqueue calls = %d, want 1", repo.enqueueCalls)
	}

	im := repo.lastEnvelope.InternalMetadata

	if _, ok := im[internalMetadataKeyActiveProjectionRecord]; !ok {
		t.Error("InternalMetadata missing active_projection_record")
	}
	if _, ok := im[internalMetadataKeyActiveProjectionOldRecord]; !ok {
		t.Error("InternalMetadata missing active_projection_old_record")
	}
	if v := im[internalMetadataKeyActiveProjectionAttempted]; v != "true" {
		t.Errorf("active_projection_attempted: want 'true', got %q", v)
	}
}

// TestProcessEventAtomic_StripsActiveProjectionFromPublicPayload — after
// dispatch, the published payload.Metadata must not contain active-projection
// keys, but pubCtx must carry them (for FlowTriggerListener).
//
// Reuses existing stubs: claimSuccessRepo, contextCapturingEventBus, noopTxManager
// from outbox_lineage_test.go (same package).
func TestProcessEventAtomic_StripsActiveProjectionFromPublicPayload(t *testing.T) {
	snapJSON, _ := json.Marshal(models.SObject{"StatusActive__c": "Hot"})
	oldSnapJSON, _ := json.Marshal(models.SObject{"StatusActive__c": "Cold"})
	envelope := InternalRecordEventEnvelope{
		PublicPayload: RecordEventPayload{
			ObjectAPIName: "Widget__c",
			Record:        models.SObject{"Id": "001"},
		},
		InternalMetadata: map[string]string{
			internalMetadataKeyActiveProjectionRecord:    string(snapJSON),
			internalMetadataKeyActiveProjectionOldRecord: string(oldSnapJSON),
			internalMetadataKeyActiveProjectionAttempted: "true",
		},
	}
	payloadJSON, _ := json.Marshal(envelope)

	// claimSuccessRepo and contextCapturingEventBus declared in outbox_lineage_test.go
	repo := &claimSuccessRepo{}
	bus := &contextCapturingEventBus{}
	svc := NewOutboxService(repo, bus, noopTxManager{})

	err := svc.processEventAtomic(context.Background(), "evt-001",
		string(sharedevents.RecordCreated), string(payloadJSON))
	if err != nil {
		t.Fatalf("processEventAtomic: %v", err)
	}

	if bus.capturedPayload == nil {
		t.Fatal("eventBus.Publish was not called")
	}
	pub, ok := bus.capturedPayload.(RecordEventPayload)
	if !ok {
		t.Fatalf("published payload type = %T, want RecordEventPayload", bus.capturedPayload)
	}

	// Public payload metadata must not expose active-projection keys.
	if pub.Metadata != nil {
		if _, exists := pub.Metadata[internalMetadataKeyActiveProjectionRecord]; exists {
			t.Error("active_projection_record must be stripped from published payload.Metadata")
		}
		if _, exists := pub.Metadata[internalMetadataKeyActiveProjectionAttempted]; exists {
			t.Error("active_projection_attempted must be stripped from published payload.Metadata")
		}
	}

	// pubCtx should carry the snap (restored by restoreInternalMetadata via default case).
	if bus.capturedCtx == nil {
		t.Fatal("capturedCtx is nil")
	}
	snap, snapErr := ActiveProjectionRecordFromCtx(bus.capturedCtx)
	if snapErr != nil {
		t.Fatalf("ActiveProjectionRecordFromCtx on pubCtx: %v", snapErr)
	}
	if snap == nil {
		t.Fatal("active-projection snap should be on pubCtx for FlowTriggerListener")
	}
	if v, _ := snap["StatusActive__c"].(string); v != "Hot" {
		t.Errorf("snap.StatusActive__c: want Hot, got %q", v)
	}
}
