package models

import (
	"bytes"
	"encoding/json"
	"reflect"
	"testing"
)

// legacyLayoutConfig is the stored `_System_PageLayout.Config` shape as it
// existed before PageSection carried FieldBehaviors. Production rows look like
// this.
//
// Scope note: whole-PageLayout encoding is deliberately NOT asserted to be
// byte-identical — `createdDate` / `lastModifiedDate` are `time.Time`, and
// `omitempty` is a no-op on struct kinds, so they have always been re-emitted
// as zero timestamps. That predates this field and is not what this guard is
// for. What must hold is that adding FieldBehaviors
// changes nothing about a section that has none: an older binary decoding the
// re-encoded row, and every config fence that compares the raw JSON string,
// depend on no new key appearing.
const legacyLayoutConfig = `{"id":"curated_Account_default","objectApiName":"Account","layoutName":"Account Layout (Curated)","type":"Detail","compactLayout":["Name"],"sections":[{"id":"section_1","label":"Account Information","columns":2,"fields":["Name","Type","Rating"],"fieldLabels":{"Type":"Account Type"}}],"relatedLists":[],"headerActions":[],"quickActions":[]}`

func TestLegacyLayoutSectionsRoundTripWithoutFieldBehaviorsKey(t *testing.T) {
	var layout PageLayout
	if err := json.Unmarshal([]byte(legacyLayoutConfig), &layout); err != nil {
		t.Fatalf("unmarshal legacy config: %v", err)
	}

	if got := layout.Sections[0].FieldBehaviors; got != nil {
		t.Fatalf("legacy config decoded a non-nil FieldBehaviors: %#v", got)
	}

	encoded, err := json.Marshal(&layout)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}

	var before, after struct {
		Sections []map[string]any `json:"sections"`
	}
	if err := json.Unmarshal([]byte(legacyLayoutConfig), &before); err != nil {
		t.Fatalf("unmarshal expected: %v", err)
	}
	if err := json.Unmarshal(encoded, &after); err != nil {
		t.Fatalf("unmarshal actual: %v", err)
	}
	if !reflect.DeepEqual(before.Sections, after.Sections) {
		t.Fatalf("legacy sections did not round-trip\n before: %#v\n  after: %#v", before.Sections, after.Sections)
	}
	for i, section := range after.Sections {
		if _, present := section["fieldBehaviors"]; present {
			t.Fatalf("section %d gained a fieldBehaviors key: %s", i, encoded)
		}
	}
}

// The platform stores FieldBehaviors; it does not own the vocabulary. A mode
// key or behavior value the platform has never heard of must survive a
// decode/encode cycle unchanged, so an app layer can ship new vocabulary
// without a platform release and an authoring round-trip cannot silently
// discard what an admin wrote.
func TestPageSectionFieldBehaviorsPreserveUnknownVocabulary(t *testing.T) {
	// Every token below is deliberately hostile to a normalizer: mixed case at
	// all three levels (field API name, mode key, behavior value), a camelCase
	// mode key no current app layer defines, a value with surrounding spaces,
	// and a value with an embedded hyphen. An all-lowercase fixture would pass
	// even if something case-folded or trimmed on the way through, so it would
	// prove only the "not pruned" half of the promise.
	const stored = `{"id":"s1","label":"S","columns":2,"fields":["Name"],"fieldBehaviors":{"Name":{"create":"required","Edit":"Hidden","quickCreate":"ReadOnly","audit":"  Not-A-Known-Value  "},"OrphanField__c":{"create":"hidden"}}}`

	want := map[string]map[string]string{
		"Name": {
			"create":      "required",
			"Edit":        "Hidden",
			"quickCreate": "ReadOnly",
			"audit":       "  Not-A-Known-Value  ",
		},
		// An entry whose field is no longer in Fields stays inert rather than
		// being pruned, matching how FieldLabels already behaves.
		"OrphanField__c": {"create": "hidden"},
	}

	var section PageSection
	if err := json.Unmarshal([]byte(stored), &section); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if !reflect.DeepEqual(section.FieldBehaviors, want) {
		t.Fatalf("decode rewrote behaviors\n got: %#v\nwant: %#v", section.FieldBehaviors, want)
	}

	encoded, err := json.Marshal(&section)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}

	// Assert on the encoded BYTES too, not only on a re-decoded value: a
	// rewrite that is symmetric across encode and decode would survive a
	// decode/encode/decode comparison while still corrupting what is stored.
	for _, token := range []string{
		`"Edit":"Hidden"`,
		`"quickCreate":"ReadOnly"`,
		`"audit":"  Not-A-Known-Value  "`,
	} {
		if !bytes.Contains(encoded, []byte(token)) {
			t.Fatalf("encoded output lost or rewrote %s\n got: %s", token, encoded)
		}
	}

	var reEncoded PageSection
	if err := json.Unmarshal(encoded, &reEncoded); err != nil {
		t.Fatalf("re-unmarshal: %v", err)
	}
	if !reflect.DeepEqual(reEncoded.FieldBehaviors, want) {
		t.Fatalf("behaviors changed across round-trip: %#v", reEncoded.FieldBehaviors)
	}
}

func TestPageSectionEmptyFieldBehaviorsIsOmitted(t *testing.T) {
	encoded, err := json.Marshal(&PageSection{ID: "s1", Label: "S", Columns: 2, Fields: []string{"Name"}})
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var decoded map[string]any
	if err := json.Unmarshal(encoded, &decoded); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if _, present := decoded["fieldBehaviors"]; present {
		t.Fatalf("fieldBehaviors key emitted for a section that has none: %s", encoded)
	}
}
