package catalog

import (
	"context"
	"reflect"
	"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"
	spellreference "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog"
)

func TestValidatorApprovesCanonicalNormalizedAndOverlayAliasNames(t *testing.T) {
	validator, err := New(Options{}, overlayReferences())
	if err != nil {
		t.Fatalf("New() error = %v, want nil", err)
	}
	value := spellList(" cure   wounds ", "Emberfall   Aegis")
	result, err := validator.Validate(context.Background(), validationRequest(value))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Validate() = %#v, want approved", result)
	}
}

func TestValidatorCheckpointFingerprintUsesEffectiveCatalogDigest(t *testing.T) {
	validator, err := New(Options{}, overlayReferences())
	if err != nil {
		t.Fatal(err)
	}
	fingerprints := validator.CheckpointFingerprints()
	if len(fingerprints) != 2 || fingerprints[0].Name != "policy" || fingerprints[0].Value != policy || fingerprints[1].Name != "effective_catalog" || fingerprints[1].Value != validator.catalog.Digest() {
		t.Fatalf("checkpoint fingerprints = %#v, want effective catalog digest %q", fingerprints, validator.catalog.Digest())
	}
}

func TestValidatorRejectsMultipleUnknownCastsInStableOrder(t *testing.T) {
	value := spellList("Unknown First", "Cure Wounds", "Unknown Second")
	validator, err := New(Options{})
	if err != nil {
		t.Fatalf("New() error = %v, want nil", err)
	}
	result, err := validator.Validate(context.Background(), validationRequest(value))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if result.Approved || result.ReasonCode != ReasonCode {
		t.Fatalf("Validate() = %#v, want unknown-spell rejection", result)
	}
	if err := contracts.ValidateValidationResult(result); err != nil {
		t.Fatalf("ValidateValidationResult() error = %v for %#v", err, result)
	}
	if want := `spell_casts[0].spell "Unknown First", spell_casts[2].spell "Unknown Second"`; !strings.Contains(result.Message, want) {
		t.Fatalf("message = %q, want %q", result.Message, want)
	}
	for _, want := range []string{"Unknown First", "Unknown Second", "source unit 1", "complete replacement"} {
		if !strings.Contains(result.CorrectionGuidance, want) {
			t.Fatalf("CorrectionGuidance = %q, want %q", result.CorrectionGuidance, want)
		}
	}
	if strings.Contains(result.CorrectionGuidance, "spell_casts[") || strings.Contains(result.CorrectionGuidance, ReasonCode) {
		t.Fatalf("CorrectionGuidance exposed internal diagnostics: %q", result.CorrectionGuidance)
	}
}

func TestValidatorQuotesControlCharactersInUnknownSpellName(t *testing.T) {
	validator, err := New(Options{})
	if err != nil {
		t.Fatal(err)
	}
	result, err := validator.Validate(context.Background(), validationRequest(spellList("Unknown\nSpell\tName")))
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(result.Message, "Unknown\nSpell") || !strings.Contains(result.Message, `Unknown\nSpell\tName`) {
		t.Fatalf("message = %q, want safely quoted control characters", result.Message)
	}
}

func TestValidatorDefersInvalidShape(t *testing.T) {
	value := dnd.SpellList{SpellCasts: []dnd.SpellCast{{Spell: "Unknown Spell"}}}
	validator, err := New(Options{})
	if err != nil {
		t.Fatalf("New() error = %v, want nil", err)
	}
	result, err := validator.Validate(context.Background(), validationRequest(value))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved || result.ReasonCode != "" || result.Message != "" {
		t.Fatalf("Validate() = %#v, want approval without catalog diagnostics", result)
	}
}

func TestValidatorDoesNotMutateArtifactOrCatalog(t *testing.T) {
	value := spellList("Cure Wounds")
	before := value
	validator, err := New(Options{}, overlayReferences())
	if err != nil {
		t.Fatalf("New() error = %v, want nil", err)
	}
	names := validator.catalog.CanonicalNames()
	names[0] = "mutated"
	result, err := validator.Validate(context.Background(), validationRequest(value))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved || !reflect.DeepEqual(value, before) {
		t.Fatalf("Validate() = %#v and value = %#v, want approval without mutation", result, value)
	}
	if _, ok := validator.catalog.Lookup("Aegis of Emberfall"); !ok {
		t.Fatal("catalog lost overlay canonical name after mutating returned names")
	}
}

func TestValidatorUsesStrictEmptyOptions(t *testing.T) {
	if _, err := DecodeOptions(nil); err != nil {
		t.Fatalf("DecodeOptions(nil) error = %v, want nil", err)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
		t.Fatal("DecodeOptions() error = nil, want unknown option error")
	}
}

func TestValidatorSpecAndRegister(t *testing.T) {
	spec := Spec()
	if spec.Key != Key || spec.ExecutionClass != contracts.ExecutionClassDeterministic {
		t.Fatalf("Spec() = %#v, want deterministic catalog validator", spec)
	}
	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v, want nil", err)
	}
	registered, ok := registry.Spec(Key)
	if !ok || registered != spec {
		t.Fatalf("registry.Spec(%q) = %#v, present = %t; want %#v", Key, registered, ok, spec)
	}
}

func validationRequest(value dnd.SpellList) contracts.TypedValidationRequest[dnd.SpellList] {
	return contracts.TypedValidationRequest[dnd.SpellList]{Value: value}
}

func spellList(names ...string) dnd.SpellList {
	value := dnd.SpellList{SpellCasts: make([]dnd.SpellCast, len(names))}
	for index, name := range names {
		value.SpellCasts[index] = validCast(name)
	}
	return value
}

func validCast(name string) dnd.SpellCast {
	return dnd.SpellCast{
		Caster:     "Aria",
		Spell:      name,
		SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
	}
}

func overlayReferences() contracts.ReferenceSet {
	content := []byte(`{
  "schema_version": "notarius.dnd.spell-catalog-overlay.v1",
  "catalogs": [{
    "id": "campaign-spells",
    "ruleset": "dnd-5e-2014",
    "source": {"title": "Campaign spell names"},
    "spells": [{"name": "Aegis of Emberfall", "aliases": ["Emberfall Aegis"]}]
  }]
}`)
	return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		spellreference.SpellCatalogReferenceSlot: {
			Slot: contracts.ReferenceSlot{Name: spellreference.SpellCatalogReferenceSlot},
			Items: []contracts.ReferenceItem{{
				SlotName:  spellreference.SpellCatalogReferenceSlot,
				MediaType: "application/json",
				Content:   content,
			}},
		},
	}}
}
