package records

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

func cacheInvalidationProbeSchema(capabilities ...string) *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		ID:           "obj_cache_invalidation_probe",
		APIName:      "CacheInvalidationProbe__c",
		Label:        "Cache Invalidation Probe",
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{APIName: constants.FieldName, Type: constants.FieldTypeString, Createable: true, Updateable: true},
		},
	}
	if len(capabilities) > 0 {
		schema.AccessPolicy = &models.ObjectAccessPolicy{
			CacheInvalidation: &models.RecordCacheInvalidationAccessPolicy{},
		}
		for _, capability := range capabilities {
			schema.AccessPolicy.CacheInvalidation.Handlers = append(schema.AccessPolicy.CacheInvalidation.Handlers, models.RecordCacheInvalidationPolicy{
				Capability: capability,
			})
		}
	}
	makeFieldsMap(schema)
	return schema
}

func TestRecordCacheInvalidationRegistrationUsesMetadataCapability(t *testing.T) {
	schema := cacheInvalidationProbeSchema("test.permission-cache")
	ps := newRecordEntrypointTestService(schema, &captureRecordRepo{})

	if err := ps.RegisterRecordCacheInvalidation(contract.RecordCacheInvalidationRegistration{
		Capability: "test.permission-cache",
		Handler: func(_ context.Context, event contract.RecordCacheInvalidationEvent) (contract.RecordCacheInvalidationPlan, error) {
			if event.ObjectName != schema.APIName {
				t.Fatalf("ObjectName = %q, want %q", event.ObjectName, schema.APIName)
			}
			if len(event.Changes) != 1 {
				t.Fatalf("changes len = %d, want 1", len(event.Changes))
			}
			return contract.RecordCacheInvalidationPlan{
				InvalidatePermissions: true,
				UserIDs:               []string{"005-user"},
			}, nil
		},
	}); err != nil {
		t.Fatalf("RegisterRecordCacheInvalidation returned error: %v", err)
	}

	plan, err := ps.collectSecurityInvalidationPlan(context.Background(), schema.APIName, []securityRecordChange{{
		after: models.SObject{constants.FieldID: "probe-1"},
	}})
	if err != nil {
		t.Fatalf("collectSecurityInvalidationPlan returned error: %v", err)
	}
	if !plan.invalidatePermissions {
		t.Fatal("invalidatePermissions = false, want true")
	}
	if _, ok := plan.userIDs["005-user"]; !ok {
		t.Fatalf("userIDs = %#v, want 005-user", plan.userIDs)
	}
}

func TestRecordCacheInvalidationRegistrationCopiesContractRecords(t *testing.T) {
	schema := cacheInvalidationProbeSchema("test.copy-record")
	ps := newRecordEntrypointTestService(schema, &captureRecordRepo{})
	before := models.SObject{
		constants.FieldID:   "probe-1",
		constants.FieldName: "before",
		"attributes":        map[string]interface{}{"type": schema.APIName},
	}
	after := models.SObject{
		constants.FieldID:   "probe-1",
		constants.FieldName: "after",
		"attributes":        map[string]interface{}{"type": schema.APIName},
	}
	called := false

	if err := ps.RegisterRecordCacheInvalidation(contract.RecordCacheInvalidationRegistration{
		Capability: "test.copy-record",
		Handler: func(_ context.Context, event contract.RecordCacheInvalidationEvent) (contract.RecordCacheInvalidationPlan, error) {
			called = true
			if len(event.Changes) != 1 {
				t.Fatalf("changes len = %d, want 1", len(event.Changes))
			}
			change := event.Changes[0]
			if _, ok := change.Before["attributes"]; ok {
				t.Fatal("Before includes attributes, want contract record shape")
			}
			if _, ok := change.After["attributes"]; ok {
				t.Fatal("After includes attributes, want contract record shape")
			}
			change.Before[constants.FieldName] = "mutated-before"
			change.After[constants.FieldName] = "mutated-after"
			return contract.RecordCacheInvalidationPlan{}, nil
		},
	}); err != nil {
		t.Fatalf("RegisterRecordCacheInvalidation returned error: %v", err)
	}

	if _, err := ps.collectSecurityInvalidationPlan(context.Background(), schema.APIName, []securityRecordChange{{
		before: before,
		after:  after,
	}}); err != nil {
		t.Fatalf("collectSecurityInvalidationPlan returned error: %v", err)
	}
	if !called {
		t.Fatal("cache invalidation handler was not called")
	}
	if got := before[constants.FieldName]; got != "before" {
		t.Fatalf("before record Name = %v, want original value", got)
	}
	if got := after[constants.FieldName]; got != "after" {
		t.Fatalf("after record Name = %v, want original value", got)
	}
}

func TestRecordCacheInvalidationRegistrationFailsClosedWhenMetadataCapabilityHasNoHandler(t *testing.T) {
	schema := cacheInvalidationProbeSchema("test.permission-cache")
	ps := newRecordEntrypointTestService(schema, &captureRecordRepo{})

	_, err := ps.collectSecurityInvalidationPlan(context.Background(), schema.APIName, []securityRecordChange{{
		after: models.SObject{constants.FieldID: "probe-1"},
	}})
	if err == nil {
		t.Fatal("collectSecurityInvalidationPlan succeeded, want missing-handler error")
	}
	if !strings.Contains(err.Error(), "missing record cache invalidation handler") ||
		!strings.Contains(err.Error(), "test.permission-cache") {
		t.Fatalf("collectSecurityInvalidationPlan error = %v, want missing handler capability", err)
	}
}

func TestValidateRecordCacheInvalidationConfigurationPropagatesMissingCustomHandler(t *testing.T) {
	schema := cacheInvalidationProbeSchema("test.custom-cache")
	ps := newRecordEntrypointTestService(schema, &captureRecordRepo{})

	err := ps.validateRecordCacheInvalidationConfiguration(context.Background(), schema.APIName)
	if err == nil {
		t.Fatal("validateRecordCacheInvalidationConfiguration succeeded, want missing custom handler error")
	}
	if !strings.Contains(err.Error(), "missing record cache invalidation handler") ||
		!strings.Contains(err.Error(), "test.custom-cache") {
		t.Fatalf("validateRecordCacheInvalidationConfiguration error = %v, want missing custom handler", err)
	}
}

func TestRecordCacheInvalidationFailsClosedWhenSecurityMetadataUnavailable(t *testing.T) {
	for _, objectName := range []string{
		constants.TableObjectPermissions,
		constants.TablePermissionSetPermission,
	} {
		t.Run(objectName, func(t *testing.T) {
			ps := &PersistenceService{}

			_, err := ps.collectSecurityInvalidationPlan(context.Background(), objectName, []securityRecordChange{{
				after: models.SObject{constants.FieldID: "security-record-1"},
			}})
			assertMissingSecurityInvalidationMetadata(t, err, objectName)
		})
	}
}

func TestRecordCacheInvalidationFailsClosedWhenSecurityMetadataHasNoCapabilities(t *testing.T) {
	for _, objectName := range []string{
		constants.TableObjectPermissions,
		constants.TablePermissionSetPermission,
	} {
		t.Run(objectName, func(t *testing.T) {
			schema := cacheInvalidationProbeSchema()
			schema.APIName = objectName
			ps := newRecordEntrypointTestService(schema, &captureRecordRepo{})

			_, err := ps.collectSecurityInvalidationPlan(context.Background(), objectName, []securityRecordChange{{
				after: models.SObject{constants.FieldID: "security-record-1"},
			}})
			assertMissingSecurityInvalidationMetadata(t, err, objectName)
		})
	}
}

func TestRecordCacheInvalidationAllowsNonSecurityObjectsWithoutCapabilities(t *testing.T) {
	schema := cacheInvalidationProbeSchema()
	ps := newRecordEntrypointTestService(schema, &captureRecordRepo{})

	plan, err := ps.collectSecurityInvalidationPlan(context.Background(), schema.APIName, []securityRecordChange{{
		after: models.SObject{constants.FieldID: "probe-1"},
	}})
	if err != nil {
		t.Fatalf("collectSecurityInvalidationPlan returned error: %v", err)
	}
	if plan == nil {
		t.Fatal("collectSecurityInvalidationPlan returned nil plan")
	}
	if plan.invalidatePermissions || plan.invalidateSharing || plan.refreshRoleHierarchy || len(plan.userIDs) != 0 {
		t.Fatalf("plan = %#v, want no invalidation for non-security object without capabilities", plan)
	}
}

func TestSecurityCacheInvalidationMetadataRequiredUsesManagedDeclarations(t *testing.T) {
	definitions := []schema.TableDefinition{
		{
			TableName: "RegisteredInvalidationObject",
			AccessPolicy: &models.ObjectAccessPolicy{
				CacheInvalidation: &models.RecordCacheInvalidationAccessPolicy{
					Handlers: []models.RecordCacheInvalidationPolicy{{Capability: "toy.cache.invalidate"}},
				},
			},
		},
		{
			TableName: "UnregisteredObject",
		},
	}

	required := securityCacheInvalidationMetadataRequiredObjectsFromDefinitions(definitions)
	if _, ok := required[securityCacheInvalidationObjectKey("registeredinvalidationobject")]; !ok {
		t.Fatal("metadata requirement was not derived from cacheInvalidation declaration")
	}
	if _, ok := required[securityCacheInvalidationObjectKey("UnregisteredObject")]; ok {
		t.Fatal("metadata requirement was derived for object without cacheInvalidation declaration")
	}
}

func assertMissingSecurityInvalidationMetadata(t *testing.T, err error, objectName string) {
	t.Helper()
	if err == nil {
		t.Fatal("collectSecurityInvalidationPlan succeeded, want missing security invalidation metadata error")
	}
	if !strings.Contains(err.Error(), "missing record cache invalidation metadata") ||
		!strings.Contains(err.Error(), objectName) {
		t.Fatalf("collectSecurityInvalidationPlan error = %v, want missing metadata for %s", err, objectName)
	}
}
