package records

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/formula/spec"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func changeStampEntrypointSchema() *models.ObjectMetadata {
	formulaExpr := `IF(OR(ISNEW(), ISCHANGED(StageName)), NOW(), PRIORVALUE(LastStageChangeDate))`
	formulaMode := "frozen"
	returnType := models.FieldType(constants.FieldTypeDatetime)
	schema := &models.ObjectMetadata{
		ID:           "obj_generic_record",
		APIName:      "GenericRecord",
		Label:        "Generic Record",
		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},
			{APIName: "StageName", Type: constants.FieldTypeString, Createable: true, Updateable: true},
			{
				ID:                    "last-stage-change-date",
				APIName:               "LastStageChangeDate",
				Type:                  constants.FieldTypeFormula,
				Formula:               &formulaExpr,
				FormulaMode:           &formulaMode,
				ReturnType:            &returnType,
				Createable:            false,
				Updateable:            false,
				WriteProtectionPolicy: constants.FieldWriteProtectionTrustedOnly,
			},
		},
	}
	schema.FieldsMap = make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}
	return schema
}

type formulaComputeRepoStub struct {
	record models.SObject
}

func (s formulaComputeRepoStub) FindOne(_ context.Context, _ string, _ string) (models.SObject, error) {
	return s.record, nil
}

type formulaComputeSchemaStub struct {
	schema *models.ObjectMetadata
}

func (s formulaComputeSchemaStub) GetSchema(_ context.Context, apiName string) *models.ObjectMetadata {
	if s.schema != nil && s.schema.APIName == apiName {
		return s.schema
	}
	return nil
}

func TestBuildRecordFetcherNormalizesFetchedRelationPercentBySchema(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{{
			APIName:          "WhoId",
			Type:             constants.FieldTypeReference,
			RelationshipName: recordsStringPtr("Who"),
			ReferenceTo:      []string{"Lead"},
		}},
	}
	leadSchema := &models.ObjectMetadata{
		APIName: "Lead",
		Fields:  []models.FieldMetadata{{APIName: "Score", Type: constants.FieldTypePercent}},
	}

	fetcher := buildRecordFetcher(context.Background(), schema, nil, formulaComputeRepoStub{
		record: models.SObject{"Id": "00Q000000000001AAA", "Score": 40},
	}, formulaComputeSchemaStub{schema: leadSchema})

	related, err := fetcher(map[string]interface{}{
		"WhoId":      "00Q000000000001AAA",
		"WhoId_type": "Lead",
	}, "Who")
	if err != nil {
		t.Fatalf("fetcher returned error: %v", err)
	}
	if got := related["Score"]; got != 0.4 {
		t.Fatalf("related Score = %#v, want 0.4 after schema-guided percent normalization", got)
	}
}

func TestComputeFormulaFieldsWritesPercentResultBackToStorageDomain(t *testing.T) {
	formulaExpr := "Probability"
	formulaMode := "frozen"
	returnType := models.FieldType(constants.FieldTypePercent)

	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Probability", Type: constants.FieldTypePercent},
			{
				ID:          "frozen-percent",
				APIName:     "FrozenPct__c",
				Type:        constants.FieldTypeFormula,
				Formula:     &formulaExpr,
				FormulaMode: &formulaMode,
				ReturnType:  &returnType,
			},
		},
	}
	row := models.SObject{"Probability": 40}

	computeFormulaFields(context.Background(), schema, row, formulaComputeRepoStub{}, nil, "Opportunity")

	if got := row["FrozenPct__c"]; got != 40.0 {
		t.Fatalf("FrozenPct__c = %#v, want 40.0 after 0.4 formula result is written back to storage domain", got)
	}
}

func TestComputeFormulaFieldsWithPriorSupportsChangeStampLifecycle(t *testing.T) {
	formulaExpr := `IF(ISNEW(), "created", IF(ISCHANGED(StageName), "changed", PRIORVALUE(StageStamp)))`
	formulaMode := "frozen"
	returnType := models.FieldType(constants.FieldTypeString)
	schema := &models.ObjectMetadata{
		APIName: "GenericRecord",
		Fields: []models.FieldMetadata{
			{APIName: "StageName", Type: constants.FieldTypeString},
			{
				ID:          "stage-stamp",
				APIName:     "StageStamp",
				Type:        constants.FieldTypeFormula,
				Formula:     &formulaExpr,
				FormulaMode: &formulaMode,
				ReturnType:  &returnType,
			},
		},
	}

	t.Run("create", func(t *testing.T) {
		row := models.SObject{"StageName": "A"}
		computeFormulaFieldsWithPrior(context.Background(), schema, row, nil, formulaComputeRepoStub{}, nil, schema.APIName)
		if got := row["StageStamp"]; got != "created" {
			t.Fatalf("StageStamp = %#v, want create stamp", got)
		}
	})

	t.Run("stage changed", func(t *testing.T) {
		prior := models.SObject{"StageName": "A", "StageStamp": "old"}
		row := models.SObject{"StageName": "B", "StageStamp": "old"}
		computeFormulaFieldsWithPrior(context.Background(), schema, row, prior, formulaComputeRepoStub{}, nil, schema.APIName)
		if got := row["StageStamp"]; got != "changed" {
			t.Fatalf("StageStamp = %#v, want changed stamp", got)
		}
	})

	t.Run("unrelated update preserves stamp", func(t *testing.T) {
		prior := models.SObject{"StageName": "A", "StageStamp": "old"}
		row := models.SObject{"StageName": "A", "StageStamp": "old"}
		computeFormulaFieldsWithPrior(context.Background(), schema, row, prior, formulaComputeRepoStub{}, nil, schema.APIName)
		if got := row["StageStamp"]; got != "old" {
			t.Fatalf("StageStamp = %#v, want prior stamp", got)
		}
	})
}

func TestComputeFormulaFieldsWithPriorSupportsDatetimeSelfPreservation(t *testing.T) {
	formulaExpr := `IF(OR(ISNEW(), ISCHANGED(StageName)), NOW(), PRIORVALUE(LastStageChangeDate))`
	formulaMode := "frozen"
	returnType := models.FieldType(constants.FieldTypeDatetime)
	schema := &models.ObjectMetadata{
		APIName: "GenericRecord",
		Fields: []models.FieldMetadata{
			{APIName: "StageName", Type: constants.FieldTypeString},
			{
				ID:          "last-stage-change-date",
				APIName:     "LastStageChangeDate",
				Type:        constants.FieldTypeFormula,
				Formula:     &formulaExpr,
				FormulaMode: &formulaMode,
				ReturnType:  &returnType,
			},
		},
	}

	created := models.SObject{"StageName": "A"}
	computeFormulaFieldsWithPrior(context.Background(), schema, created, nil, formulaComputeRepoStub{}, nil, schema.APIName)
	if got := created["LastStageChangeDate"]; got == nil || got == "" {
		t.Fatalf("create LastStageChangeDate = %#v, want NOW() value", got)
	}

	const oldStamp = "2026-08-18T12:00:00Z"
	prior := models.SObject{"StageName": "A", "LastStageChangeDate": oldStamp}
	unchanged := models.SObject{"StageName": "A", "LastStageChangeDate": oldStamp}
	computeFormulaFieldsWithPrior(context.Background(), schema, unchanged, prior, formulaComputeRepoStub{}, nil, schema.APIName)
	if got := unchanged["LastStageChangeDate"]; got != oldStamp {
		t.Fatalf("unchanged LastStageChangeDate = %#v, want %q", got, oldStamp)
	}

	changed := models.SObject{"StageName": "B", "LastStageChangeDate": oldStamp}
	computeFormulaFieldsWithPrior(context.Background(), schema, changed, prior, formulaComputeRepoStub{}, nil, schema.APIName)
	if got := changed["LastStageChangeDate"]; got == nil || got == "" || got == oldStamp {
		t.Fatalf("changed LastStageChangeDate = %#v, want fresh NOW() value", got)
	}
}

func TestFrozenChangeStampRecordLifecyclePreservesWriteProtection(t *testing.T) {
	schema := changeStampEntrypointSchema()

	t.Run("direct caller write is rejected", func(t *testing.T) {
		repo := &captureRecordRepo{}
		ps := newRecordEntrypointTestService(schema, repo)
		_, err := ps.Insert(context.Background(), schema.APIName, models.SObject{
			constants.FieldName:   "probe",
			"StageName":           "A",
			"LastStageChangeDate": "2026-08-18T12:00:00Z",
		}, recordEntrypointUser())
		if err == nil || !strings.Contains(err.Error(), constants.FieldWriteProtectionTrustedOnly) {
			t.Fatalf("direct protected insert error = %v, want trusted_only rejection", err)
		}
		if repo.inserted != nil {
			t.Fatalf("direct protected insert reached repository: %#v", repo.inserted)
		}
	})

	t.Run("create stamps", func(t *testing.T) {
		repo := &captureRecordRepo{}
		ps := newRecordEntrypointTestService(schema, repo)
		if _, err := ps.Insert(context.Background(), schema.APIName, models.SObject{
			constants.FieldName: "probe",
			"StageName":         "A",
		}, recordEntrypointUser()); err != nil {
			t.Fatalf("Insert() error = %v", err)
		}
		if got := repo.inserted["LastStageChangeDate"]; got == nil || got == "" {
			t.Fatalf("created LastStageChangeDate = %#v, want engine stamp", got)
		}
	})

	t.Run("stage update moves stamp", func(t *testing.T) {
		const oldStamp = "2026-08-18T12:00:00Z"
		repo := &captureRecordRepo{old: models.SObject{
			constants.FieldID:     "a00-row",
			constants.FieldName:   "probe",
			"StageName":           "A",
			"LastStageChangeDate": oldStamp,
		}}
		ps := newRecordEntrypointTestService(schema, repo)
		if err := ps.Update(context.Background(), schema.APIName, "a00-row", models.SObject{
			"StageName": "B",
		}, recordEntrypointUser()); err != nil {
			t.Fatalf("Update() error = %v", err)
		}
		if got := repo.updated["LastStageChangeDate"]; got == nil || got == "" || got == oldStamp {
			t.Fatalf("updated LastStageChangeDate = %#v, want fresh engine stamp", got)
		}
	})

	t.Run("unrelated update holds stamp", func(t *testing.T) {
		const oldStamp = "2026-08-18T12:00:00Z"
		repo := &captureRecordRepo{old: models.SObject{
			constants.FieldID:     "a00-row",
			constants.FieldName:   "probe",
			"StageName":           "A",
			"LastStageChangeDate": oldStamp,
		}}
		ps := newRecordEntrypointTestService(schema, repo)
		if err := ps.Update(context.Background(), schema.APIName, "a00-row", models.SObject{
			constants.FieldName: "renamed",
		}, recordEntrypointUser()); err != nil {
			t.Fatalf("Update() error = %v", err)
		}
		if _, changed := repo.updated["LastStageChangeDate"]; changed {
			t.Fatalf("unrelated update rewrote LastStageChangeDate: %#v", repo.updated)
		}
	})
}

func TestComputeFormulaFieldsRenormalizesPercentReturningFormulaForChainedReads(t *testing.T) {
	percentFormulaExpr := "Probability"
	doubleFormulaExpr := "FrozenPct__c * 2"
	formulaMode := "frozen"
	percentReturnType := models.FieldType(constants.FieldTypePercent)

	schema := &models.ObjectMetadata{
		APIName: "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "Probability", Type: constants.FieldTypePercent},
			{
				ID:          "frozen-percent",
				APIName:     "FrozenPct__c",
				Type:        constants.FieldTypeFormula,
				Formula:     &percentFormulaExpr,
				FormulaMode: &formulaMode,
				ReturnType:  &percentReturnType,
			},
			{
				ID:          "double-frozen-percent",
				APIName:     "DoubleFrozenPct__c",
				Type:        constants.FieldTypeFormula,
				Formula:     &doubleFormulaExpr,
				FormulaMode: &formulaMode,
				ReturnType:  &percentReturnType,
			},
		},
	}
	row := models.SObject{"Probability": 40}

	computeFormulaFields(context.Background(), schema, row, formulaComputeRepoStub{}, nil, "Opportunity")

	if got := row["FrozenPct__c"]; got != 40.0 {
		t.Fatalf("FrozenPct__c = %#v, want 40.0", got)
	}
	if got := row["DoubleFrozenPct__c"]; got != 80.0 {
		t.Fatalf("DoubleFrozenPct__c = %#v, want 80.0 after reading FrozenPct__c as 0.4 then writing back 80.0", got)
	}
}

func TestComputeFormulaFieldsWritesActiveWriteMaterializedFormula(t *testing.T) {
	formulaExpr := "Name"
	formulaMode := "active"
	formulaStorageMode := "write_materialized"
	returnType := models.FieldType(constants.FieldTypeString)

	schema := &models.ObjectMetadata{
		APIName: "WorkItem",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				ID:                 "active-materialized",
				APIName:            "NameStored",
				Type:               constants.FieldTypeFormula,
				Formula:            &formulaExpr,
				FormulaMode:        &formulaMode,
				FormulaStorageMode: &formulaStorageMode,
				ReturnType:         &returnType,
			},
		},
	}
	row := models.SObject{"Name": "Alpha"}

	computeFormulaFields(context.Background(), schema, row, formulaComputeRepoStub{}, nil, "WorkItem")

	if got := row["NameStored"]; got != "Alpha" {
		t.Fatalf("NameStored = %#v, want Alpha", got)
	}
}

func TestComputeFormulaFieldsUsesRuntimeFunctionBundlesRegisteredAfterPackageEngineInit(t *testing.T) {
	if err := formula.RegisterRuntimeFunctionBundles(nil); err != nil {
		t.Fatalf("clear runtime bundles before test: %v", err)
	}
	t.Cleanup(func() {
		_ = formula.RegisterRuntimeFunctionBundles(nil)
	})

	bundle := formula.FunctionBundle{Functions: []formula.FunctionDefinition{{
		Name:    "PACK_PREFIX",
		MinArgs: 1,
		MaxArgs: 1,
		Return:  formula.FunctionReturnDefinition{Kind: "fixed", Type: spec.TypeString},
		Args:    []spec.ArgSpec{{Type: spec.TypeString, Nullable: true}},
		Evaluator: formula.FunctionEvaluator{
			Kind:   formula.FormulaExprV1EvaluatorKind,
			Params: []string{"value"},
			Body:   `value + "-probe"`,
		},
	}}}
	if err := formula.RegisterRuntimeFunctionBundles([]formula.FunctionBundle{bundle}); err != nil {
		t.Fatalf("RegisterRuntimeFunctionBundles: %v", err)
	}

	formulaExpr := "PACK_PREFIX(Name)"
	formulaMode := "active"
	formulaStorageMode := "write_materialized"
	returnType := models.FieldType(constants.FieldTypeString)
	schema := &models.ObjectMetadata{
		APIName: "WorkItem",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				ID:                 "active-materialized-runtime-bundle",
				APIName:            "NameStored",
				Type:               constants.FieldTypeFormula,
				Formula:            &formulaExpr,
				FormulaMode:        &formulaMode,
				FormulaStorageMode: &formulaStorageMode,
				ReturnType:         &returnType,
			},
		},
	}
	row := models.SObject{"Name": "Alpha"}

	computeFormulaFields(context.Background(), schema, row, formulaComputeRepoStub{}, nil, "WorkItem")

	if got := row["NameStored"]; got != "Alpha-probe" {
		t.Fatalf("NameStored = %#v, want Alpha-probe from runtime formula bundle", got)
	}
}

func TestComputeFormulaFieldsSkipsActiveViewOnlyFormula(t *testing.T) {
	formulaExpr := "Name"
	formulaMode := "active"
	returnType := models.FieldType(constants.FieldTypeString)

	schema := &models.ObjectMetadata{
		APIName: "WorkItem",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				ID:          "active-view-only",
				APIName:     "NameEcho",
				Type:        constants.FieldTypeFormula,
				Formula:     &formulaExpr,
				FormulaMode: &formulaMode,
				ReturnType:  &returnType,
			},
		},
	}
	row := models.SObject{"Name": "Alpha"}

	computeFormulaFields(context.Background(), schema, row, formulaComputeRepoStub{}, nil, "WorkItem")

	if _, ok := row["NameEcho"]; ok {
		t.Fatalf("NameEcho was materialized into row = %#v, want view-only active formula to stay absent", row["NameEcho"])
	}
}

func TestComputeFormulaFieldsInvalidStorageModeFallsBackToActiveDefault(t *testing.T) {
	formulaExpr := "Name"
	formulaMode := "active"
	formulaStorageMode := "WRITE_MATERIALIZED"
	returnType := models.FieldType(constants.FieldTypeString)

	schema := &models.ObjectMetadata{
		APIName: "WorkItem",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				ID:                 "active-invalid-storage",
				APIName:            "NameEcho",
				Type:               constants.FieldTypeFormula,
				Formula:            &formulaExpr,
				FormulaMode:        &formulaMode,
				FormulaStorageMode: &formulaStorageMode,
				ReturnType:         &returnType,
			},
		},
	}
	row := models.SObject{"Name": "Alpha"}

	computeFormulaFields(context.Background(), schema, row, formulaComputeRepoStub{}, nil, "WorkItem")

	if _, ok := row["NameEcho"]; ok {
		t.Fatalf("NameEcho was materialized into row = %#v, want invalid storage mode to fall back to active view-only behavior", row["NameEcho"])
	}
}

func TestInjectComputedFormulaFieldsIncludesActiveWriteMaterializedFormula(t *testing.T) {
	formulaExpr := "Name"
	formulaMode := "active"
	formulaStorageMode := "write_materialized"
	returnType := models.FieldType(constants.FieldTypeString)

	schema := &models.ObjectMetadata{
		APIName: "WorkItem",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				ID:                 "active-materialized",
				APIName:            "NameStored",
				Type:               constants.FieldTypeFormula,
				Formula:            &formulaExpr,
				FormulaMode:        &formulaMode,
				FormulaStorageMode: &formulaStorageMode,
				ReturnType:         &returnType,
			},
		},
	}
	merged := models.SObject{
		"Name":       "Beta",
		"NameStored": "Beta",
	}
	oldRecord := models.SObject{
		"Name":       "Alpha",
		"NameStored": "Alpha",
	}
	effectiveUpdates := models.SObject{
		"Name": "Beta",
	}

	injectComputedFormulaFields(schema, merged, oldRecord, effectiveUpdates, nil)

	if got := effectiveUpdates["NameStored"]; got != "Beta" {
		t.Fatalf("effectiveUpdates[NameStored] = %#v, want Beta", got)
	}
}

func TestBuildRecordFetcherNormalizesFetchedParentFormulaPercentByReturnType(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Task",
		Fields: []models.FieldMetadata{{
			APIName:          "WhoId",
			Type:             constants.FieldTypeReference,
			RelationshipName: recordsStringPtr("Who"),
			ReferenceTo:      []string{"Lead"},
		}},
	}
	percentReturnType := models.FieldType(constants.FieldTypePercent)
	leadSchema := &models.ObjectMetadata{
		APIName: "Lead",
		Fields: []models.FieldMetadata{{
			APIName:    "FrozenScore__c",
			Type:       constants.FieldTypeFormula,
			ReturnType: &percentReturnType,
		}},
	}

	fetcher := buildRecordFetcher(context.Background(), schema, nil, formulaComputeRepoStub{
		record: models.SObject{"Id": "00Q000000000001AAA", "FrozenScore__c": 40.0},
	}, formulaComputeSchemaStub{schema: leadSchema})

	related, err := fetcher(map[string]interface{}{
		"WhoId":      "00Q000000000001AAA",
		"WhoId_type": "Lead",
	}, "Who")
	if err != nil {
		t.Fatalf("fetcher returned error: %v", err)
	}
	if got := related["FrozenScore__c"]; got != 0.4 {
		t.Fatalf("related FrozenScore__c = %#v, want 0.4 after ReturnType-aware percent normalization", got)
	}
}

func recordsStringPtr(v string) *string {
	return &v
}
