package records

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/logging"
	metadataFeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// Without HasInsertPreviewPreparation on ctx the validator must still treat
// preview-* IDs as non-durable lookups: callers outside the no-DML preview
// preparation path have no preview-provenance guarantee that the synthetic
// record will exist when the row is committed for real. Regression guard
// against the preview-provenance gate accidentally opening the door for
// every caller that happens to attach a registry.
func TestValidateNonPolymorphicLookupsRejectsSyntheticPreviewIDOutsidePreviewContext(t *testing.T) {
	schema := &models.ObjectMetadata{
		ID:           "obj_child",
		APIName:      "ChildObject",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:     "ParentId",
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{"ParentObject"},
				Createable:  true,
				Updateable:  true,
			},
		},
	}
	ps := previewSyntheticLookupTestService(schema)
	ctx := context.Background()
	ctx = constants.WithPreviewSyntheticRecordRegistry(ctx)
	constants.RegisterPreviewSyntheticRecord(ctx, "preview-ParentObject-1", "ParentObject", map[string]interface{}{
		constants.FieldID: "preview-ParentObject-1",
	})

	err := ps.validateNonPolymorphicLookups(ctx, models.SObject{
		"ParentId": "preview-ParentObject-1",
	}, schema, previewSyntheticLookupUser())
	if err == nil {
		t.Fatal("validateNonPolymorphicLookups accepted synthetic preview ID outside preview preparation; want durable parent validation failure")
	}
	if !strings.Contains(err.Error(), "referenced ParentObject record") {
		t.Fatalf("validateNonPolymorphicLookups error = %v; want ParentObject lookup failure", err)
	}
}

func TestValidateNonPolymorphicLookupsRejectsMissingGroupUserRoleReference(t *testing.T) {
	groupSchema := &models.ObjectMetadata{
		ID:           "obj_group",
		APIName:      constants.TableGroup,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:     constants.FieldGroup_UserRoleID,
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{constants.TableUserRole},
				Createable:  true,
				Updateable:  true,
			},
		},
	}
	userRoleSchema := &models.ObjectMetadata{
		ID:           "obj_userrole",
		APIName:      constants.TableUserRole,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
		},
	}
	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{
		schemas: []*models.ObjectMetadata{groupSchema, userRoleSchema},
	}, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	ps := &PersistenceService{
		repo:        &stubUpsertRecordRepo{},
		metadata:    metaSvc,
		permissions: permSvc,
		logger:      logging.New(context.Background()),
	}

	err := ps.validateNonPolymorphicLookups(context.Background(), models.SObject{
		constants.FieldGroup_UserRoleID: "00E000000000404",
	}, groupSchema, previewSyntheticLookupUser())
	if err == nil {
		t.Fatal("validateNonPolymorphicLookups accepted missing Group.UserRoleId target; want lookup validation failure")
	}
	if !strings.Contains(err.Error(), "referenced UserRole record") {
		t.Fatalf("validateNonPolymorphicLookups error = %v; want UserRole lookup failure", err)
	}
}

// Inside a no-DML insert preview preparation the validator must accept
// preview-* IDs that the request-local registry can prove came from an
// earlier same-run RecordCreate. Models the Account→lookup→Contact flow
// case that flowVersionCreatePreviewSupported reports as supported.
func TestValidateNonPolymorphicLookupsAcceptsSyntheticPreviewIDUnderPreviewContext(t *testing.T) {
	schema := &models.ObjectMetadata{
		ID:           "obj_child",
		APIName:      "ChildObject",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:     "ParentId",
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{"ParentObject"},
				Createable:  true,
				Updateable:  true,
			},
		},
	}
	ps := previewSyntheticLookupTestService(schema)
	ctx := constants.WithInsertPreviewPreparation(context.Background())
	ctx = constants.WithPreviewSyntheticRecordRegistry(ctx)
	constants.RegisterPreviewSyntheticRecord(ctx, "preview-ParentObject-1", "ParentObject", map[string]interface{}{
		constants.FieldID: "preview-ParentObject-1",
	})

	if err := ps.validateNonPolymorphicLookups(ctx, models.SObject{
		"ParentId": "preview-ParentObject-1",
	}, schema, previewSyntheticLookupUser()); err != nil {
		t.Fatalf("validateNonPolymorphicLookups rejected registered preview-* ID under preview preparation: %v", err)
	}
}

// A stray "preview-*"-shaped literal that the registry does NOT recognise
// for the target object must still fall through to the normal access/
// existence gate even inside preview preparation. Without this, anyone
// could hand-craft a preview-* string to bypass FK validation.
func TestValidateNonPolymorphicLookupsRejectsUnregisteredPreviewIDUnderPreviewContext(t *testing.T) {
	schema := &models.ObjectMetadata{
		ID:           "obj_child",
		APIName:      "ChildObject",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:     "ParentId",
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{"ParentObject"},
				Createable:  true,
				Updateable:  true,
			},
		},
	}
	ps := previewSyntheticLookupTestService(schema)
	ctx := constants.WithInsertPreviewPreparation(context.Background())
	ctx = constants.WithPreviewSyntheticRecordRegistry(ctx)
	// Note: no RegisterPreviewSyntheticRecord — the literal is unbacked.

	err := ps.validateNonPolymorphicLookups(ctx, models.SObject{
		"ParentId": "preview-ParentObject-1",
	}, schema, previewSyntheticLookupUser())
	if err == nil {
		t.Fatal("validateNonPolymorphicLookups accepted unregistered preview-* literal under preview preparation; want durable parent validation failure")
	}
	if !strings.Contains(err.Error(), "referenced ParentObject record") {
		t.Fatalf("validateNonPolymorphicLookups error = %v; want ParentObject lookup failure", err)
	}
}

// Non-preview polymorphic case still rejects preview-* IDs: same regression
// guard as the non-polymorphic sibling.
func TestValidatePolymorphicLookupsRejectsSyntheticPreviewIDOutsidePreviewContext(t *testing.T) {
	schema := &models.ObjectMetadata{
		ID:           "obj_activity",
		APIName:      "ActivityObject",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:       "WhatId",
				Type:          constants.FieldTypeReference,
				ReferenceTo:   []string{"ParentObject", "AlternateObject"},
				IsPolymorphic: true,
				Createable:    true,
				Updateable:    true,
			},
		},
	}
	ps := previewSyntheticLookupTestService(schema)
	ctx := context.Background()
	ctx = constants.WithPreviewSyntheticRecordRegistry(ctx)
	constants.RegisterPreviewSyntheticRecord(ctx, "preview-ParentObject-1", "ParentObject", map[string]interface{}{
		constants.FieldID: "preview-ParentObject-1",
	})

	_, err := ps.validatePolymorphicLookups(ctx, models.SObject{
		"WhatId": "preview-ParentObject-1",
	}, schema)
	if err == nil {
		t.Fatal("validatePolymorphicLookups accepted synthetic preview ID outside preview preparation; want durable parent validation failure")
	}
	if !strings.Contains(err.Error(), "referenced ID preview-ParentObject-1 not found") {
		t.Fatalf("validatePolymorphicLookups error = %v; want missing polymorphic lookup failure", err)
	}
}

// Inside preview preparation the polymorphic validator accepts the
// preview-* ID against the registry-recorded object and reports it as the
// resolved polymorphic type so the polymorphic-type discriminator column
// survives the no-DML preview's logical write image.
func TestValidatePolymorphicLookupsAcceptsSyntheticPreviewIDUnderPreviewContext(t *testing.T) {
	schema := &models.ObjectMetadata{
		ID:           "obj_activity",
		APIName:      "ActivityObject",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:       "WhatId",
				Type:          constants.FieldTypeReference,
				ReferenceTo:   []string{"ParentObject", "AlternateObject"},
				IsPolymorphic: true,
				Createable:    true,
				Updateable:    true,
			},
		},
	}
	ps := previewSyntheticLookupTestService(schema)
	ctx := constants.WithInsertPreviewPreparation(context.Background())
	ctx = constants.WithPreviewSyntheticRecordRegistry(ctx)
	constants.RegisterPreviewSyntheticRecord(ctx, "preview-ParentObject-1", "ParentObject", map[string]interface{}{
		constants.FieldID: "preview-ParentObject-1",
	})

	resolved, err := ps.validatePolymorphicLookups(ctx, models.SObject{
		"WhatId": "preview-ParentObject-1",
	}, schema)
	if err != nil {
		t.Fatalf("validatePolymorphicLookups rejected registered preview-* polymorphic ID under preview preparation: %v", err)
	}
	if got := resolved["WhatId"]; got != "ParentObject" {
		t.Fatalf("validatePolymorphicLookups resolved type = %q; want ParentObject", got)
	}
}

// Unregistered preview-* literal under preview preparation: polymorphic
// validator must still fall through and fail closed.
func TestValidatePolymorphicLookupsRejectsUnregisteredPreviewIDUnderPreviewContext(t *testing.T) {
	schema := &models.ObjectMetadata{
		ID:           "obj_activity",
		APIName:      "ActivityObject",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{
				APIName:       "WhatId",
				Type:          constants.FieldTypeReference,
				ReferenceTo:   []string{"ParentObject", "AlternateObject"},
				IsPolymorphic: true,
				Createable:    true,
				Updateable:    true,
			},
		},
	}
	ps := previewSyntheticLookupTestService(schema)
	ctx := constants.WithInsertPreviewPreparation(context.Background())
	ctx = constants.WithPreviewSyntheticRecordRegistry(ctx)
	// No RegisterPreviewSyntheticRecord.

	_, err := ps.validatePolymorphicLookups(ctx, models.SObject{
		"WhatId": "preview-ParentObject-1",
	}, schema)
	if err == nil {
		t.Fatal("validatePolymorphicLookups accepted unregistered preview-* literal under preview preparation; want missing polymorphic lookup failure")
	}
	if !strings.Contains(err.Error(), "referenced ID preview-ParentObject-1 not found") {
		t.Fatalf("validatePolymorphicLookups error = %v; want missing polymorphic lookup failure", err)
	}
}

func previewSyntheticLookupTestService(schema *models.ObjectMetadata) *PersistenceService {
	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{schema: schema}, nil, nil)
	permSvc := permissions.NewPermissionService(&stubUpsertPermRepo{grantCreate: true, grantEdit: true}, metaSvc, nil)
	return &PersistenceService{
		repo:        &stubUpsertRecordRepo{},
		metadata:    metaSvc,
		permissions: permSvc,
		logger:      logging.New(context.Background()),
	}
}

func previewSyntheticLookupUser() *models.UserSession {
	return &models.UserSession{ID: "005-preview-user", ProfileID: constants.ProfileStandardUser}
}
