package scenedescriptions

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 TestNewRequiresLLMClientAndRejectsAmbiguousReferences(t *testing.T) {
	if _, err := New(nil, Options{}); err == nil || !strings.Contains(err.Error(), "LLM client") {
		t.Fatalf("New(nil) error = %v, want dependency error", err)
	}
	if _, err := New(&fakeSceneDescriptionsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one reference set") {
		t.Fatalf("New() error = %v, want reference-set error", err)
	}
	if got := newExtractor(t, &fakeSceneDescriptionsLLMClient{}).Key(); got != Key {
		t.Fatalf("Key() = %q, want %q", got, Key)
	}
}

func TestModuleSpecAndReferenceSlots(t *testing.T) {
	want := pipeline.ModuleSpec{
		Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.scene_descriptions"}, ArtifactKind: dnd.SceneDescriptionListKind,
		ReferenceSlots: []contracts.ReferenceSlot{
			{Name: "glossary", Description: "Optional campaign glossary reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
			{Name: "party", Description: "Optional party roster reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
			{Name: "players", Description: "Optional player list reference material used only to disambiguate scene descriptions.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
		},
	}
	if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
		t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
	}
	got := ModuleSpec()
	got.Requires[0] = "changed"
	got.Provides[0] = "changed"
	got.ReferenceSlots[0].AcceptedMediaTypes[0] = "changed"
	if !reflect.DeepEqual(ModuleSpec(), want) {
		t.Fatal("ModuleSpec() returned mutable shared slices")
	}
	if slots := newExtractor(t, &fakeSceneDescriptionsLLMClient{}).ReferenceSlots(); !reflect.DeepEqual(slots, want.ReferenceSlots) {
		t.Fatalf("ReferenceSlots() = %#v, want %#v", slots, want.ReferenceSlots)
	}
}

func TestRegisterStoresTypedModuleSpecAndOptionsAreStrict(t *testing.T) {
	registry := pipeline.NewExtractorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v, want nil", err)
	}
	if got, ok := registry.Spec(Key); !ok || !reflect.DeepEqual(got, ModuleSpec()) {
		t.Fatalf("registry.Spec(%q) = %#v, present = %t", Key, got, ok)
	}
	if err := Register(nil); err == nil || !strings.Contains(err.Error(), "extractor registry") {
		t.Fatalf("Register(nil) error = %v, want registry error", err)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil || !strings.Contains(err.Error(), "unknown option") {
		t.Fatalf("DecodeOptions() error = %v, want strict options error", err)
	}
}

func TestExtractorMetadataAndCheckpointIdentity(t *testing.T) {
	metadata := newExtractor(t, &fakeSceneDescriptionsLLMClient{}).ManifestMetadata()
	for key, want := range map[string]string{
		"prompt_id": PromptID, "prompt_version": SchemaVersion,
		"response_schema_key": string(ResponseSchemaKey), "response_schema_id": ResponseSchemaID,
		"response_schema_name": ResponseSchemaName, "response_schema_version": SchemaVersion,
		"mapping_policy": mappingPolicy,
	} {
		if metadata[key] != want {
			t.Fatalf("metadata[%q] = %#v, want %q", key, metadata[key], want)
		}
	}
	for _, key := range []string{"prompt_sha256", "response_schema_sha256"} {
		if value, ok := metadata[key].(string); !ok || !strings.HasPrefix(value, "sha256:") {
			t.Fatalf("metadata[%q] = %#v, want hash", key, metadata[key])
		}
	}
	want := map[string]string{"prompt": metadata["prompt_sha256"].(string), "response_schema": metadata["response_schema_sha256"].(string), "mapping_policy": mappingPolicy}
	fingerprints := newExtractor(t, &fakeSceneDescriptionsLLMClient{}).CheckpointFingerprints()
	if len(fingerprints) != len(want) {
		t.Fatalf("CheckpointFingerprints() = %#v, want %d entries", fingerprints, len(want))
	}
	for _, fingerprint := range fingerprints {
		if fingerprint.Value != want[fingerprint.Name] {
			t.Fatalf("fingerprint %q = %q, want %#v", fingerprint.Name, fingerprint.Value, want)
		}
	}
}
