package cli

import (
	"bytes"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/core/config"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

func TestReferenceSelectorsParseAndApplyAllDocumentedForms(t *testing.T) {
	tests := []struct {
		name     string
		selector string
		want     []string
	}{
		{name: "pipeline", selector: "shared", want: []string{"alpha.extract.shared", "alpha.merge.shared", "alpha.normalize.shared", "beta.extract.shared", "beta.merge.shared", "beta.normalize.shared"}},
		{name: "chunk", selector: "chunk.chunk-slot", want: []string{"chunk.chunk-slot"}},
		{name: "lane", selector: "alpha.shared", want: []string{"alpha.extract.shared", "alpha.merge.shared", "alpha.normalize.shared"}},
		{name: "lane extract", selector: "alpha.extract.alpha-slot", want: []string{"alpha.extract.alpha-slot"}},
		{name: "lane merge", selector: "alpha.merge.alpha-merge", want: []string{"alpha.merge.alpha-merge"}},
		{name: "lane normalize", selector: "alpha.normalize.alpha-normalize", want: []string{"alpha.normalize.alpha-normalize"}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := referenceContractConfig()
			catalog := referenceContractCatalog(t, true, true)
			selector, err := parseReferenceSelector(tt.selector, "--reference")
			if err != nil {
				t.Fatal(err)
			}
			overrides, _, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, []cliReferenceRequest{{Selector: selector, Source: "reference.txt"}}, nil)
			if err != nil {
				t.Fatalf("resolve selector: %v", err)
			}
			if got := referenceContractBindingLabels(overrides); !slices.Equal(got, tt.want) {
				t.Fatalf("binding targets = %#v, want %#v", got, tt.want)
			}
			for _, binding := range overrides {
				if binding.Source != "reference.txt" || binding.BindingSource != contracts.ReferenceBindingSourceCLI {
					t.Fatalf("binding = %#v, want CLI source", binding)
				}
			}
		})
	}
}

func TestReferenceSelectorSpecificityAndFinalOccurrenceChooseConcreteBindings(t *testing.T) {
	cfg := referenceContractConfig()
	catalog := referenceContractCatalog(t, true, true)
	requests := []cliReferenceRequest{
		{Selector: mustParseReferenceSelector(t, "shared", "--reference"), Source: "pipeline-first.txt"},
		{Selector: mustParseReferenceSelector(t, "shared", "--reference"), Source: "pipeline-final.txt"},
		{Selector: mustParseReferenceSelector(t, "alpha.shared", "--reference"), Source: "lane.txt"},
		{Selector: mustParseReferenceSelector(t, "alpha.extract.shared", "--reference"), Source: "binding.txt"},
	}
	overrides, unbinds, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, requests, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(unbinds) != 0 {
		t.Fatalf("unbinds = %#v, want none", unbinds)
	}
	want := map[string]string{
		"alpha.extract.shared":   "binding.txt",
		"alpha.merge.shared":     "lane.txt",
		"alpha.normalize.shared": "lane.txt",
		"beta.extract.shared":    "pipeline-final.txt",
		"beta.merge.shared":      "pipeline-final.txt",
		"beta.normalize.shared":  "pipeline-final.txt",
	}
	for _, binding := range overrides {
		label := referenceContractBindingLabel(binding)
		if binding.Source != want[label] {
			t.Fatalf("binding %s source = %q, want %q", label, binding.Source, want[label])
		}
		delete(want, label)
	}
	if len(want) != 0 {
		t.Fatalf("missing bindings: %#v", want)
	}
}

func TestCompleteDNDSharedCLIReferencesExpandAcrossCompatibleTargets(t *testing.T) {
	cfg := loadMaintainedExample(t, repositoryPath("examples", "dnd-complete.config.yml"))
	catalog := catalogFromRegistries(productionTestComponents(t).registries)
	sources := map[string]string{
		"party":         "/references/party.txt",
		"players":       "/references/players.txt",
		"glossary":      "/references/glossary.txt",
		"spell_catalog": "/references/spells.json",
	}
	requests := make([]cliReferenceRequest, 0, len(sources))
	for _, slot := range []string{"party", "players", "glossary", "spell_catalog"} {
		requests = append(requests, cliReferenceRequest{
			Selector: mustParseReferenceSelector(t, slot, "--reference"),
			Source:   sources[slot],
		})
	}
	overrides, unbinds, err := resolveCLIReferenceRequests(cfg, "dnd-session", nil, catalog, requests, nil)
	if err != nil {
		t.Fatalf("expand complete D&D references: %v", err)
	}
	if len(unbinds) != 0 {
		t.Fatalf("unbinds = %#v, want none", unbinds)
	}

	actual := make(map[string]pipeline.ReferenceBinding, len(overrides))
	for _, binding := range overrides {
		actual[referenceContractBindingLabel(binding)] = binding
	}
	targets, err := selectedReferenceTargets(cfg, "dnd-session", nil, catalog)
	if err != nil {
		t.Fatal(err)
	}
	matched := make(map[string]int, len(sources))
	for _, target := range targets {
		for slot, sourcePath := range sources {
			if _, ok := target.slots[slot]; !ok {
				continue
			}
			matched[slot]++
			label := targetLabel(target) + "." + slot
			binding, ok := actual[label]
			if !ok || binding.Source != sourcePath || binding.BindingSource != contracts.ReferenceBindingSourceCLI {
				t.Fatalf("binding %q = %#v, want CLI source %q", label, binding, sourcePath)
			}
		}
	}
	for slot := range sources {
		if matched[slot] < 2 {
			t.Fatalf("reference %q matched %d target(s), want a shared D&D reference", slot, matched[slot])
		}
	}
	if _, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalog, ReferenceOverrides: overrides}); err != nil {
		t.Fatalf("resolve complete D&D CLI references: %v", err)
	}
}

func TestReferenceSelectorsRejectInvalidOrUnselectedScopesBeforeMaterialization(t *testing.T) {
	cfg := referenceContractConfig()
	catalog := referenceContractCatalog(t, true, true)
	for _, tt := range []struct {
		name     string
		selector string
		only     []string
		want     string
	}{
		{name: "pipeline slot", selector: "missing", want: `reference slot "missing" is not declared by any selected target`},
		{name: "lane slot", selector: "alpha.missing", want: `reference slot "missing" is not declared by selected lane "alpha"`},
		{name: "binding slot", selector: "alpha.extract.missing", want: `reference slot "missing" is not declared`},
		{name: "former merge shorthand", selector: "merge.shared", want: `reference lane "merge" is not selected`},
		{name: "unselected lane", selector: "beta.extract.beta-slot", only: []string{"alpha"}, want: `reference lane "beta" is not selected`},
		{name: "unknown lane", selector: "missing.extract.beta-slot", want: `reference lane "missing" is not selected`},
	} {
		t.Run(tt.name, func(t *testing.T) {
			selector := mustParseReferenceSelector(t, tt.selector, "--reference")
			_, _, err := resolveCLIReferenceRequests(cfg, "demo", tt.only, catalog, []cliReferenceRequest{{Selector: selector, Source: filepath.Join(t.TempDir(), "missing.txt")}}, nil)
			if err == nil || !strings.Contains(err.Error(), tt.want) || strings.Contains(err.Error(), "missing.txt") {
				t.Fatalf("error = %v, want selection failure containing %q before file access", err, tt.want)
			}
		})
	}
}

func TestReferenceSyntaxErrorsReturnTwo(t *testing.T) {
	tests := []struct {
		name string
		args []string
	}{
		{name: "reference missing value", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--reference"}},
		{name: "reference missing selector", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--reference", "=path.txt"}},
		{name: "reference missing separator", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--reference", "slot"}},
		{name: "reference missing path", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--reference", "slot="}},
		{name: "reference excess segments", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--reference", "a.b.c.d=path.txt"}},
		{name: "unbind with path", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--without-reference", "slot=path.txt"}},
		{name: "unbind excess segments", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--without-reference", "a.b.c.d"}},
		{name: "unbind missing value", args: []string{"run", "demo", "--config", "missing.yml", "--input", "input.txt", "--without-reference"}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var stdout, stderr bytes.Buffer
			code := RunWithOptions(tt.args, &stdout, &stderr, Options{LookupEnv: emptyLookup})
			if code != 2 || stdout.Len() != 0 || stderr.Len() == 0 {
				t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
			}
		})
	}
}

func TestReferenceBindAndUnbindSpecificity(t *testing.T) {
	cfg := referenceContractConfig()
	catalog := referenceContractCatalog(t, true, true)
	t.Run("specific unbind carves out broad binding", func(t *testing.T) {
		overrides, unbinds, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog,
			[]cliReferenceRequest{{Selector: mustParseReferenceSelector(t, "shared", "--reference"), Source: "shared.txt"}},
			[]cliReferenceUnbindRequest{{Selector: mustParseReferenceSelector(t, "alpha.extract.shared", "--without-reference")}},
		)
		if err != nil {
			t.Fatal(err)
		}
		if got := referenceContractBindingLabels(overrides); slices.Contains(got, "alpha.extract.shared") || len(got) != 5 {
			t.Fatalf("overrides = %#v, want all shared targets except alpha extract", got)
		}
		if got := referenceContractUnbindLabels(unbinds); !slices.Equal(got, []string{"alpha.extract.shared"}) {
			t.Fatalf("unbinds = %#v, want alpha extract", got)
		}
	})

	t.Run("specific binding restores broad unbind", func(t *testing.T) {
		overrides, unbinds, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog,
			[]cliReferenceRequest{{Selector: mustParseReferenceSelector(t, "alpha.extract.shared", "--reference"), Source: "alpha.txt"}},
			[]cliReferenceUnbindRequest{{Selector: mustParseReferenceSelector(t, "shared", "--without-reference")}},
		)
		if err != nil {
			t.Fatal(err)
		}
		if got := referenceContractBindingLabels(overrides); !slices.Equal(got, []string{"alpha.extract.shared"}) {
			t.Fatalf("overrides = %#v, want alpha extract", got)
		}
		if got := referenceContractUnbindLabels(unbinds); slices.Contains(got, "alpha.extract.shared") || len(got) != 5 {
			t.Fatalf("unbinds = %#v, want all shared targets except alpha extract", got)
		}
	})

	t.Run("same specificity conflicts", func(t *testing.T) {
		_, _, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog,
			[]cliReferenceRequest{{Selector: mustParseReferenceSelector(t, "alpha.shared", "--reference"), Source: "alpha.txt"}},
			[]cliReferenceUnbindRequest{{Selector: mustParseReferenceSelector(t, "alpha.shared", "--without-reference")}},
		)
		if err == nil || !strings.Contains(err.Error(), "same specificity") {
			t.Fatalf("error = %v, want same-specificity conflict", err)
		}
	})
}

func TestReferenceUnbindsRemoveOptionalAndProtectRequiredSlots(t *testing.T) {
	cfg := referenceContractConfig()
	catalog := referenceContractCatalog(t, true, true)
	optional, err := parseReferenceSelector("alpha.extract.alpha-slot", "--without-reference")
	if err != nil {
		t.Fatal(err)
	}
	_, without, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, nil, []cliReferenceUnbindRequest{{Selector: optional}})
	if err != nil {
		t.Fatal(err)
	}
	effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "demo", Catalog: catalog, ReferenceUnbinds: without})
	if err != nil {
		t.Fatalf("optional unbind: %v", err)
	}
	if binding := referenceContractFindBinding(referenceContractLane(t, effective.ResolvedPipeline, "alpha").ExtractReferences.Bindings, "alpha-slot"); binding != nil {
		t.Fatalf("optional binding after unbind = %#v, want absent", binding)
	}

	for _, tt := range []struct {
		name     string
		selector string
	}{
		{name: "chunk", selector: "chunk.required-chunk"},
		{name: "extract", selector: "alpha.extract.required-extract"},
		{name: "merge", selector: "alpha.merge.required-merge"},
		{name: "normalize", selector: "alpha.normalize.required-normalize"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			selector, err := parseReferenceSelector(tt.selector, "--without-reference")
			if err != nil {
				t.Fatal(err)
			}
			_, unbinds, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, nil, []cliReferenceUnbindRequest{{Selector: selector}})
			if err != nil {
				t.Fatal(err)
			}
			_, err = cfg.Resolve(config.ResolveInput{PipelineID: "demo", Catalog: catalog, ReferenceUnbinds: unbinds})
			if err == nil || !strings.Contains(err.Error(), "required reference slot") {
				t.Fatalf("resolve error = %v, want required-slot failure", err)
			}
		})
	}
}

func TestReferenceMaterializationSeparatesCLIAndConfigPathOrigins(t *testing.T) {
	configDir := t.TempDir()
	workingDir := t.TempDir()
	cfg := referenceContractConfig()
	configPath := filepath.Join(configDir, "config.yml")
	if err := os.WriteFile(configPath, []byte("version: 4\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(configDir, "required.txt"), []byte("config reference"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(configDir, "optional.txt"), []byte("optional reference"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(workingDir, "cli-reference.txt"), []byte("CLI reference"), 0o600); err != nil {
		t.Fatal(err)
	}
	catalog := referenceContractCatalog(t, true, true)
	selector, err := parseReferenceSelector("alpha.extract.alpha-slot", "--reference")
	if err != nil {
		t.Fatal(err)
	}
	overrides, unbinds, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, []cliReferenceRequest{{Selector: selector, Source: "cli-reference.txt"}}, nil)
	if err != nil {
		t.Fatal(err)
	}
	effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "demo", Catalog: catalog, ReferenceOverrides: overrides, ReferenceUnbinds: unbinds})
	if err != nil {
		t.Fatalf("resolve pipeline: %v", err)
	}
	materialized, _, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, catalog, pipeline.ReferenceMaterializationOptions{ConfigPath: configPath, WorkingDir: workingDir})
	if err != nil {
		t.Fatalf("materialize references: %v", err)
	}
	alpha := referenceContractLane(t, materialized, "alpha")
	cliItem := alpha.ExtractReferences.ReferenceSet.Slots["alpha-slot"].Items[0]
	if string(cliItem.Content) != "CLI reference" || cliItem.BindingSource != contracts.ReferenceBindingSourceCLI || cliItem.Origin.URI != referenceContractFileURI(filepath.Join(workingDir, "cli-reference.txt")) {
		t.Fatalf("CLI materialization = %#v, want working-directory provenance", cliItem)
	}
	configItem := alpha.ExtractReferences.ReferenceSet.Slots["required-extract"].Items[0]
	if string(configItem.Content) != "config reference" || configItem.BindingSource != contracts.ReferenceBindingSourceConfig || configItem.Origin.URI != referenceContractFileURI(filepath.Join(configDir, "required.txt")) {
		t.Fatalf("config materialization = %#v, want config-directory provenance", configItem)
	}
}

func TestPipelineScopedCLIReferenceProtectsGeneratedHandoff(t *testing.T) {
	cfg := referenceContractConfig()
	profile := cfg.Pipelines["demo"]
	alpha := profile.Artifacts["alpha"]
	beta := profile.Artifacts["beta"]
	alpha.Extract.References["shared"] = pipeline.GeneratedReference("produce", "beta")
	profile.Artifacts = nil
	profile.Steps = []pipeline.PipelineStepProfile{
		{ID: "produce", Artifacts: map[string]pipeline.ArtifactLaneProfile{"beta": beta}},
		{ID: "consume", Artifacts: map[string]pipeline.ArtifactLaneProfile{"alpha": alpha}},
	}
	cfg.Pipelines["demo"] = profile
	catalog := referenceContractCatalog(t, true, true)

	t.Run("binding conflicts before file access", func(t *testing.T) {
		overrides, _, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, []cliReferenceRequest{{
			Selector: mustParseReferenceSelector(t, "shared", "--reference"),
			Source:   filepath.Join(t.TempDir(), "never-read.json"),
		}}, nil)
		if err != nil {
			t.Fatalf("expand CLI reference: %v", err)
		}
		_, err = cfg.Resolve(config.ResolveInput{PipelineID: "demo", Catalog: catalog, ReferenceOverrides: overrides})
		if err == nil || !strings.Contains(err.Error(), "conflicting generated and external bindings") || strings.Contains(err.Error(), "never-read.json") {
			t.Fatalf("resolve error = %v, want generated/external conflict before file access", err)
		}
	})

	t.Run("unbind leaves generated source intact", func(t *testing.T) {
		_, unbinds, err := resolveCLIReferenceRequests(cfg, "demo", nil, catalog, nil, []cliReferenceUnbindRequest{{
			Selector: mustParseReferenceSelector(t, "shared", "--without-reference"),
		}})
		if err != nil {
			t.Fatalf("expand CLI unbind: %v", err)
		}
		effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "demo", Catalog: catalog, ReferenceUnbinds: unbinds})
		if err != nil {
			t.Fatalf("resolve generated reference with CLI unbind: %v", err)
		}
		binding := referenceContractFindBinding(referenceContractLane(t, effective.ResolvedPipeline, "alpha").ExtractReferences.Bindings, "shared")
		if binding == nil || binding.Artifact == nil || binding.Artifact.Step != "produce" || binding.Artifact.Lane != "beta" {
			t.Fatalf("generated binding = %#v, want preserved produce/beta handoff", binding)
		}
	})
}

func TestReferenceTargetLookupUsesArtifactVariantsAndReportsMissingContext(t *testing.T) {
	cfg := referenceContractConfig()
	full := referenceContractCatalog(t, true, true)
	targets, err := selectedReferenceTargets(cfg, "demo", nil, full)
	if err != nil {
		t.Fatalf("select reference targets: %v", err)
	}
	var alphaMerge, betaMerge selectedReferenceTarget
	for _, target := range targets {
		if target.stage == pipeline.StageMerge && target.laneID == "alpha" {
			alphaMerge = target
		}
		if target.stage == pipeline.StageMerge && target.laneID == "beta" {
			betaMerge = target
		}
	}
	if _, ok := alphaMerge.slots["alpha-merge"]; !ok {
		t.Fatalf("alpha merger slots = %#v, want alpha artifact variant", alphaMerge.slots)
	}
	if _, ok := betaMerge.slots["beta-merge"]; !ok {
		t.Fatalf("beta merger slots = %#v, want beta artifact variant", betaMerge.slots)
	}
	if _, ok := betaMerge.slots["alpha-merge"]; ok {
		t.Fatalf("beta merger slots = %#v, must not use alpha variant", betaMerge.slots)
	}

	missingMerger := referenceContractCatalog(t, false, true)
	_, err = selectedReferenceTargets(cfg, "demo", nil, missingMerger)
	if err == nil || !strings.Contains(err.Error(), "merger") || !strings.Contains(err.Error(), string(referenceContractKindBeta)) {
		t.Fatalf("missing merger error = %v, want artifact variant context", err)
	}
	missingNormalizer := referenceContractCatalog(t, true, false)
	_, err = selectedReferenceTargets(cfg, "demo", nil, missingNormalizer)
	if err == nil || !strings.Contains(err.Error(), "normalizer") || !strings.Contains(err.Error(), string(referenceContractKindBeta)) {
		t.Fatalf("missing normalizer error = %v, want artifact variant context", err)
	}
	missingExtractor := referenceContractCatalog(t, true, true)
	missingExtractor.Extractors = pipeline.NewExtractorRegistry()
	_, err = selectedReferenceTargets(cfg, "demo", nil, missingExtractor)
	if err == nil || !strings.Contains(err.Error(), `lane "alpha" extract module`) || !strings.Contains(err.Error(), "not registered") {
		t.Fatalf("missing extractor error = %v, want lane/module context", err)
	}
}

const (
	referenceContractKindAlpha contracts.ArtifactKind = "reference/alpha"
	referenceContractKindBeta  contracts.ArtifactKind = "reference/beta"
)

func referenceContractConfig() config.Config {
	cfg := config.Default()
	cfg.Pipelines = map[string]pipeline.PipelineProfile{
		"demo": {
			ID:     "demo",
			Input:  pipeline.Binding("reference/input"),
			Chunk:  pipeline.Binding("reference/chunk"),
			Output: pipeline.Binding("reference/output"),
			Artifacts: map[string]pipeline.ArtifactLaneProfile{
				"alpha": {
					Extract:    pipeline.Binding("reference/extract-alpha"),
					Merge:      pipeline.Binding("reference/shared-merge"),
					Normalize:  pipeline.Binding("reference/shared-normalize"),
					References: pipeline.ExternalReferenceMap(map[string]string{"required-extract": "required.txt"}),
				},
				"beta": {
					Extract:    pipeline.Binding("reference/extract-beta"),
					Merge:      pipeline.Binding("reference/shared-merge"),
					Normalize:  pipeline.Binding("reference/shared-normalize"),
					References: pipeline.ExternalReferenceMap(map[string]string{"required-extract": "required.txt"}),
				},
			},
		},
	}
	profile := cfg.Pipelines["demo"]
	profile.Chunk.References = pipeline.ExternalReferenceMap(map[string]string{"required-chunk": "required.txt"})
	alpha := profile.Artifacts["alpha"]
	alpha.Extract.References = pipeline.ExternalReferenceMap(map[string]string{"required-extract": "required.txt", "alpha-slot": "optional.txt"})
	alpha.Merge.References = pipeline.ExternalReferenceMap(map[string]string{"required-merge": "required.txt"})
	alpha.Normalize.References = pipeline.ExternalReferenceMap(map[string]string{"required-normalize": "required.txt"})
	profile.Artifacts["alpha"] = alpha
	beta := profile.Artifacts["beta"]
	beta.Extract.References = pipeline.ExternalReferenceMap(map[string]string{"required-extract": "required.txt"})
	beta.Merge.References = pipeline.ExternalReferenceMap(map[string]string{"required-merge": "required.txt"})
	beta.Normalize.References = pipeline.ExternalReferenceMap(map[string]string{"required-normalize": "required.txt"})
	profile.Artifacts["beta"] = beta
	cfg.Pipelines["demo"] = profile
	return cfg
}

func referenceContractCatalog(t *testing.T, includeBetaMerger, includeBetaNormalizer bool) pipeline.ModuleCatalog {
	t.Helper()
	registries := pipeline.Registries{
		Inputs:          pipeline.NewInputAdapterRegistry(),
		Chunkers:        pipeline.NewChunkerRegistry(),
		ArtifactCodecs:  pipeline.NewArtifactCodecRegistry(),
		Extractors:      pipeline.NewExtractorRegistry(),
		Mergers:         pipeline.NewMergerRegistry(),
		Normalizers:     pipeline.NewNormalizerRegistry(),
		Validators:      pipeline.NewValidatorRegistry(),
		ValidatorChains: pipeline.NewValidatorChainRegistry(),
		Outputs:         pipeline.NewOutputEncoderRegistry(),
	}
	register := func(err error) {
		if err != nil {
			t.Fatal(err)
		}
	}
	register(registries.Inputs.RegisterBuilderWithSpec(pipeline.ModuleSpec{Key: "reference/input", Stage: pipeline.StageInput, ExecutionClass: contracts.ExecutionClassDeterministic, Provides: []string{"source"}}, func(map[string]any) error { return nil }, func(pipeline.BuildRequest) (contracts.InputAdapter, error) { return stateTestInput{}, nil }))
	register(registries.Chunkers.RegisterWithSpec(pipeline.ModuleSpec{Key: "reference/chunk", Stage: pipeline.StageChunk, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"source"}, Provides: []string{"chunks"}, ReferenceSlots: []contracts.ReferenceSlot{{Name: "chunk-slot"}, {Name: "required-chunk", Required: true}}}, func() (contracts.Chunker, error) { return stateTestChunker{}, nil }))
	register(pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, referenceContractCodecA{}))
	register(pipeline.RegisterArtifactCodec(registries.ArtifactCodecs, referenceContractCodecB{}))
	register(pipeline.RegisterExtractor(registries.Extractors, pipeline.ModuleSpec{Key: "reference/extract-alpha", Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"chunks"}, Provides: []string{"artifact"}, ArtifactKind: referenceContractKindAlpha, ReferenceSlots: []contracts.ReferenceSlot{{Name: "shared", AcceptedArtifactKinds: []contracts.ArtifactKind{referenceContractKindBeta}, AcceptedMediaTypes: []string{"application/json"}}, {Name: "alpha-slot"}, {Name: "required-extract", Required: true}}}, func() (contracts.Extractor[stateTestArtifact], error) { return stateTestExtractor{}, nil }))
	register(pipeline.RegisterExtractor(registries.Extractors, pipeline.ModuleSpec{Key: "reference/extract-beta", Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"chunks"}, Provides: []string{"artifact"}, ArtifactKind: referenceContractKindBeta, ReferenceSlots: []contracts.ReferenceSlot{{Name: "shared"}, {Name: "beta-slot"}, {Name: "required-extract", Required: true}}}, func() (contracts.Extractor[stateTestArtifact], error) { return stateTestExtractor{}, nil }))
	register(pipeline.RegisterMerger(registries.Mergers, pipeline.ModuleSpec{Key: "reference/shared-merge", Stage: pipeline.StageMerge, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"artifact"}, Provides: []string{"merged"}, ArtifactKind: referenceContractKindAlpha, ReferenceSlots: []contracts.ReferenceSlot{{Name: "shared"}, {Name: "alpha-merge"}, {Name: "required-merge", Required: true}}}, func() (contracts.Merger[stateTestArtifact], error) { return stateTestMerger{}, nil }))
	if includeBetaMerger {
		register(pipeline.RegisterMerger(registries.Mergers, pipeline.ModuleSpec{Key: "reference/shared-merge", Stage: pipeline.StageMerge, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"artifact"}, Provides: []string{"merged"}, ArtifactKind: referenceContractKindBeta, ReferenceSlots: []contracts.ReferenceSlot{{Name: "shared"}, {Name: "beta-merge"}, {Name: "required-merge", Required: true}}}, func() (contracts.Merger[stateTestArtifact], error) { return stateTestMerger{}, nil }))
	}
	register(pipeline.RegisterNormalizer(registries.Normalizers, pipeline.ModuleSpec{Key: "reference/shared-normalize", Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: referenceContractKindAlpha, ReferenceSlots: []contracts.ReferenceSlot{{Name: "shared"}, {Name: "alpha-normalize"}, {Name: "required-normalize", Required: true}}}, func() (contracts.Normalizer[stateTestArtifact], error) { return stateTestNormalizer{}, nil }))
	if includeBetaNormalizer {
		register(pipeline.RegisterNormalizer(registries.Normalizers, pipeline.ModuleSpec{Key: "reference/shared-normalize", Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: referenceContractKindBeta, ReferenceSlots: []contracts.ReferenceSlot{{Name: "shared"}, {Name: "beta-normalize"}, {Name: "required-normalize", Required: true}}}, func() (contracts.Normalizer[stateTestArtifact], error) { return stateTestNormalizer{}, nil }))
	}
	register(registries.Outputs.RegisterWithSpec(pipeline.ModuleSpec{Key: "reference/output", Stage: pipeline.StageOutput, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"normalized"}, Provides: []string{"output"}}, func() (contracts.OutputEncoder, error) { return stateTestOutput{}, nil }))
	return catalogFromRegistries(registries)
}

type referenceContractCodecB struct{}

type referenceContractCodecA struct{}

func (referenceContractCodecA) Kind() contracts.ArtifactKind { return referenceContractKindAlpha }
func (referenceContractCodecA) Schema() contracts.ArtifactSchema {
	return contracts.ArtifactSchema{ID: "reference.alpha", Name: "reference_alpha", Version: "v1", JSONSchema: []byte(`{"type":"object"}`)}
}
func (referenceContractCodecA) MediaType() string { return "application/json" }
func (referenceContractCodecA) EncodeCandidate(stateTestArtifact) ([]byte, error) {
	return []byte(`{"value":"ok"}`), nil
}
func (referenceContractCodecA) Encode(stateTestArtifact) ([]byte, error) {
	return []byte(`{"value":"ok"}`), nil
}
func (referenceContractCodecA) Decode([]byte) (stateTestArtifact, error) {
	return stateTestArtifact{Value: "ok"}, nil
}
func (codec referenceContractCodecA) DecodeCandidate(content []byte) (stateTestArtifact, error) {
	return codec.Decode(content)
}

func (referenceContractCodecB) Kind() contracts.ArtifactKind { return referenceContractKindBeta }
func (referenceContractCodecB) Schema() contracts.ArtifactSchema {
	return contracts.ArtifactSchema{ID: "reference.beta", Name: "reference_beta", Version: "v1", JSONSchema: []byte(`{"type":"object"}`)}
}
func (referenceContractCodecB) MediaType() string { return "application/json" }
func (referenceContractCodecB) EncodeCandidate(stateTestArtifact) ([]byte, error) {
	return []byte(`{"value":"ok"}`), nil
}
func (referenceContractCodecB) Encode(stateTestArtifact) ([]byte, error) {
	return []byte(`{"value":"ok"}`), nil
}
func (referenceContractCodecB) Decode([]byte) (stateTestArtifact, error) {
	return stateTestArtifact{Value: "ok"}, nil
}
func (codec referenceContractCodecB) DecodeCandidate(content []byte) (stateTestArtifact, error) {
	return codec.Decode(content)
}

func referenceContractLane(t *testing.T, resolved pipeline.ResolvedPipeline, id string) pipeline.ResolvedArtifactLane {
	t.Helper()
	for _, step := range resolved.Steps {
		for _, lane := range step.ArtifactLanes {
			if lane.ID == id {
				return lane
			}
		}
	}
	t.Fatalf("lane %q not found", id)
	return pipeline.ResolvedArtifactLane{}
}

func referenceContractBindingSource(bindings []pipeline.ReferenceBinding, slot string) string {
	for _, binding := range bindings {
		if binding.SlotName == slot {
			return binding.Source
		}
	}
	return ""
}

func mustParseReferenceSelector(t *testing.T, value, flagName string) cliReferenceSelector {
	t.Helper()
	selector, err := parseReferenceSelector(value, flagName)
	if err != nil {
		t.Fatal(err)
	}
	return selector
}

func referenceContractBindingLabels(bindings []pipeline.ReferenceBinding) []string {
	labels := make([]string, 0, len(bindings))
	for _, binding := range bindings {
		labels = append(labels, referenceContractBindingLabel(binding))
	}
	return labels
}

func referenceContractBindingLabel(binding pipeline.ReferenceBinding) string {
	if binding.Stage == pipeline.StageChunk {
		return "chunk." + binding.SlotName
	}
	return binding.LaneID + "." + string(binding.Stage) + "." + binding.SlotName
}

func referenceContractUnbindLabels(unbinds []pipeline.ReferenceUnbind) []string {
	labels := make([]string, 0, len(unbinds))
	for _, unbind := range unbinds {
		labels = append(labels, referenceContractBindingLabel(pipeline.ReferenceBinding{
			Stage:    unbind.Stage,
			LaneID:   unbind.LaneID,
			SlotName: unbind.SlotName,
		}))
	}
	return labels
}

func referenceContractFindBinding(bindings []pipeline.ReferenceBinding, slot string) *pipeline.ReferenceBinding {
	for i := range bindings {
		if bindings[i].SlotName == slot {
			return &bindings[i]
		}
	}
	return nil
}

func referenceContractFileURI(path string) string {
	absolute, err := filepath.Abs(path)
	if err != nil {
		absolute = path
	}
	return "file://" + filepath.ToSlash(absolute)
}
