package records

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	eventFeature "github.com/nexuscrm/events"
	metadataFeature "github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// stubProjectionPort is a test double for ActiveFormulaProjectionPort.
type stubProjectionPort struct {
	row                 models.SObject
	err                 error
	liveCalls           int
	includeDeletedCalls int
	selectFields        []string
}

func (s *stubProjectionPort) QueryActiveProjectionRow(_ context.Context, _ *models.UserSession, _, _ string, includeDeleted bool, selectFields []string) (models.SObject, error) {
	if includeDeleted {
		s.includeDeletedCalls++
	} else {
		s.liveCalls++
	}
	s.selectFields = append([]string(nil), selectFields...)
	return s.row, s.err
}

func activeFieldMeta(apiName string) models.FieldMetadata {
	mode := "active"
	formula := "IF(Score__c >= 50, 'Hot', 'Cold')"
	return models.FieldMetadata{
		APIName:     apiName,
		FormulaMode: &mode,
		Formula:     &formula,
	}
}

func plainFieldMeta(apiName string) models.FieldMetadata {
	return models.FieldMetadata{APIName: apiName}
}

func schemaWith(fields ...models.FieldMetadata) *models.ObjectMetadata {
	return &models.ObjectMetadata{
		APIName: "Widget__c",
		Fields:  fields,
	}
}

// TestActiveFormulaProjector_NilSchema — no panic, returns (nil, nil).
func TestActiveFormulaProjector_NilSchema(t *testing.T) {
	p := newActiveFormulaProjector(&stubProjectionPort{})
	snap, err := p.project(context.Background(), nil, nil, "001")
	if err != nil {
		t.Fatalf("want nil err, got %v", err)
	}
	if snap != nil {
		t.Fatalf("want nil snap for nil schema, got %v", snap)
	}
}

// TestActiveFormulaProjector_NoActiveFields — schema has no active fields.
func TestActiveFormulaProjector_NoActiveFields(t *testing.T) {
	schema := schemaWith(plainFieldMeta("Score__c"), plainFieldMeta("Name"))
	p := newActiveFormulaProjector(&stubProjectionPort{row: models.SObject{"Score__c": float64(10)}})
	snap, err := p.project(context.Background(), nil, schema, "001")
	if err != nil {
		t.Fatalf("want nil err for no-active-fields schema, got %v", err)
	}
	if snap != nil {
		t.Fatalf("want nil snap for no-active-fields schema, got %v", snap)
	}
}

// TestActiveFormulaProjector_QueryError — port returns error.
func TestActiveFormulaProjector_QueryError(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	sentinel := errors.New("db down")
	p := newActiveFormulaProjector(&stubProjectionPort{err: sentinel})
	_, err := p.project(context.Background(), nil, schema, "001")
	if err == nil {
		t.Fatal("want error from query failure, got nil")
	}
	if !errors.Is(err, sentinel) {
		t.Fatalf("want sentinel wrapped in error, got %v", err)
	}
}

// TestActiveFormulaProjector_CycleErrorFromProjectionQueryFailsClosed locks the
// current runtime contract for #1368 on main: flow-visible active fields are
// projected through the formula-view query path, so a circular derived-view
// dependency must surface as a hard error here rather than triggering any
// recursive in-memory fallback.
func TestActiveFormulaProjector_CycleErrorFromProjectionQueryFailsClosed(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	sentinel := errors.New("circular derived-view dependency detected among Child__c and Parent__c")
	p := newActiveFormulaProjector(&stubProjectionPort{err: sentinel})
	snap, err := p.project(context.Background(), nil, schema, "001")
	if err == nil {
		t.Fatal("want cycle error from projection query, got nil")
	}
	if !errors.Is(err, sentinel) {
		t.Fatalf("want cycle sentinel wrapped in error, got %v", err)
	}
	if snap != nil {
		t.Fatalf("want nil snap on cycle error, got %v", snap)
	}
}

// TestActiveFormulaProjector_RowNotFound_FailClosed — view row absent with a
// schema that has active fields is a view drift / eventual-consistency
// condition; project() MUST fail-closed so the listener does not silently
// fall back to the raw payload (which is exactly the #1362 bug class).
func TestActiveFormulaProjector_RowNotFound_FailClosed(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	p := newActiveFormulaProjector(&stubProjectionPort{row: nil})
	_, err := p.project(context.Background(), nil, schema, "001")
	if err == nil {
		t.Fatal("want error when view row absent (fail-closed), got nil")
	}
}

// TestActiveFormulaProjector_ProjectFields_ScopedStrictness — the strict
// variant restricted to a field subset (#3945 approval criteria: read
// authority capped at what the criteria author wrote) must (a) SELECT only
// the subset, (b) fail closed when a SUBSET field is absent from the view
// row, and (c) NOT fail when an UNRELATED active field is absent — drift on
// a field the caller never asked about must not fail a scoped read.
func TestActiveFormulaProjector_ProjectFields_ScopedStrictness(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"), activeFieldMeta("Margin__c"), plainFieldMeta("Score__c"))

	// (a)+(c): row carries only the requested field; the other active field
	// is missing from the view row entirely.
	port := &stubProjectionPort{row: models.SObject{"Id": "001", "Margin__c": float64(42)}}
	p := newActiveFormulaProjector(port)
	snap, err := p.projectFields(context.Background(), nil, schema, "001", []string{"Margin__c"})
	if err != nil {
		t.Fatalf("scoped project() error = %v, want nil: unrelated active-field drift must not fail a scoped read", err)
	}
	if got := snap["Margin__c"]; got != float64(42) {
		t.Fatalf("snap[Margin__c] = %v, want 42", got)
	}
	if _, present := snap["StatusActive__c"]; present {
		t.Fatalf("snap carries unrequested active field: %v", snap)
	}
	if len(port.selectFields) != 1 || port.selectFields[0] != "Margin__c" {
		t.Fatalf("view SELECT = %v, want exactly [Margin__c]", port.selectFields)
	}

	// (b): the requested field itself absent from the row stays fail-closed.
	p = newActiveFormulaProjector(&stubProjectionPort{row: models.SObject{"Id": "001"}})
	if _, err := p.projectFields(context.Background(), nil, schema, "001", []string{"Margin__c"}); err == nil {
		t.Fatal("scoped project() error = nil, want fail-closed when the requested field is absent from the view row")
	}

	// Case-insensitive subset matching; names that are not active fields of
	// the schema are ignored (no matching active fields -> nothing to
	// project).
	snap, err = p.projectFields(context.Background(), nil, schema, "001", []string{"score__c", "NoSuchField__c"})
	if err != nil || snap != nil {
		t.Fatalf("scoped project() = (%v, %v), want (nil, nil) when no requested name is an active field", snap, err)
	}
}

// TestActiveFormulaProjector_Success — happy path.
func TestActiveFormulaProjector_Success(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"), plainFieldMeta("Score__c"))
	row := models.SObject{
		"Id":              "001",
		"Score__c":        float64(20),
		"StatusActive__c": "Cold",
	}
	port := &stubProjectionPort{row: row}
	p := newActiveFormulaProjector(port)
	snap, err := p.project(context.Background(), nil, schema, "001")
	if err != nil {
		t.Fatalf("want nil err, got %v", err)
	}
	if snap == nil {
		t.Fatal("want non-nil snap")
	}
	// Only active field should be in snap.
	if v, ok := snap["StatusActive__c"]; !ok || v != "Cold" {
		t.Fatalf("want StatusActive__c=Cold, got %v (ok=%v)", v, ok)
	}
	// Non-active fields must NOT be projected.
	if _, ok := snap["Score__c"]; ok {
		t.Fatal("non-active field Score__c must not be in snap")
	}
	if len(port.selectFields) != 1 || port.selectFields[0] != "StatusActive__c" {
		t.Fatalf("projection select fields = %#v, want only active field StatusActive__c", port.selectFields)
	}
}

func TestActiveFormulaProjector_ProjectIncludingDeletedUsesScanAllPort(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	row := models.SObject{"StatusActive__c": "DeletedHot"}
	port := &stubProjectionPort{row: row}
	p := newActiveFormulaProjector(port)

	snap, err := p.projectIncludingDeleted(context.Background(), nil, schema, "001")
	if err != nil {
		t.Fatalf("projectIncludingDeleted: %v", err)
	}
	if port.includeDeletedCalls != 1 || port.liveCalls != 0 {
		t.Fatalf("projection calls live=%d includeDeleted=%d, want live=0 includeDeleted=1", port.liveCalls, port.includeDeletedCalls)
	}
	if v := snap["StatusActive__c"]; v != "DeletedHot" {
		t.Fatalf("StatusActive__c = %#v, want DeletedHot", v)
	}
}

// TestActiveFormulaProjector_MissingFieldKey — fail-closed on missing active
// field key in view row (view/metadata drift).
func TestActiveFormulaProjector_MissingFieldKey(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	// The view row exists but is missing the active field key.
	row := models.SObject{"Id": "001", "OtherField__c": "x"}
	p := newActiveFormulaProjector(&stubProjectionPort{row: row})
	_, err := p.project(context.Background(), nil, schema, "001")
	if err == nil {
		t.Fatal("want error on missing active field key (fail-closed), got nil")
	}
}

// TestStageOldRecordActiveProjection_StampsSnapAndClearsInherited locks the
// slice-2 helper contract used by both PersistenceService.Update and
// PersistenceService.CommitPreparedUpdate (Codex #1403 R1 HIGH: both Update
// entry points must stage the snapshot identically; without this helper
// CommitPreparedUpdate path was emitting RecordUpdated without $Record__Old
// active fields).
//
// Asserts:
//   - any inherited active_projection_old_record on ctx is cleared
//   - attempted=true is stamped
//   - the projected oldSnap is stamped onto ctx's old-record key
//   - the projected snap carries the recordID under "Id" (scope marker)
func TestStageOldRecordActiveProjection_StampsSnapAndClearsInherited(t *testing.T) {
	const recordID = "001000000000ABCAAA"
	const outerLeakID = "001000000000ZZZAAA"

	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	// Stub port returns a snap for THIS record.
	row := models.SObject{"StatusActive__c": "Hot"}
	port := &stubProjectionPort{row: row}

	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{schema: schema}, nil, nil)
	ps := &PersistenceService{
		metadata:               metaSvc,
		activeFormulaProjector: newActiveFormulaProjector(port),
	}

	// Pre-stamp ctx with an outer caller's snap (simulates nested-write
	// inheritance — what Clear must wipe).
	ctx := context.Background()
	leakedNew, _ := json.Marshal(models.SObject{"Id": outerLeakID, "StatusActive__c": "Leaked"})
	leakedOld, _ := json.Marshal(models.SObject{"Id": outerLeakID, "StatusActive__c": "LeakedOld"})
	ctx = context.WithValue(ctx, constants.ContextKey("active_projection_record"), string(leakedNew))
	ctx = context.WithValue(ctx, constants.ContextKey("active_projection_old_record"), string(leakedOld))

	stagedCtx, err := ps.stageOldRecordActiveProjection(ctx, schema, recordID, nil)
	if err != nil {
		t.Fatalf("stageOldRecordActiveProjection: %v", err)
	}

	if !eventFeature.ActiveProjectionAttempted(stagedCtx) {
		t.Error("staged ctx must have attempted=true")
	}

	// New-record key must be cleared (no leaked outer snap surviving on ctx).
	newSnap, _ := eventFeature.ActiveProjectionRecordFromCtx(stagedCtx)
	if newSnap != nil {
		t.Errorf("staged ctx must NOT carry a new-record snap (helper only stamps old): got %v", newSnap)
	}

	// Old-record key must be the freshly-projected snap with the right Id.
	oldSnap, oldErr := eventFeature.ActiveProjectionOldRecordFromCtx(stagedCtx)
	if oldErr != nil {
		t.Fatalf("decode staged old snap: %v", oldErr)
	}
	if oldSnap == nil {
		t.Fatal("staged ctx must carry the projected old snap")
	}
	if v, _ := oldSnap[constants.FieldID].(string); v != recordID {
		t.Errorf("staged old snap Id: want %q (scope marker), got %q (leak risk if mismatched)", recordID, v)
	}
	if v, _ := oldSnap["StatusActive__c"].(string); v != "Hot" {
		t.Errorf("staged old snap StatusActive__c: want Hot (from projector), got %q (leak from outer would be 'LeakedOld')", v)
	}
}

// TestStageOldRecordActiveProjection_NilProjector_PassesThrough — when the
// projector adapter isn't wired, the helper must return ctx unchanged
// (degraded mode, no error).
func TestStageOldRecordActiveProjection_NilProjector_PassesThrough(t *testing.T) {
	ps := &PersistenceService{} // no activeFormulaProjector
	ctx := context.Background()
	got, err := ps.stageOldRecordActiveProjection(ctx, nil, "001", nil)
	if err != nil {
		t.Fatalf("nil projector: want nil err, got %v", err)
	}
	if got != ctx {
		t.Errorf("nil projector: want ctx unchanged, got new ctx (helper should pass through)")
	}
}

// ---------------------------------------------------------------------------
// projectBestEffort + RecomputeActiveFormulaProjection (slice 4)
// ---------------------------------------------------------------------------

// TestActiveFormulaProjector_BestEffort_MissingFieldKey_SilentSkip locks
// the read-side contract: projectBestEffort treats a missing active-field
// key as FLS-hidden (silent skip), NOT view drift. This is the variant
// FlowExecutor uses post-RecordUpdate to avoid surfacing a partial-commit
// regression for user-invoked flows.
func TestActiveFormulaProjector_BestEffort_MissingFieldKey_SilentSkip(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"), activeFieldMeta("Other__c"))
	// View row has only one of the two active fields (the other is FLS-hidden).
	row := models.SObject{"Id": "001", "StatusActive__c": "Hot"}
	p := newActiveFormulaProjector(&stubProjectionPort{row: row})
	snap, err := p.projectBestEffort(context.Background(), nil, schema, "001", nil)
	if err != nil {
		t.Fatalf("best-effort must not fail on missing key (FLS-hidden), got %v", err)
	}
	if snap == nil {
		t.Fatal("best-effort: want non-nil snap when at least one active field is readable")
	}
	if v := snap["StatusActive__c"]; v != "Hot" {
		t.Errorf("StatusActive__c: want Hot, got %v", v)
	}
	if _, ok := snap["Other__c"]; ok {
		t.Errorf("FLS-hidden Other__c must not appear in best-effort snap")
	}
	if v, _ := snap[constants.FieldID].(string); v != "001" {
		t.Errorf("snap must carry Id scope marker: got %q, want 001", v)
	}
}

// TestActiveFormulaProjector_BestEffort_RowAbsent_SilentNil locks that
// an absent view row under user context returns (nil, nil) — RLS hid the
// row, caller keeps existing snapshot.
func TestActiveFormulaProjector_BestEffort_RowAbsent_SilentNil(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	p := newActiveFormulaProjector(&stubProjectionPort{row: nil})
	snap, err := p.projectBestEffort(context.Background(), nil, schema, "001", nil)
	if err != nil {
		t.Fatalf("best-effort must not fail on absent row (RLS-hidden), got %v", err)
	}
	if snap != nil {
		t.Errorf("best-effort: want nil snap on absent row, got %v", snap)
	}
}

// TestRecomputeActiveFormulaProjection_NilProjector — degraded mode.
func TestRecomputeActiveFormulaProjection_NilProjector(t *testing.T) {
	ps := &PersistenceService{}
	snap, err := ps.RecomputeActiveFormulaProjection(context.Background(), nil, "Widget__c", "001", nil)
	if err != nil {
		t.Fatalf("nil projector: want nil err, got %v", err)
	}
	if snap != nil {
		t.Fatalf("nil projector: want nil snap, got %v", snap)
	}
}

// TestRecomputeActiveFormulaProjection_EmptyArgs — degraded inputs.
func TestRecomputeActiveFormulaProjection_EmptyArgs(t *testing.T) {
	port := &stubProjectionPort{row: models.SObject{"StatusActive__c": "Hot"}}
	ps := &PersistenceService{activeFormulaProjector: newActiveFormulaProjector(port)}
	for _, tc := range []struct{ obj, id string }{
		{"", "001"},
		{"Widget__c", ""},
		{"", ""},
	} {
		snap, err := ps.RecomputeActiveFormulaProjection(context.Background(), nil, tc.obj, tc.id, nil)
		if err != nil {
			t.Errorf("obj=%q id=%q: want nil err, got %v", tc.obj, tc.id, err)
		}
		if snap != nil {
			t.Errorf("obj=%q id=%q: want nil snap, got %v", tc.obj, tc.id, snap)
		}
	}
}

// TestRecomputeActiveFormulaProjection_BypassedCtx_FailsClosedOnAbsentRow
// locks the dispatch contract: under CRUD/FLS-bypassed ctx (record-
// triggered flows arrive elevated to SystemContext), Recompute must use
// the strict projector and surface view drift instead of silently returning
// nil. Without this dispatch a missing row under SystemContext would leave
// the outer caller branching on the pre-update active snapshot.
func TestRecomputeActiveFormulaProjection_BypassedCtx_FailsClosedOnAbsentRow(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	port := &stubProjectionPort{row: nil} // simulate view drift / rebuild lag

	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{schema: schema}, nil, nil)
	ps := &PersistenceService{
		metadata:               metaSvc,
		activeFormulaProjector: newActiveFormulaProjector(port),
	}

	ctx := constants.WithSecurityContext(context.Background(),
		constants.SystemContext, "test: triggered flow")

	snap, err := ps.RecomputeActiveFormulaProjection(ctx, nil, "Widget__c", "001", nil)
	if err == nil {
		t.Fatal("want fail-closed error under SystemContext when row absent, got nil")
	}
	if snap != nil {
		t.Errorf("want nil snap on fail-closed, got %v", snap)
	}
}

// TestRecomputeActiveFormulaProjection_UserCtx_SilentSkipOnAbsentRow —
// the other half of the dispatch: under user-level ctx an absent row is
// treated as RLS-hidden (silent (nil, nil)). Pairing with the BypassedCtx
// test ensures the strict vs best-effort selection actually switches on
// ctx security level.
func TestRecomputeActiveFormulaProjection_UserCtx_SilentSkipOnAbsentRow(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"))
	port := &stubProjectionPort{row: nil}

	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{schema: schema}, nil, nil)
	ps := &PersistenceService{
		metadata:               metaSvc,
		activeFormulaProjector: newActiveFormulaProjector(port),
	}

	// Default ctx is UserContext.
	snap, err := ps.RecomputeActiveFormulaProjection(context.Background(), nil, "Widget__c", "001", nil)
	if err != nil {
		t.Fatalf("want nil err under UserContext (RLS-hidden), got %v", err)
	}
	if snap != nil {
		t.Errorf("want nil snap on best-effort skip, got %v", snap)
	}
}

// TestRecomputeActiveFormulaProjection_BypassedCtx_SuccessPath verifies the
// happy path under SystemContext: projector returns a snap including the
// scope-marker Id and the active-formula fields.
func TestRecomputeActiveFormulaProjection_BypassedCtx_SuccessPath(t *testing.T) {
	schema := schemaWith(activeFieldMeta("StatusActive__c"), plainFieldMeta("Score__c"))
	row := models.SObject{"StatusActive__c": "Hot", "Score__c": float64(99)}
	port := &stubProjectionPort{row: row}

	metaSvc := metadataFeature.NewMetadataService(&stubUpsertMetaRepo{schema: schema}, nil, nil)
	ps := &PersistenceService{
		metadata:               metaSvc,
		activeFormulaProjector: newActiveFormulaProjector(port),
	}

	ctx := constants.WithSecurityContext(context.Background(),
		constants.SystemContext, "test: triggered flow")

	snap, err := ps.RecomputeActiveFormulaProjection(ctx, nil, "Widget__c", "001", nil)
	if err != nil {
		t.Fatalf("want nil err, got %v", err)
	}
	if snap == nil {
		t.Fatal("want non-nil snap")
	}
	if v, ok := snap["StatusActive__c"]; !ok || v != "Hot" {
		t.Errorf("want StatusActive__c=Hot, got %v (ok=%v)", v, ok)
	}
	if _, ok := snap["Score__c"]; ok {
		t.Errorf("non-active field Score__c must not appear in Recompute snap")
	}
	if v, _ := snap[constants.FieldID].(string); v != "001" {
		t.Errorf("snap must carry Id scope marker: got %q, want 001", v)
	}
}
