package cli

import (
	"reflect"
	"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"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	combatextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/combatturns"
	combatnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/combatturns"
)

func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
	components := productionTestComponents(t)
	cfg := productionCombatContractConfig()
	effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
	if err != nil {
		t.Fatalf("Resolve() error = %v, want nil", err)
	}
	if effective.ResolvedPipeline.Chunk.Module != pipeline.DefaultChunkModule {
		t.Fatalf("chunk module = %q, want %q", effective.ResolvedPipeline.Chunk.Module, pipeline.DefaultChunkModule)
	}
	if len(effective.ResolvedPipeline.Steps[0].ArtifactLanes) != 1 {
		t.Fatalf("artifact lanes = %#v, want one combat lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
	}
	lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
	if lane.ID != "combat" || lane.ArtifactKind != dnd.CombatTurnListKind || lane.Extract.Module != combatextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != combatnormalize.Key {
		t.Fatalf("resolved combat lane = %#v, want typed production composition", lane)
	}

	catalog := catalogFromRegistries(components.registries)
	extractSpec, ok := catalog.Extractors.Spec(combatextract.Key)
	if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.combat_turns"}) {
		t.Fatalf("combat extractor spec = %#v, want source and artifact capabilities", extractSpec)
	}
	normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(combatnormalize.Key, dnd.CombatTurnListKind)
	if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
		t.Fatalf("combat normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
	}
	mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.CombatTurnListKind)
	if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
		t.Fatalf("combat merger spec = %#v, want merged capability", mergeSpec)
	}
	codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.CombatTurnListKind)
	if !ok || codecSpec.Schema.ID != "notarius.dnd.combat_turns" || codecSpec.Schema.Version != "v1" {
		t.Fatalf("combat codec spec = %#v, want compatible durable schema", codecSpec)
	}
	if !hasReferenceSlot(extractSpec.ReferenceSlots, "npc_registry") || !hasReferenceSlot(extractSpec.ReferenceSlots, "scene_descriptions") || !hasReferenceSlot(normalizeSpec.ReferenceSlots, "npc_registry") {
		t.Fatalf("combat reference slots = %#v / %#v, want extraction scene and NPC slots plus normalization NPC slot", extractSpec.ReferenceSlots, normalizeSpec.ReferenceSlots)
	}
	sceneSlot := referenceSlot(extractSpec.ReferenceSlots, "scene_descriptions")
	if !sceneSlot.Required || !reflect.DeepEqual(sceneSlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(sceneSlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.SceneDescriptionListKind}) || sceneSlot.MaxBytes != 1048576 {
		t.Fatalf("scene description slot = %#v, want required approved scene artifact", sceneSlot)
	}

	wantExtractChain := []pipeline.ModuleBinding{
		pipeline.Binding("generic/valid_json"),
		pipeline.Binding("extract/dnd/combat-turns/shape"),
		pipeline.Binding("extract/dnd/combat-turns/source_refs"),
		pipeline.Binding("generic/valid_json_schema"),
		pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
	}
	wantNormalizeChain := []pipeline.ModuleBinding{
		pipeline.Binding("generic/valid_json"),
		pipeline.Binding("extract/dnd/combat-turns/shape"),
		pipeline.Binding("normalize/dnd/combat-turns/invariants"),
		pipeline.Binding("extract/dnd/combat-turns/source_refs"),
		pipeline.Binding("generic/valid_json_schema"),
		pipeline.Binding("extract/dnd/combat-turns/source_relatedness"),
	}
	if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, combatextract.Key); !reflect.DeepEqual(got, wantExtractChain) {
		t.Fatalf("combat extract chain = %#v, want %#v", got, wantExtractChain)
	}
	if got := validatorChain(effective.ResolvedPipeline, pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, wantNormalizeChain) {
		t.Fatalf("combat normalize chain = %#v, want %#v", got, wantNormalizeChain)
	}
	if got := validatorChain(effective.ResolvedPipeline, pipeline.StageMerge, pipeline.DefaultMergeModule); len(got) != 0 {
		t.Fatalf("combat merge chain = %#v, want empty", got)
	}

	bound, err := cfg.Resolve(config.ResolveInput{
		PipelineID: "dnd-combat",
		Catalog:    catalog,
		ReferenceOverrides: []pipeline.ReferenceBinding{
			{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
			{Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npc_registry", Source: "npc-run/lanes/npc_registry.json", BindingSource: contracts.ReferenceBindingSourceCLI},
		},
	})
	if err != nil {
		t.Fatalf("Resolve(bound references) error = %v, want nil", err)
	}
	boundLane := bound.ResolvedPipeline.Steps[0].ArtifactLanes[0]
	if len(boundLane.ExtractReferences.Bindings) != 2 || len(boundLane.NormalizeReferences.Bindings) != 1 || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "npc_registry") || !hasReferenceBinding(boundLane.ExtractReferences.Bindings, "scene_descriptions") || !hasReferenceBinding(boundLane.NormalizeReferences.Bindings, "npc_registry") {
		t.Fatalf("bound combat references = %#v / %#v, want extraction scene and NPC bindings plus normalization NPC binding", boundLane.ExtractReferences, boundLane.NormalizeReferences)
	}
}

func TestProductionCombatConfigurationRequiresSceneDescriptions(t *testing.T) {
	components := productionTestComponents(t)
	cfg := productionCombatContractConfig()
	profile := cfg.Pipelines["dnd-combat"]
	profile.References = nil
	cfg.Pipelines["dnd-combat"] = profile
	if _, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)}); err == nil || !strings.Contains(err.Error(), "scene_descriptions") || !strings.Contains(err.Error(), "required") {
		t.Fatalf("Resolve() error = %v, want required scene reference failure", err)
	}
}

func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *testing.T) {
	components := productionTestComponents(t)
	resolve := func(mutate func(*pipeline.PipelineProfile)) error {
		cfg := productionCombatContractConfig()
		profile := cfg.Pipelines["dnd-combat"]
		mutate(&profile)
		cfg.Pipelines["dnd-combat"] = profile
		_, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
		return err
	}
	if err := resolve(func(profile *pipeline.PipelineProfile) {
		lane := profile.Artifacts["combat"]
		lane.Extract.Options = map[string]any{"unexpected": true}
		profile.Artifacts["combat"] = lane
	}); err == nil || !strings.Contains(err.Error(), "unknown option") {
		t.Fatalf("unknown extractor option error = %v, want strict option rejection", err)
	}
	if err := resolve(func(profile *pipeline.PipelineProfile) {
		lane := profile.Artifacts["combat"]
		lane.Normalize.Options = map[string]any{"unexpected": true}
		profile.Artifacts["combat"] = lane
	}); err == nil || !strings.Contains(err.Error(), "unknown option") {
		t.Fatalf("unknown normalizer option error = %v, want strict option rejection", err)
	}
	if err := resolve(func(profile *pipeline.PipelineProfile) {
		lane := profile.Artifacts["combat"]
		lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}
		profile.Artifacts["combat"] = lane
	}); err == nil || !strings.Contains(err.Error(), "artifact lane level") {
		t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err)
	}
}

func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *testing.T) {
	components := productionTestComponents(t)
	cfg := productionCombatContractConfig()
	profile := cfg.Pipelines["dnd-combat"]
	lane := profile.Artifacts["combat"]
	lane.Extract.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}}
	lane.Normalize.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_reject")}}
	profile.Artifacts["combat"] = lane
	cfg.Pipelines["dnd-combat"] = profile
	effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
	if err != nil {
		t.Fatalf("Resolve() error = %v, want typed unconditional validators to resolve", err)
	}
	if got := validatorChain(effective.ResolvedPipeline, pipeline.StageExtract, combatextract.Key); !reflect.DeepEqual(got, []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}) {
		t.Fatalf("extract override chain = %#v, want typed always-accept", got)
	}
	if got := validatorChain(effective.ResolvedPipeline, pipeline.StageNormalize, combatnormalize.Key); !reflect.DeepEqual(got, []pipeline.ModuleBinding{pipeline.Binding("generic/always_reject")}) {
		t.Fatalf("normalize override chain = %#v, want typed always-reject", got)
	}
}

func productionCombatContractConfig() config.Config {
	cfg := config.Default()
	cfg.Pipelines["dnd-combat"] = pipeline.PipelineProfile{
		ID:         "dnd-combat",
		Input:      pipeline.Binding("seriatim"),
		Chunk:      pipeline.Binding(pipeline.DefaultChunkModule),
		References: map[string]pipeline.ReferenceSource{"scene_descriptions": pipeline.ExternalReference("scenes.json")},
		Artifacts: map[string]pipeline.ArtifactLaneProfile{
			"combat": {
				Extract:   pipeline.ModuleBinding{Module: combatextract.Key, Retries: 2},
				Normalize: pipeline.Binding(combatnormalize.Key),
			},
		},
	}
	return cfg
}

func hasReferenceSlot(slots []contracts.ReferenceSlot, name string) bool {
	for _, slot := range slots {
		if slot.Name == name {
			return true
		}
	}
	return false
}

func hasReferenceBinding(bindings []pipeline.ReferenceBinding, name string) bool {
	for _, binding := range bindings {
		if binding.SlotName == name {
			return true
		}
	}
	return false
}

func referenceSlot(slots []contracts.ReferenceSlot, name string) contracts.ReferenceSlot {
	for _, slot := range slots {
		if slot.Name == name {
			return slot
		}
	}
	return contracts.ReferenceSlot{}
}
