package spells

import (
	"bytes"
	"context"
	"encoding/json"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"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"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
)

func TestSpellExtractorUsesExactUnboundNPCPromptAndOmitsRegistryIdentity(t *testing.T) {
	extractor := newExtractor(t, &fakeSpellsLLMClient{})
	metadata := extractor.ManifestMetadata()
	if metadata["npc_registry_digest"] != nil || metadata["npc_count"] != nil {
		t.Fatalf("unbound extractor metadata = %#v, want no NPC registry fields", metadata)
	}
	fingerprints := checkpointFingerprintMap(extractor.CheckpointFingerprints())
	if fingerprints["npc_registry"] == "" {
		t.Fatalf("unbound checkpoint fingerprints = %#v, want empty-projection identity", fingerprints)
	}

	client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}}
	extractor = newExtractor(t, client)
	if _, err := extractor.Extract(context.Background(), extractionRequest()); err != nil {
		t.Fatalf("Extract() error = %v, want nil", err)
	}
	input := client.requests[0].Inputs[NPCRegistryReferenceSlot]
	if input.Name != NPCRegistryReferenceSlot || input.MediaType != npccodec.MediaType || input.Digest != fingerprints["npc_registry"] || input.OriginURI != "" || string(input.Content) != `{"npcs":[]}` {
		t.Fatalf("NPC prompt input = %#v, want exact empty registry material", input)
	}
}

func TestSpellExtractorPreservesSemanticNPCRegistryFingerprintAndPromptWiring(t *testing.T) {
	value := registryFixture()
	canonical, err := npccodec.New().Encode(value)
	if err != nil {
		t.Fatalf("encode NPC registry: %v", err)
	}
	pretty, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		t.Fatalf("MarshalIndent() error = %v", err)
	}
	first := newExtractor(t, &fakeSpellsLLMClient{}, spellNPCRegistryReference(canonical, "file:///one.json"))
	second := newExtractor(t, &fakeSpellsLLMClient{}, spellNPCRegistryReference(pretty, "file:///two.json"))
	firstFingerprints := checkpointFingerprintMap(first.CheckpointFingerprints())
	secondFingerprints := checkpointFingerprintMap(second.CheckpointFingerprints())
	if firstFingerprints["npc_registry"] == "" || firstFingerprints["npc_registry"] != secondFingerprints["npc_registry"] {
		t.Fatalf("semantic NPC fingerprints = %#v and %#v, want same npc_registry value", firstFingerprints, secondFingerprints)
	}
	metadata := first.ManifestMetadata()
	if metadata["npc_registry_digest"] == "" || metadata["npc_registry_digest"] == firstFingerprints["npc_registry"] || metadata["npc_count"] != 1 {
		t.Fatalf("NPC registry metadata = %#v, want digest and count only", metadata)
	}

	client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}}
	extractor := newExtractor(t, client, spellNPCRegistryReference(append([]byte("\n"), canonical...), "file:///npc-session.json"))
	if _, err := extractor.Extract(context.Background(), extractionRequest()); err != nil {
		t.Fatalf("Extract() error = %v, want nil", err)
	}
	input := client.requests[0].Inputs[NPCRegistryReferenceSlot]
	if input.Name != NPCRegistryReferenceSlot || input.MediaType != npccodec.MediaType || input.Digest != firstFingerprints["npc_registry"] || input.OriginURI != "" {
		t.Fatalf("NPC prompt input metadata = %#v, want semantic metadata without provenance", input)
	}
	if !bytes.Equal(input.Content, []byte(`{"npcs":[{"name":"Mira Thorn"}]}`)) {
		t.Fatalf("NPC prompt input = %s, want names-only projection", input.Content)
	}
	encoded, err := json.Marshal(map[string]any{"metadata": metadata, "fingerprints": firstFingerprints})
	if err != nil {
		t.Fatalf("marshal metadata: %v", err)
	}
	for _, forbidden := range []string{"Mira Thorn", "The Greencloak", "one.json", "two.json"} {
		if strings.Contains(string(encoded), forbidden) {
			t.Fatalf("metadata or fingerprints leaked %q: %s", forbidden, encoded)
		}
	}
}

func TestSpellExtractorResolvesOperationNPCOverrideWithoutSingletonMetadata(t *testing.T) {
	canonical, err := npccodec.New().Encode(registryFixture())
	if err != nil {
		t.Fatalf("encode NPC registry: %v", err)
	}
	client := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{}}}
	extractor := newExtractor(t, client)
	request := extractionRequest()
	request.References = spellNPCRegistryReference(canonical, "file:///generated.json")
	if _, err := extractor.Extract(context.Background(), request); err != nil {
		t.Fatalf("Extract() error = %v", err)
	}
	input := client.requests[0].Inputs[NPCRegistryReferenceSlot]
	if input.Digest == "" || string(input.Content) != `{"npcs":[{"name":"Mira Thorn"}]}` || input.OriginURI != "" {
		t.Fatalf("operation NPC input = %#v, want generated canonical grounding without provenance", input)
	}
	if metadata := extractor.ManifestMetadata(); metadata["npc_registry_digest"] != nil || metadata["npc_count"] != nil {
		t.Fatalf("singleton metadata = %#v, want no operation-varying NPC identity", metadata)
	}
	if checkpointFingerprintMap(extractor.CheckpointFingerprints())["npc_registry"] == "" {
		t.Fatalf("singleton fingerprints = %#v, want empty-projection identity", extractor.CheckpointFingerprints())
	}
}

func registryFixture() dnd.NPCRegistry {
	return dnd.NPCRegistry{NPCs: []dnd.NPC{{
		ID:         identity.DeriveID("Mira Thorn"),
		Name:       "Mira Thorn",
		SourceRefs: []source.SourceRef{{SourceID: "npc-session", StartUnitID: 41, EndUnitID: 43}},
	}}}
}

func spellNPCRegistryReference(content []byte, origin string) contracts.ReferenceSet {
	return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		registry.ReferenceSlot: {
			Slot: contracts.ReferenceSlot{Name: registry.ReferenceSlot, AcceptedMediaTypes: []string{npccodec.MediaType}, MaxBytes: registry.MaxBytes},
			Items: []contracts.ReferenceItem{{
				SlotName:  registry.ReferenceSlot,
				MediaType: npccodec.MediaType,
				Content:   append([]byte(nil), content...),
				Origin:    contracts.ReferenceOrigin{Type: "file", URI: origin},
			}},
		},
	}}
}

func checkpointFingerprintMap(values []pipeline.CheckpointFingerprint) map[string]string {
	result := make(map[string]string, len(values))
	for _, value := range values {
		result[value.Name] = value.Value
	}
	return result
}
