package packs

import (
	"reflect"
	"testing"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/offlinecandidateauthority"
)

func TestNewSourceBackedPackOwnsResolvedSourceGraph(t *testing.T) {
	inlinePayload := []byte(`{"objectApiName":"Issue","recordMutation":{"deny":[{"surfaces":["singleCreate"],"message":"closed"}]}}`)
	filePayload := []byte(`{"objectApiName":"Task","recordMutation":{"deny":[{"surfaces":["singleUpdate"],"message":"closed"}]}}`)
	source := &ResolvedPackSource{
		Kind:          PackSourceKindOfficialRepo,
		URI:           "construction-owner",
		Revision:      "revision-a",
		ManifestBytes: []byte(`{"name":"construction-owner"}`),
		Manifest: &PackManifest{
			ManifestFormatVersion: SupportedManifestFormatVersion,
			Name:                  "construction-owner",
			DisplayName:           "Construction Owner",
			Category:              AppPack,
			Version:               "1.0.0",
			PlatformCompatibility: PlatformCompatibility{Min: "0.1.0", MaxExclusive: "0.2.0"},
			Provenance:            &PackProvenance{SourceSystem: "test", SourceExportID: "export-a", GeneratedAt: "now"},
			Dependencies:          []PackDependency{{Name: "base", VersionConstraint: ">=1.0.0"}},
			HostRequirements:      []HostPrimitiveRequirement{{Kind: "renderer", Name: "record-page", Version: "1"}},
			Components: []ManifestComponent{
				{
					Kind:          ComponentKindObjectRecordMutationPolicy,
					BusinessKey:   "Issue",
					Manageability: ManageabilityLocked,
					Payload:       inlinePayload,
				},
				{
					Kind:          ComponentKindObjectRecordMutationPolicy,
					BusinessKey:   "Task",
					Manageability: ManageabilityLocked,
					Path:          "task.json",
					DependsOn: []ComponentReference{{
						Kind: ComponentKindObjectRecordMutationPolicy, BusinessKey: "Issue", Required: true,
					}},
				},
			},
			Extensions: []byte(`{"annotation":{"owner":"caller"}}`),
		},
		Files:    map[string][]byte{"task.json": filePayload},
		FileRefs: []string{"task.json"},
	}

	pack, err := NewSourceBackedPack(source)
	if err != nil {
		t.Fatalf("NewSourceBackedPack() error = %v", err)
	}
	beforeHash, err := pack.PayloadHash()
	if err != nil {
		t.Fatalf("PayloadHash() error = %v", err)
	}
	beforeLegacyHash, err := pack.LegacyPayloadHash()
	if err != nil {
		t.Fatalf("LegacyPayloadHash() error = %v", err)
	}
	beforeHostDigest, err := PackHostRequirementsDigest(pack)
	if err != nil {
		t.Fatalf("PackHostRequirementsDigest() error = %v", err)
	}
	beforeDeclared, err := pack.DeclaredComponents()
	if err != nil {
		t.Fatalf("DeclaredComponents() error = %v", err)
	}
	beforeMetadata := pack.PackHashMetadata()
	beforeDependencies := pack.PackDependencies()
	beforeManifestBytes := append([]byte(nil), pack.source.ManifestBytes...)
	beforeExtensions := append([]byte(nil), pack.manifest.Extensions...)
	beforeFilePayload := append([]byte(nil), pack.source.Files["task.json"]...)

	source.Kind = "mutated"
	source.URI = "mutated"
	source.Revision = "mutated"
	source.ManifestBytes[0] = '!'
	source.Manifest.Name = "mutated"
	source.Manifest.Category = ConnectorPack
	source.Manifest.Version = "9.9.9"
	source.Manifest.Provenance.SourceSystem = "mutated"
	source.Manifest.Dependencies[0].Name = "mutated"
	source.Manifest.HostRequirements[0].Name = "mutated"
	source.Manifest.Components[0].Payload[0] = '!'
	source.Manifest.Components[1].DependsOn[0].BusinessKey = "mutated"
	source.Manifest.Extensions[0] = '!'
	source.Files["task.json"][0] = '!'
	source.Files["extra.json"] = []byte(`{}`)
	source.FileRefs[0] = "mutated.json"

	afterHash, err := pack.PayloadHash()
	if err != nil {
		t.Fatalf("PayloadHash() after caller mutation error = %v", err)
	}
	afterLegacyHash, err := pack.LegacyPayloadHash()
	if err != nil {
		t.Fatalf("LegacyPayloadHash() after caller mutation error = %v", err)
	}
	afterHostDigest, err := PackHostRequirementsDigest(pack)
	if err != nil {
		t.Fatalf("PackHostRequirementsDigest() after caller mutation error = %v", err)
	}
	afterDeclared, err := pack.DeclaredComponents()
	if err != nil {
		t.Fatalf("DeclaredComponents() after caller mutation error = %v", err)
	}
	if pack.Name() != "construction-owner" || pack.Category() != AppPack {
		t.Fatalf("pack identity changed through caller source: %q/%q", pack.Name(), pack.Category())
	}
	if beforeHash != afterHash || beforeLegacyHash != afterLegacyHash || beforeHostDigest != afterHostDigest {
		t.Fatalf("pack fingerprints changed through caller source: v1 %q/%q legacy %q/%q host %q/%q", beforeHash, afterHash, beforeLegacyHash, afterLegacyHash, beforeHostDigest, afterHostDigest)
	}
	if !reflect.DeepEqual(beforeMetadata, pack.PackHashMetadata()) || !reflect.DeepEqual(beforeDependencies, pack.PackDependencies()) || !reflect.DeepEqual(beforeDeclared, afterDeclared) {
		t.Fatal("pack metadata or declarations changed through caller source")
	}
	if !reflect.DeepEqual(beforeManifestBytes, pack.source.ManifestBytes) || !reflect.DeepEqual(beforeExtensions, []byte(pack.manifest.Extensions)) {
		t.Fatal("pack-owned manifest bytes changed through caller source")
	}
	if got := string(pack.source.Files["task.json"]); got != string(beforeFilePayload) {
		t.Fatalf("pack-owned file payload = %q, want %q", got, beforeFilePayload)
	}
	if _, exists := pack.source.Files["extra.json"]; exists || pack.source.FileRefs[0] != "task.json" {
		t.Fatal("pack-owned file index changed through caller source")
	}
}

func TestRebuildSourceBackedPackWithAllowedOwnerlessObjectAPIsUsesOwnedSourceSnapshot(t *testing.T) {
	source := &ResolvedPackSource{
		Kind:     PackSourceKindLocalPath,
		URI:      "/candidate/source",
		Revision: "candidate-revision",
		Manifest: &PackManifest{
			ManifestFormatVersion: SupportedManifestFormatVersion,
			Name:                  "candidate-pack",
			Category:              AppPack,
			Version:               "1.0.0",
			PlatformCompatibility: PlatformCompatibility{Min: "0.1.0", MaxExclusive: "99.0.0"},
		},
		Files: map[string][]byte{},
	}
	pack, err := NewSourceBackedPack(source)
	if err != nil {
		t.Fatalf("construct source pack: %v", err)
	}
	source.Manifest.Name = "mutated-external-source"

	rebuilt, err := RebuildSourceBackedPackWithAllowedOwnerlessObjectAPIs(pack, []string{"Account"})
	if err != nil {
		t.Fatalf("rebuild owned source pack: %v", err)
	}
	if rebuilt.Name() != "candidate-pack" {
		t.Fatalf("rebuilt pack name = %q, want constructor-owned snapshot", rebuilt.Name())
	}
	if !rebuilt.allowOwnerlessObjectAPI["account"] {
		t.Fatalf("rebuilt ownerless allowance = %#v, want Account", rebuilt.allowOwnerlessObjectAPI)
	}
}

func TestStoppedSourceSetConstructionDelegatesDeclaredSurfacesToObjectSpec(t *testing.T) {
	source := &ResolvedPackSource{
		Kind:     PackSourceKindOfficialRepo,
		URI:      "jira-like-sample",
		Revision: "revision-a",
		Manifest: &PackManifest{
			ManifestFormatVersion: SupportedManifestFormatVersion,
			Name:                  "jira-like-sample",
			Category:              AppPack,
			Version:               "0.1.0",
			PlatformCompatibility: PlatformCompatibility{Min: "0.1.0", MaxExclusive: "0.2.0"},
			Components: []ManifestComponent{{
				Kind:          ComponentKindObjectSpec,
				BusinessKey:   "issue",
				Manageability: ManageabilityLocked,
				Payload:       []byte(`[{"apiName":"Issue","label":"Issue","tableType":"standard_object","category":"jira","sharingModel":"Private","fields":[{"apiName":"Subject","label":"Subject","type":"string","required":true,"isNameField":true}]}]`),
			}},
		},
	}

	pack, err := NewSourceBackedPack(
		source,
		WithStoppedSourceSetApplyOnly(offlinecandidateauthority.NewStoppedSourceSetAuthority()),
	)
	if err != nil {
		t.Fatalf("construct stopped source-set pack: %v", err)
	}
	if !pack.allowOwnerlessObjectAPI["issue"] {
		t.Fatalf("stopped source-set object inputs = %#v, want Issue", pack.allowOwnerlessObjectAPI)
	}
	objectSpec, ok := pack.components[0].(*sourceBackedObjectSpecComponent)
	if !ok {
		t.Fatalf("first component = %T, want object_spec", pack.components[0])
	}
	if !objectSpec.allowOwnerlessObjectAPI["issue"] {
		t.Fatalf("object_spec inputs = %#v, want Issue", objectSpec.allowOwnerlessObjectAPI)
	}

	ordinary, err := NewSourceBackedPack(
		source,
		WithStoppedSourceSetApplyOnly(offlinecandidateauthority.StoppedSourceSetAuthority{}),
	)
	if err != nil {
		t.Fatalf("construct pack without stopped authority: %v", err)
	}
	if ordinary.allowOwnerlessObjectAPI["issue"] {
		t.Fatal("missing stopped source-set authority enabled existing-surface apply")
	}
}

func TestSourceBackedPackOptionsAreConstructionOnly(t *testing.T) {
	bundles := []formula.FunctionBundle{{Functions: []formula.FunctionDefinition{{Name: "before"}}}}
	pack, err := NewSourceBackedPack(
		&ResolvedPackSource{Manifest: &PackManifest{Name: "sealed", Category: AppPack}},
		WithAllowedOwnerlessObjectAPIs([]string{"Issue"}),
		WithRuntimeFormulaFunctionBundlesForValidation(bundles),
	)
	if err != nil {
		t.Fatalf("NewSourceBackedPack() error = %v", err)
	}
	if !pack.constructionComplete {
		t.Fatal("constructed pack did not finish construction")
	}

	options := []SourceBackedPackOption{
		WithAllowedOwnerlessObjectAPIs([]string{"Task"}),
		WithRuntimeFormulaFunctionBundlesForValidation(nil),
		withFlowActionCallRegistrations(nil),
		withFlowActionCallHandlerImplementations(nil),
		WithFlowActionCallRuntimeFromPackSourceOptions(PackSourceOptions{}),
		withLegacyAggregateFormulaAuthoringCompat(),
		withLegacyConnectorCalloutBatchIDResponseFieldCompat(),
		withLegacyFolderTypeMissingCreatePermissionCompat(),
		withLegacyOAuthScopeMissingRouteFamilyPrefixCompat(),
		withConnectorPushValidationSchemaResolver(nil),
		WithComponentKindRegistrations(nil),
		WithComponentKindRegistrationsFromSourcePack(pack),
	}
	for index, option := range options {
		func() {
			defer func() {
				if recovered := recover(); recovered == nil {
					t.Fatalf("option %d did not panic after construction", index)
				}
			}()
			option(pack)
		}()
	}

	if !pack.allowOwnerlessObjectAPI["issue"] || pack.allowOwnerlessObjectAPI["task"] {
		t.Fatalf("post-construction option changed ownerless-object state: %#v", pack.allowOwnerlessObjectAPI)
	}
	gotBundles := pack.runtimeFormulaFunctionBundlesForValidation()
	if len(gotBundles) != 1 || len(gotBundles[0].Functions) != 1 || gotBundles[0].Functions[0].Name != "before" {
		t.Fatalf("post-construction option changed formula context: %#v", gotBundles)
	}
}

func TestCloneResolvedPackSourcePreservesNilAndEmptyCollections(t *testing.T) {
	source := &ResolvedPackSource{
		ManifestBytes: []byte{},
		Manifest: &PackManifest{
			Dependencies:     []PackDependency{},
			HostRequirements: nil,
			Components:       []ManifestComponent{{Payload: []byte{}, DependsOn: []ComponentReference{}}},
			Extensions:       []byte{},
		},
		Files:    map[string][]byte{},
		FileRefs: nil,
	}
	clone := cloneResolvedPackSource(source)
	if clone.ManifestBytes == nil || clone.Manifest.Dependencies == nil || clone.Manifest.Components == nil || clone.Manifest.Components[0].Payload == nil || clone.Manifest.Components[0].DependsOn == nil || clone.Manifest.Extensions == nil || clone.Files == nil {
		t.Fatal("non-nil empty source collection became nil")
	}
	if clone.Manifest.HostRequirements != nil || clone.FileRefs != nil {
		t.Fatal("nil source collection became non-nil")
	}
}
