package cli

import (
	"reflect"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/core/config"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
	npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
	npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
)

func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
	components := productionTestComponents(t)
	catalog := catalogFromRegistries(components.registries)
	cfg := productionNPCContractConfig()
	effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalog})
	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 NPC lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
	}
	lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
	if lane.ID != "npc_registry" || lane.ArtifactKind != dnd.NPCRegistryKind || lane.Extract.Module != npcextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != npcnormalize.Key {
		t.Fatalf("resolved NPC lane = %#v, want typed production composition", lane)
	}
	if len(lane.ExtractReferences.Bindings) != 0 || len(lane.NormalizeReferences.Bindings) != 0 {
		t.Fatalf("unbound NPC references = %#v / %#v, want none", lane.ExtractReferences, lane.NormalizeReferences)
	}

	extractSpec, ok := catalog.Extractors.Spec(npcextract.Key)
	if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.npc_registry"}) {
		t.Fatalf("NPC extractor spec = %#v, want source and artifact capabilities", extractSpec)
	}
	mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.NPCRegistryKind)
	if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
		t.Fatalf("NPC merger spec = %#v, want merged capability", mergeSpec)
	}
	normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(npcnormalize.Key, dnd.NPCRegistryKind)
	if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
		t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
	}
	codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCRegistryKind)
	if !ok || codecSpec.Kind != dnd.NPCRegistryKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
		t.Fatalf("NPC codec spec = %#v, want typed v1 durable schema", codecSpec)
	}

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

func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *testing.T) {
	components := productionTestComponents(t)
	resolve := func(mutate func(*pipeline.PipelineProfile)) error {
		cfg := productionNPCContractConfig()
		profile := cfg.Pipelines["dnd-session"]
		mutate(&profile)
		cfg.Pipelines["dnd-session"] = profile
		_, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalogFromRegistries(components.registries)})
		return err
	}

	if err := resolve(func(profile *pipeline.PipelineProfile) {
		lane := profile.Artifacts["npc_registry"]
		lane.Extract.Options = map[string]any{"unexpected": true}
		profile.Artifacts["npc_registry"] = 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["npc_registry"]
		lane.Normalize.Options = map[string]any{"unexpected": true}
		profile.Artifacts["npc_registry"] = 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) {
		profile.References = pipeline.ExternalReferenceMap(map[string]string{
			"players":  "players.txt",
			"party":    "party.txt",
			"glossary": "glossary.txt",
		})
	}); err != nil {
		t.Fatalf("optional NPC references error = %v, want resolution success", err)
	}
	if err := resolve(func(profile *pipeline.PipelineProfile) {
		lane := profile.Artifacts["npc_registry"]
		lane.Validators = []pipeline.ModuleBinding{pipeline.Binding("normalize/dnd/npc-registry/identity")}
		profile.Artifacts["npc_registry"] = lane
	}); err == nil || !strings.Contains(err.Error(), "artifact lane level") {
		t.Fatalf("lane-level validator error = %v, want invalid placement rejection", err)
	}
}

func productionNPCContractConfig() config.Config {
	cfg := config.Default()
	cfg.Pipelines["dnd-session"] = pipeline.PipelineProfile{
		ID:    "dnd-session",
		Input: pipeline.Binding("seriatim"),
		Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
		Artifacts: map[string]pipeline.ArtifactLaneProfile{
			"npc_registry": {
				Extract:   pipeline.ModuleBinding{Module: npcextract.Key, Retries: 2},
				Normalize: pipeline.Binding(npcnormalize.Key),
			},
		},
	}
	return cfg
}

func validatorChain(resolved pipeline.ResolvedPipeline, stage pipeline.ModuleStage, module string) []pipeline.ModuleBinding {
	for _, chain := range resolved.ValidatorChains {
		if chain.Stage == stage && chain.ModuleKey == module {
			bindings := make([]pipeline.ModuleBinding, len(chain.Validators))
			for index, validator := range chain.Validators {
				bindings[index] = validator.Binding
			}
			return bindings
		}
	}
	return nil
}
