package metadata

import (
	"context"
	"reflect"
	"testing"

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

// The platform commits to storing FieldBehaviors verbatim: entries are never
// rewritten and never pruned, including entries whose field is no longer in
// Sections[*].Fields. Only one read-time path removes fields from a served
// layout — stripAutonumberSectionFields, which runs on every ResolveLayout and
// GetLayoutByID — so that path is where "never pruned" is actually at risk, and
// a struct-level marshal round-trip in shared/pkg/models cannot observe it.
//
// This also pins the asymmetry deliberately: the field leaves Fields, its
// behavior entry stays. Pruning here would look like tidiness and would
// silently destroy admin-authored content on a plain read.
func TestResolveLayoutScrubKeepsBehaviorEntryForStrippedField(t *testing.T) {
	repo := &resolveLayoutFallbackRepo{
		schema: quoteSchemaWithAutonumberField(),
		layouts: []*models.PageLayout{{
			ID:                "curated_Quote_default",
			ObjectAPIName:     "Quote",
			LayoutName:        "Quote Layout",
			Source:            "curated",
			GenerationVersion: currentGenerationVersion,
			Sections: []models.PageSection{{
				ID:     "quote_information",
				Label:  "Quote Information",
				Fields: []string{"Name", "QuoteNumber", "Status"},
				FieldLabels: map[string]string{
					"QuoteNumber": "Quote #",
					"Status":      "Quote Status",
				},
				// Mixed case, a camelCase mode key no app layer defines, and a
				// value with surrounding whitespace: this is the only test on
				// the metadata read path, so an all-lowercase fixture here
				// would leave a normalizer added to ResolveLayout or
				// stripAutonumberSectionFields invisible to the whole suite.
				FieldBehaviors: map[string]map[string]string{
					"QuoteNumber": {"create": "hidden"},
					"Status":      {"Create": "  Required  ", "quickCreate": "ReadOnly"},
				},
			}},
		}},
	}
	service := newResolveLayoutFallbackService(repo)
	service.SetAutoNumberFieldResolver(func(string) map[string]struct{} {
		return map[string]struct{}{"QuoteNumber": {}}
	})

	got := service.ResolveLayout(context.Background(), "Quote", nil, "", "")
	if got == nil {
		t.Fatal("ResolveLayout returned nil, want curated fallback layout")
	}

	// Precondition: the scrubber really did run. Without this the assertions
	// below would pass vacuously on a layout nothing touched.
	if wantFields := []string{"Name", "Status"}; !reflect.DeepEqual(got.Sections[0].Fields, wantFields) {
		t.Fatalf("section Fields = %v, want %v (scrubber did not run as expected)", got.Sections[0].Fields, wantFields)
	}

	wantBehaviors := map[string]map[string]string{
		"QuoteNumber": {"create": "hidden"},
		"Status":      {"Create": "  Required  ", "quickCreate": "ReadOnly"},
	}
	if !reflect.DeepEqual(got.Sections[0].FieldBehaviors, wantBehaviors) {
		t.Fatalf("FieldBehaviors = %#v, want %#v (entries must survive their field being stripped)",
			got.Sections[0].FieldBehaviors, wantBehaviors)
	}

	// Same commitment, already-shipped sibling: FieldLabels is not pruned
	// either. Asserting both keeps the two from drifting apart.
	wantLabels := map[string]string{"QuoteNumber": "Quote #", "Status": "Quote Status"}
	if !reflect.DeepEqual(got.Sections[0].FieldLabels, wantLabels) {
		t.Fatalf("FieldLabels = %#v, want %#v", got.Sections[0].FieldLabels, wantLabels)
	}
}

// clonePageLayoutForFieldAdmission backs the rollback snapshot taken before
// add-field admission transfers a layout through SaveLayout. The struct copy it
// starts from aliases every map a section carries, so without an explicit deep
// copy a later mutation of the clone would rewrite the snapshot that is
// supposed to restore the original.
func TestClonePageLayoutForFieldAdmissionDeepCopiesFieldBehaviors(t *testing.T) {
	src := &models.PageLayout{
		ID:            "curated_Account_default",
		ObjectAPIName: "Account",
		Sections: []models.PageSection{{
			ID:     "section_1",
			Label:  "Account Information",
			Fields: []string{"Name", "Rating"},
			FieldBehaviors: map[string]map[string]string{
				"Rating": {"create": "hidden"},
			},
		}},
	}

	clone := clonePageLayoutForFieldAdmission(src)

	clone.Sections[0].FieldBehaviors["Rating"]["create"] = "shown"
	clone.Sections[0].FieldBehaviors["Name"] = map[string]string{"create": "required"}

	if got := src.Sections[0].FieldBehaviors["Rating"]["create"]; got != "hidden" {
		t.Fatalf("mutating the clone's inner behavior map changed the source: got %q, want %q", got, "hidden")
	}
	if _, added := src.Sections[0].FieldBehaviors["Name"]; added {
		t.Fatal("adding a field to the clone's behavior map changed the source")
	}
}

func TestClonePageLayoutForFieldAdmissionDeepCopiesFieldLabels(t *testing.T) {
	src := &models.PageLayout{
		ID:            "curated_Account_default",
		ObjectAPIName: "Account",
		Sections: []models.PageSection{{
			ID:          "section_1",
			Fields:      []string{"Type"},
			FieldLabels: map[string]string{"Type": "Account Type"},
		}},
	}

	clone := clonePageLayoutForFieldAdmission(src)
	clone.Sections[0].FieldLabels["Type"] = "Rewritten"

	if got := src.Sections[0].FieldLabels["Type"]; got != "Account Type" {
		t.Fatalf("mutating the clone's label map changed the source: got %q, want %q", got, "Account Type")
	}
}

func TestClonePageLayoutForFieldAdmissionDeepCopiesRecordPanelConfig(t *testing.T) {
	src := &models.PageLayout{
		ID:            "curated_Account_default",
		ObjectAPIName: "Account",
		RecordPanels: []models.RecordPanelConfig{{
			ID:         "revenue_panel",
			RendererID: "app-pack:example/record-panel/revenue",
			Config: map[string]interface{}{
				"query": map[string]interface{}{
					"fields": []interface{}{"CloseDate", "Amount"},
				},
			},
		}},
	}

	clone := clonePageLayoutForFieldAdmission(src)
	query := clone.RecordPanels[0].Config["query"].(map[string]interface{})
	query["fields"].([]interface{})[0] = "CreatedDate"
	query["limit"] = float64(200)

	sourceQuery := src.RecordPanels[0].Config["query"].(map[string]interface{})
	if got := sourceQuery["fields"].([]interface{})[0]; got != "CloseDate" {
		t.Fatalf("mutating the clone's nested config array changed the source: got %#v", got)
	}
	if _, added := sourceQuery["limit"]; added {
		t.Fatal("mutating the clone's nested config map changed the source")
	}
}

// A clone must be structurally identical to its source, not merely equivalent:
// a nil map must stay nil rather than becoming an allocated empty one, at both
// levels of the nested behavior map. The two levels are guarded for different
// reasons, so both assertions below matter:
//
//   - The inner assertion is the config-fence guard. Inner maps carry no
//     omitempty, so nil encodes as null and an allocated empty map as {} —
//     different bytes for the raw-string comparison that fences the
//     field-admission rollback snapshot.
//   - The outer assertion is the structural-fidelity guard. The outer map does
//     carry omitempty, which suppresses the key either way, so the bytes match
//     regardless and only the snapshot's shape is at stake.
func TestClonePageLayoutForFieldAdmissionKeepsNilMapsNil(t *testing.T) {
	src := &models.PageLayout{
		ID:            "curated_Account_default",
		ObjectAPIName: "Account",
		Sections: []models.PageSection{{
			ID:     "section_1",
			Fields: []string{"Name", "Rating"},
			// A field key whose inner map is nil: distinct from the field key
			// being absent, and the shape maps.Clone must not "helpfully"
			// allocate.
			FieldBehaviors: map[string]map[string]string{"Rating": nil},
		}, {
			ID:     "section_2",
			Fields: []string{"Industry"},
		}},
	}

	clone := clonePageLayoutForFieldAdmission(src)

	inner, present := clone.Sections[0].FieldBehaviors["Rating"]
	if !present {
		t.Fatal("clone dropped the field key whose inner behavior map was nil")
	}
	if inner != nil {
		t.Fatalf("nil inner behavior map became %#v", inner)
	}

	if clone.Sections[1].FieldBehaviors != nil {
		t.Fatalf("nil FieldBehaviors became %#v", clone.Sections[1].FieldBehaviors)
	}
	if clone.Sections[1].FieldLabels != nil {
		t.Fatalf("nil FieldLabels became %#v", clone.Sections[1].FieldLabels)
	}
}
