//go:build crm

package crm

import (
	"context"
	"testing"

	recordEvents "github.com/nexuscrm/events"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestCRMProductHooksRegisterSalesforceStreamingTopics(t *testing.T) {
	hooks := ProductHooks()
	mappings := hooks.Capabilities.StreamingEventChannelMappings
	if len(mappings) == 0 {
		t.Fatal("CRM ProductHooks registered no streaming topic mappings")
	}

	for _, mapping := range mappings {
		if mapping.EventType == sharedEvents.RecordCreated && mapping.Channel == "/topic/records/created" {
			return
		}
	}
	t.Fatalf("CRM ProductHooks missing RecordCreated -> /topic/records/created mapping: %#v", mappings)
}

// TestCRMRecordTopicsDropNotificationOnly pins the privacy boundary flagged on
// #3627: the public record topics broadcast full record payloads to every
// subscribed session, so Notification rows (whose REST read path is
// deliberately recipient-scoped) must never pass the mapping filter. Scope is
// Notification-only: every other object — including other system tables like
// User — keeps its prior broadcast behaviour and must pass. Covers both
// payload shapes record events travel as, plus the fail-open verdict for
// undeterminable payloads (only a positive Notification match is dropped).
func TestCRMRecordTopicsDropNotificationOnly(t *testing.T) {
	recordChannels := map[string]bool{
		"/topic/records/created": false,
		"/topic/records/updated": false,
		"/topic/records/deleted": false,
	}
	ctx := context.Background()
	for _, mapping := range StreamingEventChannelMappings() {
		if _, ok := recordChannels[mapping.Channel]; !ok {
			continue
		}
		recordChannels[mapping.Channel] = true
		if mapping.Filter == nil {
			t.Fatalf("record topic %s has no filter; Notification rows would broadcast to every subscriber", mapping.Channel)
		}
		// Notification is dropped in both payload shapes it travels as.
		if mapping.Filter(ctx, recordEvents.RecordEventPayload{ObjectAPIName: constants.TableNotification, Record: models.SObject{"title": "private"}}) {
			t.Fatalf("record topic %s broadcast a %s row (typed payload)", mapping.Channel, constants.TableNotification)
		}
		if mapping.Filter(ctx, map[string]interface{}{"object_api_name": constants.TableNotification}) {
			t.Fatalf("record topic %s broadcast a %s row (map payload)", mapping.Channel, constants.TableNotification)
		}
		// Scope is deliberately Notification-only: other system tables (User) and
		// tenant objects keep their prior broadcast behaviour and must pass.
		if !mapping.Filter(ctx, recordEvents.RecordEventPayload{ObjectAPIName: constants.TableUser}) {
			t.Fatalf("record topic %s dropped a %s row; scope must be Notification-only, not all system tables", mapping.Channel, constants.TableUser)
		}
		if !mapping.Filter(ctx, recordEvents.RecordEventPayload{ObjectAPIName: "MyObject__c"}) {
			t.Fatalf("record topic %s dropped an ordinary tenant object row", mapping.Channel)
		}
		// An undeterminable payload passes through (fail-open): only a positive
		// Notification match is ever dropped, so unrelated shapes are not affected.
		if !mapping.Filter(ctx, map[string]interface{}{"id": "001"}) {
			t.Fatalf("record topic %s dropped a payload with no determinable object; only Notification must be dropped", mapping.Channel)
		}
	}
	for channel, seen := range recordChannels {
		if !seen {
			t.Fatalf("mapping for record topic %s missing", channel)
		}
	}
}
