package spells

import (
	"reflect"
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
)

func TestNewRequiresLLMClientAndReturnsExtractor(t *testing.T) {
	if _, err := New(nil, Options{}); err == nil || !strings.Contains(err.Error(), "LLM client") {
		t.Fatalf("New(nil) error = %v, want LLM client error", err)
	}
	extractor := newExtractor(t, &fakeSpellsLLMClient{})
	if extractor.Key() != Key {
		t.Fatalf("extractor.Key() = %q, want %q", extractor.Key(), Key)
	}
}

func TestModuleSpec(t *testing.T) {
	got := ModuleSpec()
	want := pipeline.ModuleSpec{
		Key:                Key,
		Stage:              pipeline.StageExtract,
		ExecutionClass:     contracts.ExecutionClassLLMBacked,
		CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1,
		Requires: []string{
			"chunks",
			"source.transcript",
		},
		Provides: []string{
			"dnd.spell_casts",
		},
		ArtifactKind: dnd.SpellListKind,
		ReferenceSlots: []contracts.ReferenceSlot{
			{
				Name:               "glossary",
				Description:        "Optional campaign glossary reference material used only for disambiguation.",
				AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"},
			},
			{
				Name:                  NPCRegistryReferenceSlot,
				Description:           "Optional normalized NPC registry used for canonical caster-name grounding.",
				AcceptedMediaTypes:    []string{"application/json"},
				AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
				MaxBytes:              NPCRegistryMaxBytes,
			},
			{
				Name:               "party",
				Description:        "Optional party roster reference material used only for disambiguation.",
				AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"},
			},
			{
				Name:               "players",
				Description:        "Optional player list reference material used only for disambiguation.",
				AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"},
			},
			{
				Name:               "roster",
				Description:        "Deprecated alias for party roster reference material used only for disambiguation.",
				AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"},
			},
			{
				Name:               "spell_catalog",
				Description:        "Optional canonical spell-name catalog used for extraction grounding.",
				AcceptedMediaTypes: []string{"application/json"},
				MaxBytes:           1048576,
			},
		},
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
	}

	got.Requires[0] = "changed"
	got.Provides[0] = "changed"
	got.ReferenceSlots[0].AcceptedMediaTypes[0] = "changed"
	again := ModuleSpec()
	if !reflect.DeepEqual(again, want) {
		t.Fatalf("ModuleSpec() after caller mutation = %#v, want %#v", again, want)
	}
}

func TestRegisterMakesExtractorBuildable(t *testing.T) {
	registry := pipeline.NewExtractorRegistry()

	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v, want nil", err)
	}

	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil || !strings.Contains(err.Error(), "unknown option") {
		t.Fatalf("DecodeOptions() error = %v, want unknown option error", err)
	}
}

func TestRegisterStoresModuleSpec(t *testing.T) {
	registry := pipeline.NewExtractorRegistry()

	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v, want nil", err)
	}

	got, ok := registry.Spec(Key)
	if !ok {
		t.Fatal("Spec() ok = false, want true")
	}
	want := ModuleSpec()
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("Spec() = %#v, want %#v", got, want)
	}
}

func TestRuntimeReferenceSlotsMatchModuleSpec(t *testing.T) {
	extractor := newExtractor(t, &fakeSpellsLLMClient{})
	spec := ModuleSpec()

	if !reflect.DeepEqual(extractor.ReferenceSlots(), spec.ReferenceSlots) {
		t.Fatalf("ReferenceSlots() = %#v, want spec slots %#v", extractor.ReferenceSlots(), spec.ReferenceSlots)
	}
}

func TestRegisterNilRegistryReturnsError(t *testing.T) {
	err := Register(nil)
	if err == nil {
		t.Fatal("Register(nil) error = nil, want error")
	}
	if !strings.Contains(err.Error(), "extractor registry") {
		t.Fatalf("Register(nil) error = %q, want registry context", err.Error())
	}
}
