package packs

import (
	"context"
	"reflect"
	"strings"
	"testing"
)

func testPageLayoutPayload(objectAPIName, layoutName string) []byte {
	return []byte(`{"objectApiName":"` + objectAPIName + `","layoutName":"` + layoutName + `","sections":[]}`)
}

func TestInstallPlanOrdersComponentsByGenericPhase(t *testing.T) {
	var applied []string
	components := []PackComponent{
		&fakePlanComponent{kind: ComponentKindStandardFlow, key: "flow", applied: &applied},
		&fakePlanComponent{kind: ComponentKindReportType, key: "report", applied: &applied},
		&fakePlanComponent{kind: ComponentKindObjectSpec, key: "object", manageability: ManageabilityLocked, applied: &applied},
		&fakePlanComponent{kind: ComponentKindPermission, key: "permission", applied: &applied},
	}

	plan, err := NewInstallPlan(components)
	if err != nil {
		t.Fatalf("NewInstallPlan: %v", err)
	}
	if got, want := plan.Phases(), []InstallPlanPhase{
		InstallPlanPhaseObjectMetadata,
		InstallPlanPhaseSecurity,
		InstallPlanPhaseAutomation,
		InstallPlanPhaseReporting,
	}; !reflect.DeepEqual(got, want) {
		t.Fatalf("phases = %#v, want %#v", got, want)
	}

	var boundaries []InstallPlanPhase
	err = plan.Apply(context.Background(), ApplyDeps{}, InstallPlanHooks{
		AfterPhase: func(_ context.Context, phase InstallPlanPhase) error {
			boundaries = append(boundaries, phase)
			return nil
		},
	})
	if err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if got, want := applied, []string{
		"object_spec/object",
		"permission/permission",
		"standard_flow/flow",
		"report_type/report",
	}; !reflect.DeepEqual(got, want) {
		t.Fatalf("apply order = %#v, want %#v", got, want)
	}
	if got, want := boundaries, plan.Phases(); !reflect.DeepEqual(got, want) {
		t.Fatalf("phase boundaries = %#v, want %#v", got, want)
	}
}

func TestInstallPlanAppliesNonComparableValueComponent(t *testing.T) {
	var applied []string
	component := nonComparablePlanComponent{
		fakePlanComponent: &fakePlanComponent{kind: ComponentKindObjectSpec, key: "object", applied: &applied},
		values:            []string{"non-comparable"},
	}

	plan, err := NewInstallPlan([]PackComponent{component})
	if err != nil {
		t.Fatalf("NewInstallPlan: %v", err)
	}
	if err := plan.Apply(context.Background(), ApplyDeps{}, InstallPlanHooks{}); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if got, want := applied, []string{"object_spec/object"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("applied = %#v, want %#v", got, want)
	}
}

func TestInstallPlanAppliesPackLevelGroupsOnce(t *testing.T) {
	var applied []string
	components := []PackComponent{
		&fakePlanComponent{kind: ComponentKindStandardFlow, key: "flow-a", groupKey: string(ComponentKindStandardFlow), applied: &applied},
		&fakePlanComponent{kind: ComponentKindStandardFlow, key: "flow-b", groupKey: string(ComponentKindStandardFlow), applied: &applied},
	}

	plan, err := NewInstallPlan(components)
	if err != nil {
		t.Fatalf("NewInstallPlan: %v", err)
	}
	if err := plan.Apply(context.Background(), ApplyDeps{}, InstallPlanHooks{}); err != nil {
		t.Fatalf("Apply: %v", err)
	}
	if got, want := applied, []string{"standard_flow/flow-a"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("grouped apply = %#v, want %#v", got, want)
	}
}

func TestInstallPlanAllowsDefaultAndNonDefaultSourceBackedPageLayoutsForSameObject(t *testing.T) {
	_, err := NewInstallPlan([]PackComponent{
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "Quote/default", payload: testPageLayoutPayload("Quote", "Default Layout")},
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "Quote/Quote Layout Expansion & Renewal", payload: testPageLayoutPayload("Quote", "Quote Layout Expansion & Renewal")},
	})
	if err != nil {
		t.Fatalf("NewInstallPlan() unexpected error: %v", err)
	}
}

func TestInstallPlanRejectsPageLayoutBusinessKeyPayloadMismatch(t *testing.T) {
	_, err := NewInstallPlan([]PackComponent{
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "Quote/default", payload: testPageLayoutPayload("Opportunity", "Default Layout")},
	})
	if err == nil {
		t.Fatal("NewInstallPlan() error = nil, want business-key/payload mismatch rejection")
	}
	if !strings.Contains(err.Error(), `objectApiName "Opportunity" must match business key head "Quote"`) {
		t.Fatalf("error = %v, want payload identity mismatch", err)
	}
}

func TestTopologicallySortManifestComponentsAllowsDefaultAndNonDefaultPageLayoutsForSameObject(t *testing.T) {
	ordered, err := topologicallySortManifestComponents(nil, []ManifestComponent{
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "Quote/default",
			Payload:     testPageLayoutPayload("Quote", "Default Layout"),
		},
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "Quote/Quote Layout Expansion & Renewal",
			Payload:     testPageLayoutPayload("Quote", "Quote Layout Expansion & Renewal"),
		},
	})
	if err != nil {
		t.Fatalf("topologicallySortManifestComponents() unexpected error: %v", err)
	}
	if len(ordered) != 2 {
		t.Fatalf("ordered len = %d, want 2", len(ordered))
	}
	if got, want := ordered[0].BusinessKey, "Quote/default"; got != want {
		t.Fatalf("ordered[0] = %q, want %q", got, want)
	}
	if got, want := ordered[1].BusinessKey, "Quote/Quote Layout Expansion & Renewal"; got != want {
		t.Fatalf("ordered[1] = %q, want %q", got, want)
	}
}

func TestTopologicallySortManifestComponentsRejectsDuplicateDefaultPageLayoutsForSameObject(t *testing.T) {
	_, err := topologicallySortManifestComponents(nil, []ManifestComponent{
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "Quote/default",
			Payload:     testPageLayoutPayload("Quote", "Default Layout"),
		},
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "quote/default",
			Payload:     testPageLayoutPayload("Quote", "Default Layout"),
		},
	})
	if err == nil {
		t.Fatal("topologicallySortManifestComponents() error = nil, want duplicate default page_layout rejection")
	}
	if !strings.Contains(err.Error(), "identity") {
		t.Fatalf("error = %v, want duplicate identity label", err)
	}
}

func TestInstallPlanIgnoresSyntheticPageLayoutSweeperComponent(t *testing.T) {
	_, err := NewInstallPlan([]PackComponent{
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "_pack_sweep_non_default"},
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "Quote/default", payload: testPageLayoutPayload("Quote", "Default Layout")},
	})
	if err != nil {
		t.Fatalf("NewInstallPlan() unexpected error: %v", err)
	}
}

func TestInstallPlanRejectsSourceBackedPageLayoutsWhoseManagedIDsCollide(t *testing.T) {
	_, err := NewInstallPlan([]PackComponent{
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "Quote/Foo", payload: testPageLayoutPayload("Quote", "Foo")},
		&fakePlanComponent{kind: ComponentKindPageLayout, key: "Quote/foo", payload: testPageLayoutPayload("Quote", "foo")},
	})
	if err == nil {
		t.Fatal("NewInstallPlan() error = nil, want business-key collision rejection")
	}
	if !strings.Contains(err.Error(), "business key") {
		t.Fatalf("error = %v, want business-key collision label", err)
	}
}

func TestTopologicallySortManifestComponentsRejectsPageLayoutBusinessKeyPayloadMismatch(t *testing.T) {
	_, err := topologicallySortManifestComponents(nil, []ManifestComponent{
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "Quote/default",
			Payload:     testPageLayoutPayload("Opportunity", "Default Layout"),
		},
	})
	if err == nil {
		t.Fatal("topologicallySortManifestComponents() error = nil, want business-key/payload mismatch rejection")
	}
	if !strings.Contains(err.Error(), `objectApiName "Opportunity" must match business key head "Quote"`) {
		t.Fatalf("error = %v, want payload identity mismatch", err)
	}
}

func TestTopologicallySortManifestComponentsRejectsSourceBackedPageLayoutsWhoseManagedIDsCollide(t *testing.T) {
	_, err := topologicallySortManifestComponents(nil, []ManifestComponent{
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "Quote/Foo",
			Payload:     testPageLayoutPayload("Quote", "Foo"),
		},
		{
			Kind:        ComponentKindPageLayout,
			BusinessKey: "Quote/foo",
			Payload:     testPageLayoutPayload("Quote", "foo"),
		},
	})
	if err == nil {
		t.Fatal("topologicallySortManifestComponents() error = nil, want business-key collision rejection")
	}
	if !strings.Contains(err.Error(), "business key") {
		t.Fatalf("error = %v, want business-key collision label", err)
	}
}

func TestDefaultComponentKindContractsCoverInstallableCapabilities(t *testing.T) {
	contracts := DefaultComponentKindContracts()
	for kind, capability := range DefaultSourceBackedComponentCapabilityRegistry() {
		contract, ok := contracts[kind]
		if !ok {
			t.Fatalf("installable kind %s has no component-kind contract", kind)
		}
		if contract.Phase == "" {
			t.Fatalf("kind %s has empty phase", kind)
		}
		if len(contract.WriteSet) == 0 {
			t.Fatalf("kind %s has empty write set", kind)
		}
		if contract.BusinessKey == "" || contract.VerifyProjection == "" || contract.CollisionKey == "" {
			t.Fatalf("kind %s has incomplete contract: %#v", kind, contract)
		}
		if len(contract.Manageabilities) == 0 {
			t.Fatalf("kind %s has no manageability contract", kind)
		}
		if !reflect.DeepEqual(contract.Manageabilities, capability.Manageabilities) {
			t.Fatalf("kind %s manageability = %#v, capability = %#v", kind, contract.Manageabilities, capability.Manageabilities)
		}
	}
}

func TestUIActionContractMatchesLockedActionWriter(t *testing.T) {
	contract, ok := ComponentKindContractFor(ComponentKindUIAction)
	if !ok {
		t.Fatal("ui_action contract missing")
	}
	if got, want := contract.Phase, InstallPlanPhaseUISurfaces; got != want {
		t.Fatalf("ui_action phase = %q, want %q", got, want)
	}
	if _, ok := contract.Manageabilities[ManageabilityLocked]; !ok || len(contract.Manageabilities) != 1 {
		t.Fatalf("ui_action manageability = %#v, want locked only", contract.Manageabilities)
	}
	if got, want := contract.WriteSet[0].Table, "_System_Action"; got != want {
		t.Fatalf("ui_action write table = %q, want %q", got, want)
	}
}

// TestInstallPlanBothRouteSweepersBothExecute verifies that the orphan sweeper
// (connector_inbound_route_orphan) and the incarnation sweeper
// (connector_inbound_route_incarnation) both execute through InstallPlan.Apply,
// i.e. neither is silently dropped by the per-group dedup mechanism.
//
// Before this fix both sweepers shared applyGroupKey=connector_inbound_route,
// and only the first one (orphan, appended earlier in source_backed_pack.go
// line 896) would run; the incarnation sweeper (appended at line 897) was
// always skipped.  After the fix the two keys are distinct so both run.
func TestInstallPlanBothRouteSweepersBothExecute(t *testing.T) {
	var applied []string
	orphan := &fakePlanComponent{
		kind:     ComponentKindConnectorInboundRoute,
		key:      "test-pack/@route-sweeper",
		groupKey: connectorInboundRouteOrphanSweeperGroupKey,
		applied:  &applied,
	}
	incarnation := &fakePlanComponent{
		kind:     ComponentKindConnectorInboundRoute,
		key:      "test-pack/@incarnation-sweeper",
		groupKey: connectorInboundRouteIncarnationSweeperGroupKey,
		applied:  &applied,
	}

	plan, err := NewInstallPlan([]PackComponent{orphan, incarnation})
	if err != nil {
		t.Fatalf("NewInstallPlan: %v", err)
	}
	if err := plan.Apply(context.Background(), ApplyDeps{}, InstallPlanHooks{}); err != nil {
		t.Fatalf("Apply: %v", err)
	}

	// Both sweeper group keys are distinct, so both components must have applied.
	wantApplied := 2
	if got := len(applied); got != wantApplied {
		t.Fatalf("applied count = %d, want %d; applied = %v; "+
			"incarnation sweeper was likely silently skipped by group-dedup — "+
			"ensure connectorInboundRouteOrphanSweeperGroupKey != connectorInboundRouteIncarnationSweeperGroupKey",
			got, wantApplied, applied)
	}
	// Confirm both specific sweepers are present, not just any 2 components.
	orphanKey := string(ComponentKindConnectorInboundRoute) + "/test-pack/@route-sweeper"
	incarnationKey := string(ComponentKindConnectorInboundRoute) + "/test-pack/@incarnation-sweeper"
	appliedSet := make(map[string]bool, len(applied))
	for _, k := range applied {
		appliedSet[k] = true
	}
	if !appliedSet[orphanKey] {
		t.Errorf("orphan sweeper %q not applied; applied = %v", orphanKey, applied)
	}
	if !appliedSet[incarnationKey] {
		t.Errorf("incarnation sweeper %q not applied; applied = %v", incarnationKey, applied)
	}
}

type fakePlanComponent struct {
	kind          ComponentKind
	key           string
	payload       []byte
	manageability Manageability
	groupKey      string
	applied       *[]string
}

type nonComparablePlanComponent struct {
	*fakePlanComponent
	values []string
}

func (c *fakePlanComponent) Kind() ComponentKind {
	return c.kind
}

func (c *fakePlanComponent) PackName() string {
	return "test-pack"
}

func (c *fakePlanComponent) BusinessKey() string {
	return c.key
}

func (c *fakePlanComponent) Manageability() Manageability {
	if c.manageability != "" {
		return c.manageability
	}
	if contract, ok := ComponentKindContractFor(c.kind); ok {
		for manageability := range contract.Manageabilities {
			return manageability
		}
	}
	return ManageabilityDefaultsOnly
}

func (c *fakePlanComponent) ExpectedPayload() ([]byte, error) {
	return append([]byte(nil), c.payload...), nil
}

func (c *fakePlanComponent) ExpectedProjection() (any, error) {
	return nil, nil
}

func (c *fakePlanComponent) LiveProjection(context.Context, VerifyDeps) (any, error) {
	return nil, nil
}

func (c *fakePlanComponent) Apply(context.Context, ApplyDeps) error {
	if c.applied != nil {
		*c.applied = append(*c.applied, string(c.kind)+"/"+c.key)
	}
	return nil
}

func (c *fakePlanComponent) ApplyGroupKey() string {
	return c.groupKey
}
