package metadata

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
	"github.com/nexuscrm/shared/pkg/models"
)

func installSiblingMaxFieldProvenance(t *testing.T) {
	t.Helper()
	previous := fieldprovenance.Default()
	t.Cleanup(func() { fieldprovenance.SetDefault(previous) })
	fieldprovenance.SetDefault(fieldprovenance.NewClassifier(nil, nil, nil, nil))
}

func TestCreateFieldSiblingMaxRequiresExistingNonPolymorphicParent(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: "Parent__c", Type: constants.FieldTypeString},
		},
	}
	metaRepo := &rollupCreateTestMetaRepo{all: []*models.ObjectMetadata{obj}}
	schemaRepo := &rollupCreateTestSchemaRepo{}
	ms := NewMetadataService(metaRepo, NewSchemaManager(schemaRepo), nil)

	parentField := "Parent__c"
	err := ms.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
		APIName:               "SortOrder__c",
		Label:                 "Sort Order",
		Type:                  models.FieldType("integer"),
		Createable:            true,
		Updateable:            true,
		SiblingMaxParentField: &parentField,
	})
	if err == nil {
		t.Fatal("CreateField accepted siblingMaxParentField pointing at a non-reference parent")
	}
	if !strings.Contains(err.Error(), "non-polymorphic reference") {
		t.Fatalf("CreateField error = %v, want non-polymorphic reference diagnostic", err)
	}
	if len(schemaRepo.addedCols) != 0 {
		t.Fatalf("AddColumn calls = %d, want 0 on rejected siblingMaxParentField", len(schemaRepo.addedCols))
	}
}

func TestCreateFieldSiblingMaxRejectsDerivedParentField(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	formula := "OwnerId"
	cases := []struct {
		name   string
		parent models.FieldMetadata
	}{
		{
			name:   "formula_reference",
			parent: models.FieldMetadata{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Formula: &formula, Createable: true, Updateable: true},
		},
		{
			name:   "projection_reference",
			parent: models.FieldMetadata{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, ProjectionConfig: &models.FieldProjectionConfig{Kind: "lookup"}, Createable: true, Updateable: true},
		},
		{
			name:   "virtual_reference",
			parent: models.FieldMetadata{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, IsVirtual: true, VirtualKind: constants.VirtualFieldCompound, Createable: true, Updateable: true},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			obj := &models.ObjectMetadata{
				ID:      "obj_line",
				APIName: "Line__c",
				Fields:  []models.FieldMetadata{tc.parent},
			}
			metaRepo := &rollupCreateTestMetaRepo{all: []*models.ObjectMetadata{obj}}
			schemaRepo := &rollupCreateTestSchemaRepo{}
			ms := NewMetadataService(metaRepo, NewSchemaManager(schemaRepo), nil)

			parentField := "Parent__c"
			err := ms.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
				APIName:               "SortOrder__c",
				Label:                 "Sort Order",
				Type:                  models.FieldType("integer"),
				Createable:            true,
				Updateable:            true,
				SiblingMaxParentField: &parentField,
			})
			if err == nil {
				t.Fatal("CreateField accepted siblingMaxParentField pointing at a derived parent")
			}
			if !strings.Contains(err.Error(), "persisted non-polymorphic reference") {
				t.Fatalf("CreateField error = %v, want persisted non-polymorphic reference diagnostic", err)
			}
			if len(schemaRepo.addedCols) != 0 {
				t.Fatalf("AddColumn calls = %d, want 0 on rejected derived siblingMaxParentField", len(schemaRepo.addedCols))
			}
		})
	}
}

func (r *batchRollupSchemaRepo) EnsureColumnIndex(tableName string, columnName string) error {
	return nil
}

func TestBatchCreateFieldsSiblingMaxCanReferenceParentInSameBatch(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
	}
	metaRepo := &batchRollupMetadataRepo{schemas: map[string]*models.ObjectMetadata{"line__c": obj}}
	schemaRepo := &batchRollupSchemaRepo{}
	ms := NewMetadataService(metaRepo, NewSchemaManager(schemaRepo), nil)

	parentField := "Parent__c"
	_, err := ms.BatchCreateFields(context.Background(), "Line__c", []models.FieldMetadata{
		{APIName: parentField, Label: "Parent", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
		{APIName: "SortOrder__c", Label: "Sort Order", Type: models.FieldType("integer"), Createable: true, Updateable: true, SiblingMaxParentField: &parentField},
	})
	if err != nil {
		t.Fatalf("BatchCreateFields returned error: %v", err)
	}
	if schemaRepo.batchCalls != 1 {
		t.Fatalf("BatchAddColumns calls = %d, want 1", schemaRepo.batchCalls)
	}
}

func TestCreateFieldSiblingMaxRejectsBlankDefaultValue(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
		},
	}
	metaRepo := &rollupCreateTestMetaRepo{all: []*models.ObjectMetadata{obj}}
	schemaRepo := &rollupCreateTestSchemaRepo{}
	ms := NewMetadataService(metaRepo, NewSchemaManager(schemaRepo), nil)

	parentField := "Parent__c"
	blankDefault := ""
	err := ms.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
		APIName:               "SortOrder__c",
		Label:                 "Sort Order",
		Type:                  models.FieldType("integer"),
		Createable:            true,
		Updateable:            true,
		DefaultValue:          &blankDefault,
		SiblingMaxParentField: &parentField,
	})
	if err == nil {
		t.Fatal("CreateField accepted siblingMaxParentField with a blank defaultValue")
	}
	if !strings.Contains(err.Error(), "defaultValue") {
		t.Fatalf("CreateField error = %v, want defaultValue diagnostic", err)
	}
	if len(schemaRepo.addedCols) != 0 {
		t.Fatalf("AddColumn calls = %d, want 0 on rejected blank defaultValue", len(schemaRepo.addedCols))
	}
}

func TestCreateFieldSiblingMaxRejectsDecimalNumericTarget(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
		},
	}
	metaRepo := &rollupCreateTestMetaRepo{all: []*models.ObjectMetadata{obj}}
	schemaRepo := &rollupCreateTestSchemaRepo{}
	ms := NewMetadataService(metaRepo, NewSchemaManager(schemaRepo), nil)

	parentField := "Parent__c"
	zeroScale := 0
	twoScale := 2
	cases := []struct {
		name  string
		typ   models.FieldType
		scale *int
	}{
		{name: "double_without_scale", typ: constants.FieldTypeDouble},
		{name: "double_zero_scale", typ: constants.FieldTypeDouble, scale: &zeroScale},
		{name: "double_fractional_scale", typ: constants.FieldTypeDouble, scale: &twoScale},
		{name: "number", typ: models.FieldType("number")},
		{name: "currency", typ: constants.FieldTypeCurrency},
		{name: "percent", typ: constants.FieldTypePercent},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			schemaRepo.addedCols = nil
			err := ms.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
				APIName:               "SortOrder__c",
				Label:                 "Sort Order",
				Type:                  tc.typ,
				Scale:                 tc.scale,
				Createable:            true,
				Updateable:            true,
				SiblingMaxParentField: &parentField,
			})
			if err == nil {
				t.Fatal("CreateField accepted siblingMaxParentField on a decimal numeric target")
			}
			if !strings.Contains(err.Error(), "integer-compatible numeric target") {
				t.Fatalf("CreateField error = %v, want integer-compatible numeric target diagnostic", err)
			}
			if len(schemaRepo.addedCols) != 0 {
				t.Fatalf("AddColumn calls = %d, want 0 on rejected decimal target", len(schemaRepo.addedCols))
			}
		})
	}
}

func TestCreateFieldSiblingMaxRejectsNonWriteableTarget(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
		},
	}

	parentField := "Parent__c"
	cases := []struct {
		name                  string
		createable            bool
		updateable            bool
		writeProtectionPolicy string
	}{
		{name: "not_createable", createable: false, updateable: true},
		{name: "not_updateable", createable: true, updateable: false},
		{name: "intrinsic_write_protection", createable: true, updateable: true, writeProtectionPolicy: constants.FieldWriteProtectionTrustedOnly},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			schemaRepo := &rollupCreateTestSchemaRepo{}
			ms := NewMetadataService(&rollupCreateTestMetaRepo{all: []*models.ObjectMetadata{obj}}, NewSchemaManager(schemaRepo), nil)

			err := ms.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
				APIName:               "SortOrder__c",
				Label:                 "Sort Order",
				Type:                  models.FieldType("integer"),
				Createable:            tc.createable,
				Updateable:            tc.updateable,
				WriteProtectionPolicy: tc.writeProtectionPolicy,
				SiblingMaxParentField: &parentField,
			})
			if err == nil {
				t.Fatal("CreateField accepted siblingMaxParentField on a non-writeable target")
			}
			if !strings.Contains(err.Error(), "createable and updateable") {
				t.Fatalf("CreateField error = %v, want createable/updateable diagnostic", err)
			}
			if len(schemaRepo.addedCols) != 0 {
				t.Fatalf("AddColumn calls = %d, want 0 on rejected non-writeable target", len(schemaRepo.addedCols))
			}
		})
	}
}

func TestUpdateFieldSiblingMaxMergesAndClears(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: "Parent__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
			{APIName: "SortOrder__c", Type: models.FieldType("integer"), Createable: true, Updateable: true},
		},
	}
	schemaRepo := &metadataUpdateFieldSchemaRepo{}
	ms := NewMetadataService(&metadataUpdateFieldRepo{schema: obj}, NewSchemaManager(schemaRepo), nil)
	ms.SetValidationService(NewValidationService(nil))

	parentField := "Parent__c"
	if err := ms.UpdateField(context.Background(), "Line__c", "SortOrder__c", &models.FieldMetadata{
		SiblingMaxParentField: &parentField,
	}); err != nil {
		t.Fatalf("UpdateField set siblingMaxParentField: %v", err)
	}
	if schemaRepo.saved == nil || schemaRepo.saved.SiblingMaxParentField == nil || *schemaRepo.saved.SiblingMaxParentField != parentField {
		t.Fatalf("saved siblingMaxParentField = %#v, want %q", schemaRepo.saved, parentField)
	}

	clear := " "
	if err := ms.UpdateField(context.Background(), "Line__c", "SortOrder__c", &models.FieldMetadata{
		SiblingMaxParentField: &clear,
	}); err != nil {
		t.Fatalf("UpdateField clear siblingMaxParentField: %v", err)
	}
	if schemaRepo.saved == nil || schemaRepo.saved.SiblingMaxParentField != nil {
		t.Fatalf("saved siblingMaxParentField after clear = %#v, want nil", schemaRepo.saved)
	}
}

func TestUpdateFieldSiblingMaxRejectsExistingDecimalNumericTarget(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	parentField := "Parent__c"
	zeroScale := 0
	twoScale := 2
	cases := []struct {
		name  string
		typ   models.FieldType
		scale *int
	}{
		{name: "double_without_scale", typ: constants.FieldTypeDouble},
		{name: "double_zero_scale", typ: constants.FieldTypeDouble, scale: &zeroScale},
		{name: "double_fractional_scale", typ: constants.FieldTypeDouble, scale: &twoScale},
		{name: "number", typ: models.FieldType("number")},
		{name: "currency", typ: constants.FieldTypeCurrency},
		{name: "percent", typ: constants.FieldTypePercent},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			obj := &models.ObjectMetadata{
				ID:      "obj_line",
				APIName: "Line__c",
				Fields: []models.FieldMetadata{
					{APIName: parentField, Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
					{APIName: "SortOrder__c", Type: tc.typ, Scale: tc.scale, Createable: true, Updateable: true},
				},
			}
			schemaRepo := &metadataUpdateFieldSchemaRepo{}
			ms := NewMetadataService(&metadataUpdateFieldRepo{schema: obj}, NewSchemaManager(schemaRepo), nil)
			ms.SetValidationService(NewValidationService(nil))

			err := ms.UpdateField(context.Background(), "Line__c", "SortOrder__c", &models.FieldMetadata{
				SiblingMaxParentField: &parentField,
			})
			if err == nil {
				t.Fatal("UpdateField accepted siblingMaxParentField on an existing decimal numeric target")
			}
			if !strings.Contains(err.Error(), "integer-compatible numeric target") {
				t.Fatalf("UpdateField error = %v, want integer-compatible numeric target diagnostic", err)
			}
			if schemaRepo.saved != nil {
				t.Fatal("SaveFieldMetadataWithIDs was called for invalid decimal siblingMaxParentField target")
			}
		})
	}
}

func TestUpdateFieldSiblingMaxRejectsExistingNonWriteableTarget(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	parentField := "Parent__c"
	cases := []struct {
		name                  string
		createable            bool
		updateable            bool
		writeProtectionPolicy string
	}{
		{name: "not_createable", createable: false, updateable: true},
		{name: "not_updateable", createable: true, updateable: false},
		{name: "intrinsic_write_protection", createable: true, updateable: true, writeProtectionPolicy: constants.FieldWriteProtectionTrustedOnly},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			obj := &models.ObjectMetadata{
				ID:      "obj_line",
				APIName: "Line__c",
				Fields: []models.FieldMetadata{
					{APIName: parentField, Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
					{APIName: "SortOrder__c", Type: models.FieldType("integer"), Createable: tc.createable, Updateable: tc.updateable, WriteProtectionPolicy: tc.writeProtectionPolicy},
				},
			}
			schemaRepo := &metadataUpdateFieldSchemaRepo{}
			ms := NewMetadataService(&metadataUpdateFieldRepo{schema: obj}, NewSchemaManager(schemaRepo), nil)
			ms.SetValidationService(NewValidationService(nil))

			err := ms.UpdateField(context.Background(), "Line__c", "SortOrder__c", &models.FieldMetadata{
				SiblingMaxParentField: &parentField,
			})
			if err == nil {
				t.Fatal("UpdateField accepted siblingMaxParentField on an existing non-writeable target")
			}
			if !strings.Contains(err.Error(), "createable and updateable") {
				t.Fatalf("UpdateField error = %v, want createable/updateable diagnostic", err)
			}
			if schemaRepo.saved != nil {
				t.Fatal("SaveFieldMetadataWithIDs was called for invalid non-writeable siblingMaxParentField target")
			}
		})
	}
}

func TestUpdateFieldSiblingMaxRejectsParentFieldMutation(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	parentField := "Parent__c"
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: parentField, Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
			{APIName: "SortOrder__c", Type: models.FieldType("integer"), Createable: true, Updateable: true, SiblingMaxParentField: &parentField},
		},
	}
	schemaRepo := &metadataUpdateFieldSchemaRepo{}
	ms := NewMetadataService(&metadataUpdateFieldRepo{schema: obj}, NewSchemaManager(schemaRepo), nil)
	ms.SetValidationService(NewValidationService(nil))

	err := ms.UpdateField(context.Background(), "Line__c", parentField, &models.FieldMetadata{
		Type: constants.FieldTypeString,
	})
	if err == nil {
		t.Fatal("UpdateField accepted a mutation that invalidates an existing siblingMaxParentField")
	}
	if !strings.Contains(err.Error(), "non-polymorphic reference") {
		t.Fatalf("UpdateField error = %v, want non-polymorphic reference diagnostic", err)
	}
	if schemaRepo.saved != nil {
		t.Fatal("SaveFieldMetadataWithIDs was called for invalid siblingMaxParentField parent mutation")
	}
	if len(schemaRepo.modified) != 0 {
		t.Fatalf("ModifyColumn calls = %d, want 0 on rejected siblingMaxParentField parent mutation", len(schemaRepo.modified))
	}
}

func TestUpdateFieldSiblingMaxRejectsParentFieldMutationToDerivedReference(t *testing.T) {
	installSiblingMaxFieldProvenance(t)
	parentField := "Parent__c"
	formula := "OwnerId"
	obj := &models.ObjectMetadata{
		ID:      "obj_line",
		APIName: "Line__c",
		Fields: []models.FieldMetadata{
			{APIName: parentField, Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent__c"}, Createable: true, Updateable: true},
			{APIName: "SortOrder__c", Type: models.FieldType("integer"), Createable: true, Updateable: true, SiblingMaxParentField: &parentField},
		},
	}
	schemaRepo := &metadataUpdateFieldSchemaRepo{}
	ms := NewMetadataService(&metadataUpdateFieldRepo{schema: obj}, NewSchemaManager(schemaRepo), nil)
	ms.SetValidationService(NewValidationService(nil))

	err := ms.UpdateField(context.Background(), "Line__c", parentField, &models.FieldMetadata{
		Formula: &formula,
	})
	if err == nil {
		t.Fatal("UpdateField accepted a mutation that makes siblingMaxParentField parent derived")
	}
	if !strings.Contains(err.Error(), "persisted non-polymorphic reference") {
		t.Fatalf("UpdateField error = %v, want persisted non-polymorphic reference diagnostic", err)
	}
	if schemaRepo.saved != nil {
		t.Fatal("SaveFieldMetadataWithIDs was called for invalid siblingMaxParentField derived parent mutation")
	}
	if len(schemaRepo.modified) != 0 {
		t.Fatalf("ModifyColumn calls = %d, want 0 on rejected siblingMaxParentField derived parent mutation", len(schemaRepo.modified))
	}
}
