package layoutauthoring

import (
	"errors"
	"reflect"
	"testing"

	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
)

func testLayoutDocument() semantic.LayoutDocument {
	return semantic.LayoutDocument{
		LayoutID: "layout-main", ObjectID: "Account", Name: "Main", Kind: "Detail", LayoutVersion: 1,
		CompactFields: []string{}, Tabs: []string{}, Sections: []semantic.LayoutSection{},
		RelatedLists: []semantic.RelatedList{}, RecordPanels: []semantic.RecordPanel{},
		HeaderActions: semantic.ActionPlacement{Actions: []semantic.LayoutAction{}},
		QuickActions:  semantic.ActionPlacement{Actions: []semantic.LayoutAction{}},
		RecordStates:  []semantic.RecordStateRule{}, PrimaryHeaderActions: []string{},
	}
}

func TestLayoutDocumentStorageRoundTripPreservesAuthoredPresence(t *testing.T) {
	document := testLayoutDocument()
	tab, sectionKind, condition := "main", "Fields", "Status == 'Open'"
	behaviors := []semantic.FieldBehavior{{Mode: "Create", Behavior: "Required"}}
	enabled, decimal, density := true, "12.50", "compact"
	document.CompactFields = []string{"Name"}
	document.Tabs = []string{tab}
	document.Sections = []semantic.LayoutSection{{
		SectionID: "section-main", Label: "Main", TabID: &tab, Kind: &sectionKind,
		ComponentConfiguration: []semantic.NamedValue{
			{Name: "enabled", Value: semantic.Value{Kind: semantic.ValueBoolean, Boolean: &enabled}},
			{Name: "threshold", Value: semantic.Value{Kind: semantic.ValueDecimal, Decimal: &decimal}},
		},
		Columns: 2, Fields: []string{"Name"},
		FieldLabels:         []semantic.FieldLabel{{FieldReference: "Name", Label: "Account name"}},
		FieldBehaviors:      []semantic.FieldBehaviorGroup{{FieldReference: "Name", Behaviors: &behaviors}},
		VisibilityCondition: &condition,
	}}
	document.RelatedLists = []semantic.RelatedList{{
		RelatedListID: "contacts", Label: "Contacts", ObjectID: "Contact", LookupFieldID: "AccountId",
		FieldIDs:        []string{"Name"},
		PrefillMappings: []semantic.PrefillMapping{{TargetFieldID: "AccountId", SourceFieldID: "Id"}},
	}}
	document.RecordPanels = []semantic.RecordPanel{{
		PanelID: "panel-main", RendererID: "summary", Position: "tab", TabID: tab, TabLabel: "Summary", Order: 1,
		RendererConfiguration: []semantic.NamedValue{{
			Name: "density", Value: semantic.Value{Kind: semantic.ValueString, String: &density},
		}},
	}}
	document.HeaderActions.Authored = true
	document.HeaderActions.Actions = []semantic.LayoutAction{{
		Name: "Edit", Label: "Edit", Kind: "Standard",
		Configuration: []semantic.NamedValue{}, Extensions: []semantic.NamedValue{},
	}}
	document.RecordStates = []semantic.RecordStateRule{{
		RuleID: "state-open", Condition: condition, Priority: 1,
		Banner: &semantic.RecordStateBanner{Severity: "info", Title: "Open", LinkIDs: []string{"parent"}},
		Links: []semantic.RecordStateLink{{
			LinkID: "parent", Label: "Parent", ReferenceFieldID: "ParentId", ObjectID: "Account", Placement: "banner",
		}},
		ActionOverrides: []semantic.RecordStateActionOverride{{ActionKey: "Delete", Mode: "disable", Reason: "Open"}},
		ReadOnly:        &semantic.RecordStateReadOnly{Enabled: true, Reason: "Open"},
	}}
	document.PrimaryHeaderActions = []string{"Edit"}
	layout, err := PageLayoutFromDocument(document)
	if err != nil {
		t.Fatal(err)
	}
	projected, err := DocumentFromPageLayout(*layout)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(projected, document) {
		t.Fatalf("Layout document round trip changed authored content:\n got  %#v\n want %#v", projected, document)
	}
}

func TestApplyDocumentChangesDistinguishesPreserveClearAndSet(t *testing.T) {
	document := testLayoutDocument()
	changes := preserveDocumentChanges()
	name := "Renamed"
	fields := []string{"Name"}
	changes.Name = semantic.Change[string]{Mode: semantic.ChangeSet, Value: &name}
	changes.Kind = semantic.Change[string]{Mode: semantic.ChangeClear}
	changes.CompactFields = semantic.Change[[]string]{Mode: semantic.ChangeSet, Value: &fields}
	changes.Tabs = semantic.Change[[]string]{Mode: semantic.ChangeClear}

	updated, err := ApplyDocumentChanges(document, changes)
	if err != nil {
		t.Fatal(err)
	}
	if updated.Name != name || updated.Kind != "" || !reflect.DeepEqual(updated.CompactFields, fields) || updated.Tabs == nil || len(updated.Tabs) != 0 {
		t.Fatalf("updated document does not preserve change presence: %#v", updated)
	}
	if updated.LayoutID != document.LayoutID || updated.ObjectID != document.ObjectID {
		t.Fatal("immutable Layout identity changed through document patch")
	}
}

func TestApplyDocumentChangesRejectsInvalidRelationshipBetweenPreservedAndChangedCollections(t *testing.T) {
	document := testLayoutDocument()
	tab := "main"
	document.Tabs = []string{tab}
	document.Sections = []semantic.LayoutSection{{
		SectionID: "section-main", Label: "Main", TabID: &tab, Columns: 1,
		ComponentConfiguration: []semantic.NamedValue{}, Fields: []string{},
		FieldLabels: []semantic.FieldLabel{}, FieldBehaviors: []semantic.FieldBehaviorGroup{},
	}}
	if err := semantic.ValidateLayoutDocument(document); err != nil {
		t.Fatalf("fixture document invalid: %v", err)
	}
	changes := preserveDocumentChanges()
	changes.Tabs = semantic.Change[[]string]{Mode: semantic.ChangeClear}
	if err := semantic.ValidateLayoutDocumentChanges(changes); err != nil {
		t.Fatalf("standalone changes unexpectedly rejected: %v", err)
	}
	_, err := ApplyDocumentChanges(document, changes)
	var appliedFailure *AppliedDocumentValidationError
	var failure *semantic.Failure
	if !errors.As(err, &appliedFailure) || !errors.As(err, &failure) ||
		failure.Category != semantic.FailureInvalidRequest {
		t.Fatalf("post-apply error = %T %v, want stage-owned INVALID_REQUEST", err, err)
	}
}

func TestApplyDocumentChangesDoesNotWrapInvalidInputsAsPostApplyFailure(t *testing.T) {
	invalidCurrent := testLayoutDocument()
	invalidCurrent.Sections = nil
	invalidChanges := preserveDocumentChanges()
	invalidChanges.Name.Mode = semantic.ChangeMode("invalid")
	for _, test := range []struct {
		name    string
		current semantic.LayoutDocument
		changes semantic.LayoutDocumentChanges
	}{
		{name: "current", current: invalidCurrent, changes: preserveDocumentChanges()},
		{name: "changes", current: testLayoutDocument(), changes: invalidChanges},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, err := ApplyDocumentChanges(test.current, test.changes)
			var appliedFailure *AppliedDocumentValidationError
			if err == nil || errors.As(err, &appliedFailure) {
				t.Fatalf("input validation error = %T %v, must remain outside post-apply classification", err, err)
			}
		})
	}
}

func TestApplyListFieldsPreservesAndClearsWithoutNilCollapse(t *testing.T) {
	current := []string{"Name"}
	preserved, err := ApplyListFields(current, semantic.FieldIDListChange{Mode: semantic.ChangePreserve})
	if err != nil || !reflect.DeepEqual(preserved, current) || &preserved[0] == &current[0] {
		t.Fatalf("preserve = %#v err=%v", preserved, err)
	}
	cleared, err := ApplyListFields(current, semantic.FieldIDListChange{Mode: semantic.ChangeClear})
	if err != nil || cleared == nil || len(cleared) != 0 {
		t.Fatalf("clear = %#v err=%v", cleared, err)
	}
	empty := []string{}
	preserved, err = ApplyListFields(empty, semantic.FieldIDListChange{Mode: semantic.ChangePreserve})
	if err != nil || preserved == nil || len(preserved) != 0 {
		t.Fatalf("preserve empty = %#v err=%v", preserved, err)
	}
	set, err := ApplyListFields(current, semantic.FieldIDListChange{Mode: semantic.ChangeSet, Value: &empty})
	if err != nil || set == nil || len(set) != 0 {
		t.Fatalf("set empty = %#v err=%v", set, err)
	}
	preserved, err = ApplyListFields(nil, semantic.FieldIDListChange{Mode: semantic.ChangePreserve})
	if err != nil || preserved != nil {
		t.Fatalf("preserve absent = %#v err=%v", preserved, err)
	}
}

func preserveDocumentChanges() semantic.LayoutDocumentChanges {
	return semantic.LayoutDocumentChanges{
		Name:                 semantic.Change[string]{Mode: semantic.ChangePreserve},
		Kind:                 semantic.Change[string]{Mode: semantic.ChangePreserve},
		LayoutVersion:        semantic.Change[uint32]{Mode: semantic.ChangePreserve},
		CompactFields:        semantic.Change[[]string]{Mode: semantic.ChangePreserve},
		Tabs:                 semantic.Change[[]string]{Mode: semantic.ChangePreserve},
		Sections:             semantic.Change[[]semantic.LayoutSection]{Mode: semantic.ChangePreserve},
		RelatedLists:         semantic.Change[[]semantic.RelatedList]{Mode: semantic.ChangePreserve},
		RecordPanels:         semantic.Change[[]semantic.RecordPanel]{Mode: semantic.ChangePreserve},
		HeaderActions:        semantic.Change[semantic.ActionPlacement]{Mode: semantic.ChangePreserve},
		QuickActions:         semantic.Change[semantic.ActionPlacement]{Mode: semantic.ChangePreserve},
		RecordStates:         semantic.Change[[]semantic.RecordStateRule]{Mode: semantic.ChangePreserve},
		PrimaryHeaderActions: semantic.Change[[]string]{Mode: semantic.ChangePreserve},
	}
}
