package records

import (
	"strings"
	"testing"
	"time"

	"context"

	eventFeature "github.com/nexuscrm/events"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	"github.com/nexuscrm/kernel/pkg/utils"
	"github.com/nexuscrm/kernel/pkg/validation"
	metadataFeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// This file is the runtime oracle for the NX#930 sendNotification write-path
// dispute (design §0.1): whether a SystemContext-elevated
// PersistenceService.Insert into the Notification system table actually
// clears the real permission gate and preserves field names end to end. The
// dispute was previously resolved by static code inspection only (reading
// system_tables.json, models.Notification's JSON tags, and IsBlockedObject);
// every existing sendNotification test doubles the persistence layer with a
// fake that simply echoes back whatever it is given, so a real mismatch
// (e.g. IsBlockedObject firing, or a column-name drift) would have zero
// pre-merge signal. These tests exercise the REAL PersistenceService.Insert
// pipeline (schema resolution -> checkOperationPermission -> IsBlockedObject
// -> filterToLogicalFields -> repo.Insert) against a schema built straight
// from the embedded system_tables.json catalog — the platform's actual
// seeded source of truth — rather than a hand-typed copy that could
// silently drift from it.

// notificationSchemaFromCatalog builds the Notification ObjectMetadata from
// the real embedded schemacatalog.SystemTableDefinitions() entry, so a
// future drift in system_tables.json (e.g. someone adding
// RequiredWritePermission="BLOCKED" to Notification, or renaming a column)
// changes what this test exercises instead of silently going unnoticed
// behind a hand-typed copy.
func notificationSchemaFromCatalog(t *testing.T) *models.ObjectMetadata {
	t.Helper()
	defs, err := schemacatalog.SystemTableDefinitions()
	if err != nil {
		t.Fatalf("schemacatalog.SystemTableDefinitions() error = %v", err)
	}
	for _, def := range defs {
		if !strings.EqualFold(def.TableName, constants.TableNotification) {
			continue
		}
		schema := &models.ObjectMetadata{
			APIName:                 def.TableName,
			SharingModel:            constants.SharingModelPublicReadWrite,
			RequiredWritePermission: def.RequiredWritePermission,
			RequiredReadPermission:  def.RequiredReadPermission,
		}
		for _, col := range def.Columns {
			schema.Fields = append(schema.Fields, models.FieldMetadata{
				APIName:    col.Name,
				Type:       notificationCatalogColumnFieldType(col.Type),
				Createable: !col.PrimaryKey,
				Updateable: !col.PrimaryKey,
			})
		}
		makeFieldsMap(schema)
		return schema
	}
	t.Fatalf("Notification table definition not found in schemacatalog.SystemTableDefinitions() (catalog drift?)")
	return nil
}

func notificationCatalogColumnFieldType(sqlType string) constants.SchemaFieldType {
	switch {
	case strings.HasPrefix(sqlType, "VARCHAR"):
		return constants.FieldTypeString
	case strings.HasPrefix(sqlType, "TEXT"):
		return constants.FieldTypeTextarea
	case strings.HasPrefix(sqlType, "TINYINT"):
		return constants.FieldTypeBoolean
	case strings.HasPrefix(sqlType, "DATETIME"):
		return constants.FieldTypeDatetime
	default:
		return constants.FieldTypeString
	}
}

// notificationWritePathTestService wires a minimal-but-real PersistenceService
// (same construction pattern as the other nexuscrm-records entrypoint tests)
// against the catalog-derived Notification schema, with a permission stub
// that DENIES ordinary create/edit — so a passing insert can only be
// explained by SystemContext actually bypassing the CRUD/FLS gate, not by a
// test double that grants everything by default.
func notificationWritePathTestService(t *testing.T) (*PersistenceService, *captureRecordRepo) {
	t.Helper()
	schema := notificationSchemaFromCatalog(t)
	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{schema: schema}, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: false, grantEdit: false}, metaSvc, nil)
	repo := &captureRecordRepo{}
	ps := &PersistenceService{
		repo:            repo,
		rollup:          noopRollupProcessor{},
		metadata:        metaSvc,
		permissions:     permSvc,
		eventBus:        &eventFeature.NoOpEventBus{},
		validator:       metadataFeature.NewValidationService(formula.NewEngine()),
		schemaValidator: validation.NewSchemaValidator(),
		txManager:       inlineTxManager{},
	}
	return ps, repo
}

func notificationRecordForInsert() models.SObject {
	return (&models.Notification{
		ID:               utils.GenerateID(),
		RecipientID:      "user-active-1",
		Title:            "A record needs your attention.",
		Body:             "Please review MyObject__c/rec-1.",
		Link:             "/object/MyObject__c/rec-1",
		NotificationType: "flow",
		IsRead:           false,
		CreatedDate:      time.Now(),
	}).ToSObject()
}

// TestNotificationInsertUnderSystemContextBypassesCRUDAndPreservesFields is
// the runtime oracle closing the design's disputed write-path finding: a
// SystemContext-elevated Insert against the REAL catalog-derived
// Notification schema (a) is not rejected by IsBlockedObject, (b) is not
// rejected by the CRUD/FLS gate despite the permission stub denying create,
// and (c) round-trips every field the handler writes under its exact
// logical column name (recipient_id/title/body/link/notification_type/
// is_read), matching models.Notification.ToSObject()'s JSON keys.
func TestNotificationInsertUnderSystemContextBypassesCRUDAndPreservesFields(t *testing.T) {
	ps, repo := notificationWritePathTestService(t)
	notif := notificationRecordForInsert()

	writeCtx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "test: sendNotification system-table write")
	inserted, err := ps.Insert(writeCtx, constants.TableNotification, notif, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Insert() error = %v, want nil (SystemContext must bypass CRUD/FLS and Notification must not be IsBlockedObject)", err)
	}
	if _, ok := inserted[constants.FieldID].(string); !ok {
		t.Fatalf("inserted[Id] = %#v, want a string id", inserted[constants.FieldID])
	}
	if repo.inserted == nil {
		t.Fatal("repo.inserted = nil, want the record to have reached the repository")
	}

	for _, field := range []struct {
		key  string
		want interface{}
	}{
		{constants.FieldNotification_RecipientID, "user-active-1"},
		{constants.FieldNotification_Title, "A record needs your attention."},
		{constants.FieldNotification_Body, "Please review MyObject__c/rec-1."},
		{constants.FieldNotification_Link, "/object/MyObject__c/rec-1"},
		{constants.FieldNotification_NotificationType, "flow"},
		{constants.FieldNotification_IsRead, false},
	} {
		got, ok := repo.inserted[field.key]
		if !ok {
			t.Fatalf("repo.inserted missing key %q; filterToLogicalFields dropped it (schema logical column name mismatch against models.Notification.ToSObject())", field.key)
		}
		if got != field.want {
			t.Fatalf("repo.inserted[%q] = %#v, want %#v", field.key, got, field.want)
		}
	}
}

// TestNotificationInsertUnderOrdinaryUserContextDeniedByCRUD is the control
// case: without SystemContext elevation, the same permission stub (which
// denies create) must actually deny the insert. This proves the previous
// test's success is attributable to real SystemContext bypass semantics,
// not to a permissive test harness that would let any insert through.
func TestNotificationInsertUnderOrdinaryUserContextDeniedByCRUD(t *testing.T) {
	ps, repo := notificationWritePathTestService(t)
	notif := notificationRecordForInsert()

	callerCtx := context.Background()
	if got := constants.GetSecurityContext(callerCtx); got != constants.UserContext {
		t.Fatalf("precondition: security context = %v, want UserContext", got)
	}

	_, err := ps.Insert(callerCtx, constants.TableNotification, notif, recordEntrypointUser())
	if err == nil {
		t.Fatal("Insert() error = nil, want a permission error (ordinary UserContext must not bypass CRUD, proving the harness actually gates writes)")
	}
	if repo.inserted != nil {
		t.Fatalf("repo.inserted = %#v, want nil (denied insert must never reach the repository)", repo.inserted)
	}
}
