package shape

import (
	"context"
	"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"
)

func TestValidatorApprovesWellFormedSpellPayload(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(validSpellList()))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Validate() = %#v, want approved", result)
	}
}

func TestValidatorRejectsMissingSpellList(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(dnd.SpellList{}))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if result.Approved {
		t.Fatalf("Approved = true, want false")
	}
	if result.ReasonCode != ReasonCode {
		t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCode)
	}
}

func TestValidatorRejectsMissingRequiredSpellFields(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*dnd.SpellCast)
	}{
		{name: "blank caster", mutate: func(cast *dnd.SpellCast) { cast.Caster = " " }},
		{name: "blank spell", mutate: func(cast *dnd.SpellCast) { cast.Spell = "" }},
		{name: "empty evidence", mutate: func(cast *dnd.SpellCast) { cast.SourceRefs = nil }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			value := validSpellList()
			test.mutate(&value.SpellCasts[0])
			result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
			if err != nil || result.Approved || result.ReasonCode != ReasonCode {
				t.Fatalf("Validate() = %#v, %v; want shape rejection", result, err)
			}
		})
	}
}

func TestValidatorReportsAllSpellDefectsWithContextualGuidance(t *testing.T) {
	value := dnd.SpellList{SpellCasts: []dnd.SpellCast{
		{Caster: "", Spell: "Fire Bolt", SourceRefs: refsAt(4)},
		{Caster: "Aria", Spell: "", SourceRefs: nil},
	}}
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
	if err != nil || result.Approved {
		t.Fatalf("Validate() = %#v, %v; want rejection", result, err)
	}
	for _, want := range []string{"spell_casts[0].caster", "spell_casts[1].spell", "spell_casts[1].source_refs"} {
		if !strings.Contains(result.Message, want) {
			t.Fatalf("Message = %q, want %q", result.Message, want)
		}
	}
	for _, want := range []string{"Fire Bolt", "source unit 4", "Aria", "complete replacement"} {
		if !strings.Contains(result.CorrectionGuidance, want) {
			t.Fatalf("CorrectionGuidance = %q, want %q", result.CorrectionGuidance, want)
		}
	}
	if strings.Contains(result.CorrectionGuidance, "spell_casts[") {
		t.Fatalf("CorrectionGuidance exposed operator path: %q", result.CorrectionGuidance)
	}
}

func TestValidatorSpecCheckpointAndRegistration(t *testing.T) {
	if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != policy {
		t.Fatalf("CheckpointFingerprints() = %#v, want local policy", got)
	}
	if spec := Spec(); spec.Key != Key || spec.ExecutionClass != contracts.ExecutionClassDeterministic {
		t.Fatalf("Spec() = %#v, want deterministic shape validator", spec)
	}
	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() 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 requestWithValue(value dnd.SpellList) contracts.TypedValidationRequest[dnd.SpellList] {
	return contracts.TypedValidationRequest[dnd.SpellList]{Value: value}
}

func validSpellList() dnd.SpellList {
	return dnd.SpellList{SpellCasts: []dnd.SpellCast{{Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
}

func refsAt(unitID int) []source.SourceRef {
	return []source.SourceRef{{SourceID: "session", StartUnitID: unitID, EndUnitID: unitID}}
}
