package cli

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

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	itemregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
	locationregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
	occurrencecodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcoccurrences"
	npcregistrycodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
	itemoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/itemoccurrences"
	locationoccurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/locationoccurrences"
	occurrenceextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcoccurrences"
	npcextract "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/npcregistry"
	itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
	locationidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
	itemoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/itemoccurrences"
	locationoccurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/locationoccurrences"
	occurrencenormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcoccurrences"
	npcnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/npcregistry"
	npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
)

func TestProductionNPCOccurrencePipelineResolvesAndPrepares(t *testing.T) {
	components := productionTestComponents(t)
	resolved, err := pipeline.ResolvePipeline(npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), pipeline.ResolveOptions{}, catalogFromRegistries(components.registries))
	if err != nil {
		t.Fatalf("ResolvePipeline() error = %v", err)
	}
	if len(resolved.Steps) != 2 || len(resolved.Steps[1].ArtifactLanes) != 1 {
		t.Fatalf("resolved pipeline = %#v", resolved)
	}
	lane := resolved.Steps[1].ArtifactLanes[0]
	if lane.ArtifactKind != dnd.NPCOccurrenceListKind || lane.Extract.Module != occurrenceextract.Key || lane.Normalize.Module != occurrencenormalize.Key {
		t.Fatalf("occurrence lane = %#v", lane)
	}
	for _, bindings := range [][]pipeline.ReferenceBinding{lane.ExtractReferences.Bindings, lane.NormalizeReferences.Bindings} {
		if len(bindings) != 1 || bindings[0].SlotName != "npc_registry" || bindings[0].Artifact == nil || bindings[0].Artifact.Step != "npc-registry" || bindings[0].Artifact.Lane != "npc_registry" {
			t.Fatalf("generated bindings = %#v", bindings)
		}
	}
	if _, err := pipeline.Prepare(resolved, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err != nil {
		t.Fatalf("Prepare() error = %v", err)
	}

	catalog := catalogFromRegistries(components.registries)
	codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCOccurrenceListKind)
	if !ok || codecSpec.Schema.ID != occurrencecodec.SchemaID || codecSpec.Schema.Version != occurrencecodec.SchemaVersion {
		t.Fatalf("NPC occurrence codec spec = %#v", codecSpec)
	}
}

func TestProductionNPCOccurrenceReferencesRequireEarlierCompatibleProducer(t *testing.T) {
	components := productionTestComponents(t)
	catalog := catalogFromRegistries(components.registries)
	laterProfile := npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry"))
	laterProfile.Steps[0].ID = "seed"
	laterProfile.Steps[0].Artifacts["seed"] = laterProfile.Steps[0].Artifacts["npc_registry"]
	delete(laterProfile.Steps[0].Artifacts, "npc_registry")
	laterProfile.Steps = append(laterProfile.Steps, pipeline.PipelineStepProfile{ID: "future", Artifacts: map[string]pipeline.ArtifactLaneProfile{
		"npc_registry": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
	}})
	laterProfile.Steps[1].References["npc_registry"] = pipeline.GeneratedReference("future", "npc_registry")
	tests := []struct {
		name    string
		profile pipeline.PipelineProfile
		want    string
	}{
		{name: "missing", profile: npcOccurrenceProfile(pipeline.ReferenceSource{}), want: "source must not be empty"},
		{name: "same step", profile: npcOccurrenceProfile(pipeline.GeneratedReference("occurrences", "occurrences")), want: "earlier step"},
		{name: "later step", profile: laterProfile, want: "earlier step"},
		{name: "wrong artifact kind", profile: npcOccurrenceProfile(pipeline.GeneratedReference("npc-registry", "npc_registry")), want: "does not accept artifact kind"},
	}
	tests[3].profile.Steps[0].Artifacts["npc_registry"] = pipeline.ArtifactLaneProfile{Extract: pipeline.Binding("dnd/spells")}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := pipeline.ResolvePipeline(test.profile, pipeline.ResolveOptions{}, catalog)
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("ResolvePipeline() error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestProductionNPCOccurrenceReferencesRejectIncompatibleExternalRegistries(t *testing.T) {
	components := productionTestComponents(t)
	catalog := catalogFromRegistries(components.registries)
	root := t.TempDir()
	for _, test := range []struct {
		name    string
		file    string
		content string
		prepare bool
		want    string
	}{
		{name: "media type", file: "registry.txt", content: "not JSON", want: "media type"},
		{name: "artifact schema", file: "registry.json", content: `{"npcs":[{"name":"missing required fields"}]}`, prepare: true, want: "NPC registry"},
	} {
		t.Run(test.name, func(t *testing.T) {
			path := filepath.Join(root, test.file)
			if err := os.WriteFile(path, []byte(test.content), 0o600); err != nil {
				t.Fatal(err)
			}
			resolved, err := pipeline.ResolvePipeline(npcOccurrenceProfile(pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog)
			if err != nil {
				t.Fatalf("ResolvePipeline() error = %v", err)
			}
			materialized, _, err := pipeline.MaterializeReferences(resolved, catalog, pipeline.ReferenceMaterializationOptions{})
			if !test.prepare {
				if err == nil || !strings.Contains(err.Error(), test.want) {
					t.Fatalf("MaterializeReferences() error = %v, want %q", err, test.want)
				}
				return
			}
			if err != nil {
				t.Fatalf("MaterializeReferences() error = %v", err)
			}
			if _, err := pipeline.Prepare(materialized, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("Prepare() error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestProductionOccurrencePipelinesAcceptCompatibleExternalRegistries(t *testing.T) {
	components := productionTestComponents(t)
	catalog := catalogFromRegistries(components.registries)
	reference := source.SourceRef{SourceID: "external-registry", StartUnitID: 1, EndUnitID: 1}
	npcContent, err := npcregistrycodec.New().Encode(dnd.NPCRegistry{NPCs: []dnd.NPC{{
		ID: npcidentity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{reference},
	}}})
	if err != nil {
		t.Fatal(err)
	}
	locationContent, err := locationregistrycodec.New().Encode(dnd.LocationRegistry{Locations: []dnd.Location{{
		ID: locationidentity.DeriveID("Moon Gate", []source.SourceRef{reference}), Name: "Moon Gate", SourceRefs: []source.SourceRef{reference},
	}}})
	if err != nil {
		t.Fatal(err)
	}
	itemContent, err := itemregistrycodec.New().Encode(dnd.ItemRegistry{Items: []dnd.Item{{
		ID: itemidentity.DeriveID("Moonblade"), Name: "Moonblade", SourceRefs: []source.SourceRef{reference},
	}}})
	if err != nil {
		t.Fatal(err)
	}

	root := t.TempDir()
	for _, test := range []struct {
		name       string
		slot       string
		extractor  string
		normalizer string
		content    []byte
	}{
		{name: "NPC", slot: "npc_registry", extractor: occurrenceextract.Key, normalizer: occurrencenormalize.Key, content: npcContent},
		{name: "location", slot: "location_registry", extractor: locationoccurrenceextract.Key, normalizer: locationoccurrencenormalize.Key, content: locationContent},
		{name: "item", slot: "item_registry", extractor: itemoccurrenceextract.Key, normalizer: itemoccurrencenormalize.Key, content: itemContent},
	} {
		t.Run(test.name, func(t *testing.T) {
			path := filepath.Join(root, strings.ToLower(test.name)+"-registry.json")
			if err := os.WriteFile(path, test.content, 0o600); err != nil {
				t.Fatal(err)
			}
			resolved, err := pipeline.ResolvePipeline(externalOccurrenceProfile(test.slot, test.extractor, test.normalizer, pipeline.ExternalReference(path)), pipeline.ResolveOptions{}, catalog)
			if err != nil {
				t.Fatalf("ResolvePipeline() error = %v", err)
			}
			materialized, warnings, err := pipeline.MaterializeReferences(resolved, catalog, pipeline.ReferenceMaterializationOptions{})
			if err != nil || len(warnings) != 0 {
				t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings)
			}
			if _, err := pipeline.Prepare(materialized, components.registries, pipeline.ModuleDependencies{LLM: &productionFakeLLMClient{}}); err != nil {
				t.Fatalf("Prepare() error = %v", err)
			}
		})
	}
}

func externalOccurrenceProfile(slot, extractor, normalizer string, reference pipeline.ReferenceSource) pipeline.PipelineProfile {
	return pipeline.PipelineProfile{
		ID:     "external-registry-occurrences",
		Input:  pipeline.Binding("seriatim"),
		Chunk:  pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
		Output: pipeline.Binding("json"),
		Steps: []pipeline.PipelineStepProfile{{
			ID:         "occurrences",
			References: map[string]pipeline.ReferenceSource{slot: reference},
			Artifacts: map[string]pipeline.ArtifactLaneProfile{
				"occurrences": {Extract: pipeline.Binding(extractor), Normalize: pipeline.Binding(normalizer)},
			},
		}},
	}
}

func npcOccurrenceProfile(reference pipeline.ReferenceSource) pipeline.PipelineProfile {
	profile := pipeline.PipelineProfile{
		ID:     "dnd-npc-occurrences",
		Input:  pipeline.Binding("seriatim"),
		Chunk:  pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}},
		Output: pipeline.Binding("json"),
		Steps: []pipeline.PipelineStepProfile{
			{ID: "npc-registry", Artifacts: map[string]pipeline.ArtifactLaneProfile{
				"npc_registry": {Extract: pipeline.Binding(npcextract.Key), Normalize: pipeline.Binding(npcnormalize.Key)},
			}},
			{ID: "occurrences", References: map[string]pipeline.ReferenceSource{"npc_registry": reference}, Artifacts: map[string]pipeline.ArtifactLaneProfile{
				"occurrences": {Extract: pipeline.Binding(occurrenceextract.Key), Normalize: pipeline.Binding(occurrencenormalize.Key)},
			}},
		},
	}
	return profile
}
