package persistence_test

import (
	"testing"

	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// decoratedPermissionSetSchema mirrors what permissions.SchemaDecorator
// produces at cache-build time: virtual fields with IsVirtual=true and
// VirtualKind=SystemPermission, plus a FieldsMap keyed by lowercase
// APIName so case-insensitive lookup works.
func decoratedPermissionSetSchema() *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName: constants.TablePermissionSet,
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString},
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				APIName:              "PermissionsViewSetup",
				Type:                 constants.FieldTypeBoolean,
				IsVirtual:            true,
				VirtualKind:          constants.VirtualFieldSystemPermission,
				VirtualSourceAPIName: "ViewSetup",
			},
			{
				APIName:              "PermissionsModifyAllData",
				Type:                 constants.FieldTypeBoolean,
				IsVirtual:            true,
				VirtualKind:          constants.VirtualFieldSystemPermission,
				VirtualSourceAPIName: "ModifyAllData",
			},
		},
	}
	schema.FieldsMap = models.CloneWithAdditionalFields(schema, nil).FieldsMap
	return schema
}

func TestExtractPermissionGrants_CaseInsensitiveKeys(t *testing.T) {
	schema := decoratedPermissionSetSchema()
	// Mixed-case keys as a frontend / CSV normalizer might produce.
	record := models.SObject{
		"PermissionsViewSetup":      true,
		"permissionsmodifyalldata":  true,
		"Name":                      "Sales",
	}

	got, err := persistence.ExtractPermissionGrants(record, schema)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if got["ViewSetup"] != true {
		t.Errorf("mixed-case PascalCase key not extracted: got %+v", got)
	}
	if got["ModifyAllData"] != true {
		t.Errorf("lowercase key not extracted (case-sensitivity regression): got %+v", got)
	}
	// Matched keys must be removed from the record so ValidateSchemaCompliance
	// does not double-process them.
	if _, lingering := record["PermissionsViewSetup"]; lingering {
		t.Error("matched virtual key not deleted from record")
	}
	if _, lingering := record["permissionsmodifyalldata"]; lingering {
		t.Error("matched virtual key not deleted from record (lowercase)")
	}
	// Non-virtual fields must be left in place for downstream validation.
	if record["Name"] != "Sales" {
		t.Errorf("physical field disturbed: got %+v", record)
	}
}

func TestExtractPermissionGrants_NonPermissionSetSchemaReturnsEmpty(t *testing.T) {
	accountSchema := &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: "Name"},
			{APIName: "PermissionsViewSetup"}, // Not a virtual on Account — just a coincidental name.
		},
	}
	accountSchema.FieldsMap = models.CloneWithAdditionalFields(accountSchema, nil).FieldsMap

	record := models.SObject{"PermissionsViewSetup": true, "Name": "Acme"}
	got, err := persistence.ExtractPermissionGrants(record, accountSchema)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(got) != 0 {
		t.Errorf("non-PermissionSet schema must not yield grants: got %+v", got)
	}
	// Record must be untouched — the field looked permission-like by name but
	// is not marked virtual, so it belongs to the physical table.
	if _, ok := record["PermissionsViewSetup"]; !ok {
		t.Error("ExtractPermissionGrants deleted a non-virtual field")
	}
}

func TestExtractPermissionGrants_NilSchemaIsSafe(t *testing.T) {
	got, err := persistence.ExtractPermissionGrants(models.SObject{"X": 1}, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(got) != 0 {
		t.Errorf("nil schema should yield empty grants: got %+v", got)
	}
}

func TestExtractPermissionGrants_FailsLoudOnNonBool(t *testing.T) {
	schema := decoratedPermissionSetSchema()
	// Post-Normalize (strict), a value that couldn't be coerced survives as
	// its original non-bool type. Extract must return an error rather than
	// silently treating it as false — the review #1831 P1 concern.
	cases := []struct {
		name  string
		value interface{}
	}{
		{"unparseable_string", "not-a-bool"},
		{"map_value", map[string]interface{}{"nested": true}},
		{"slice_value", []interface{}{"a", "b"}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			record := models.SObject{"PermissionsViewSetup": tc.value}
			_, err := persistence.ExtractPermissionGrants(record, schema)
			if err == nil {
				t.Fatalf("expected error for non-bool %T, got nil", tc.value)
			}
		})
	}
}

// Pairing test for the new coercion contract: callers run NormalizeSObject
// BEFORE ExtractPermissionGrants. When Normalize produced a clean bool,
// extract records it. When Normalize couldn't coerce (strict default —
// leaves unknown types alone), extract returns an error instead of
// silently recording false (review #1831 P1).
func TestExtractPermissionGrants_TrustsNormalize(t *testing.T) {
	schema := decoratedPermissionSetSchema()
	cases := []struct {
		name    string
		value   interface{}
		want    bool
		wantErr bool
	}{
		{"true_bool", true, true, false},
		{"false_bool", false, false, false},
		{"nil_value", nil, false, false}, // nil is legal — treat as not-granted
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			record := models.SObject{"PermissionsViewSetup": tc.value}
			got, err := persistence.ExtractPermissionGrants(record, schema)
			if (err != nil) != tc.wantErr {
				t.Fatalf("err: want=%v got=%v (%v)", tc.wantErr, err != nil, err)
			}
			if err == nil && got["ViewSetup"] != tc.want {
				t.Errorf("value %v (%T): want %v, got %v", tc.value, tc.value, tc.want, got["ViewSetup"])
			}
		})
	}
}

// End-to-end coercion test: call NormalizeSObject then ExtractPermissionGrants
// in the same order the write-path does. Locks the fix for the review #1831
// P1 bug — the string "true" must become an ENABLED grant, not silently a
// revoked one. Without this pairing, regressions could re-introduce the
// silent-coerce-to-false behavior via a separate pre-extract path.
func TestExtractPermissionGrants_StringTrueViaNormalize(t *testing.T) {
	schema := decoratedPermissionSetSchema()
	record := models.SObject{"PermissionsViewSetup": "true"}
	// Step 1 — pre-extract normalization, as performed by the write path.
	normalized := persistence.NormalizeSObject(schema, record)
	// Step 2 — virtual-field extract.
	got, err := persistence.ExtractPermissionGrants(normalized, schema)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got["ViewSetup"] != true {
		t.Fatalf("string 'true' after Normalize+Extract should yield enabled=true; got %+v", got)
	}
}

// The end-to-end garbage-rejection case: unparseable string survives Normalize
// strict default, Extract rejects it loud. This is the behavior that replaces
// the pre-P12 silent-revoke.
func TestExtractPermissionGrants_GarbageStringRejectedViaNormalize(t *testing.T) {
	schema := decoratedPermissionSetSchema()
	record := models.SObject{"PermissionsViewSetup": "maybe"}
	normalized := persistence.NormalizeSObject(schema, record)
	_, err := persistence.ExtractPermissionGrants(normalized, schema)
	if err == nil {
		t.Fatal("unparseable-bool string after Normalize+Extract must error, not silently coerce to false")
	}
}

// Review #1831 P2 parity test: virtual boolean fields must accept every
// input that a physical boolean field would. Pre-fix, NormalizeSObject's
// inline bool case only recognized literal "true"/"false" strings; a
// payload `PermissionsViewSetup: "1"` was stripped / treated as error
// while the identical value on a physical boolean field passed
// ValidateSchemaCompliance via strconv.ParseBool. Lock-step between
// NormalizeSObject's coercion and ValidateSchemaCompliance's acceptance
// is what makes the virtual-field pipeline "fully metadata-driven"
// rather than a narrower side channel.
func TestExtractPermissionGrants_BooleanParityWithPhysical(t *testing.T) {
	schema := decoratedPermissionSetSchema()
	// Only the types ValidateSchemaCompliance.validateType accepts for
	// physical boolean fields — locking parity between the two.
	// int / int64 / []uint8 are deliberately NOT in this list: Validate
	// rejects them on physicals, so Normalize must not coerce them on
	// virtuals either (doing so would let virtuals silently accept
	// values the physical pipeline rejects).
	cases := []struct {
		name  string
		value interface{}
		want  bool
	}{
		{"bool_true", true, true},
		{"bool_false", false, false},
		{"float64_1", float64(1), true},
		{"float64_0", float64(0), false},
		{"string_true_lower", "true", true},
		{"string_TRUE", "TRUE", true},
		{"string_True", "True", true},
		{"string_1", "1", true},
		{"string_t", "t", true},
		{"string_T", "T", true},
		{"string_false_lower", "false", false},
		{"string_FALSE", "FALSE", false},
		{"string_0", "0", false},
		{"string_f", "f", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			record := models.SObject{"PermissionsViewSetup": tc.value}
			normalized := persistence.NormalizeSObject(schema, record)
			got, err := persistence.ExtractPermissionGrants(normalized, schema)
			if err != nil {
				t.Fatalf("value %v (%T) after Normalize: want accepted, got error: %v", tc.value, tc.value, err)
			}
			if got["ViewSetup"] != tc.want {
				t.Fatalf("value %v (%T): want grant=%v, got %v", tc.value, tc.value, tc.want, got["ViewSetup"])
			}
		})
	}
}
