package validate_test

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/modules/dnd"
	npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcregistry"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
	npcregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/registry"
	combatshape "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/shape"
	combatsourcerefs "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_refs"
	combatsourcerelatedness "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/combatturns/source_relatedness"
	npcregistryvalidator "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcoccurrences/registry"
	npcidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/npcregistry/identity"
	spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/validate/spells/catalog"
)

func TestRepresentativeProductionValidatorResultsSatisfyCorrectionContract(t *testing.T) {
	document := &source.SourceDocument{
		ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session",
		Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "Mira Thorn waits while Aria acts."}},
	}
	npcReferences := npcRegistryReferences(t, "Mira Thorn")
	npcRegistryValidator, err := npcregistryvalidator.New(npcregistryvalidator.Options{}, npcReferences)
	if err != nil {
		t.Fatal(err)
	}
	unknownNPCID := "npc:sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"

	tests := []struct {
		name                string
		validate            func() (contracts.ValidationResult, error)
		wantApproved        bool
		wantDiagnosticCount int
		wantDisposition     contracts.DiagnosticDisposition
	}{
		{
			name: "shape rejection",
			validate: func() (contracts.ValidationResult, error) {
				return combatshape.New(combatshape.Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{
					Value: dnd.CombatTurnList{},
				})
			},
		},
		{
			name: "source reference rejection",
			validate: func() (contracts.ValidationResult, error) {
				return combatsourcerefs.New(combatsourcerefs.Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{
					Source: document,
					Value: dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
						Actor: "Aria", TurnKind: dnd.CombatTurnKindTurn,
						SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 1}},
					}}},
				})
			},
		},
		{
			name: "registry identity rejection",
			validate: func() (contracts.ValidationResult, error) {
				return npcidentity.New(npcidentity.Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{
					Value: dnd.NPCRegistry{NPCs: []dnd.NPC{{
						ID: "not-an-id", Name: "Mira Thorn",
						SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
					}}},
				})
			},
		},
		{
			name: "catalog membership rejection",
			validate: func() (contracts.ValidationResult, error) {
				validator, buildErr := spellcatalog.New(spellcatalog.Options{})
				if buildErr != nil {
					return contracts.ValidationResult{}, buildErr
				}
				return validator.Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{
					Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{
						Caster: "Aria", Spell: "Definitely Not A Spell",
						SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
					}}},
				})
			},
		},
		{
			name: "registry membership rejection",
			validate: func() (contracts.ValidationResult, error) {
				return npcRegistryValidator.Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{
					References: npcReferences,
					Value: dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{
						NPCID: unknownNPCID, Name: "Mira Thorn", Kind: dnd.NPCOccurrenceKindDialogue,
						SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
					}}},
				})
			},
		},
		{
			name: "source relatedness advisory",
			validate: func() (contracts.ValidationResult, error) {
				return combatsourcerelatedness.New(combatsourcerelatedness.Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{
					Source: document,
					Value: dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
						Actor: "Unmentioned Actor", TurnKind: dnd.CombatTurnKindTurn,
						SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
					}}},
				})
			},
			wantApproved:        true,
			wantDiagnosticCount: 1,
			wantDisposition:     contracts.DiagnosticDispositionAdvisory,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			result, err := test.validate()
			if err != nil {
				t.Fatalf("Validate() error = %v", err)
			}
			if result.Approved != test.wantApproved {
				t.Fatalf("Validate() approved = %t, want %t: %#v", result.Approved, test.wantApproved, result)
			}
			if len(result.Diagnostics) != test.wantDiagnosticCount {
				t.Fatalf("Validate() diagnostics = %d, want %d: %#v", len(result.Diagnostics), test.wantDiagnosticCount, result)
			}
			if test.wantDisposition != "" && result.Diagnostics[0].Disposition != test.wantDisposition {
				t.Fatalf("Validate() diagnostic disposition = %q, want %q: %#v", result.Diagnostics[0].Disposition, test.wantDisposition, result)
			}
			if err := contracts.ValidateValidationResult(result); err != nil {
				t.Fatalf("ValidateValidationResult() error = %v for %#v", err, result)
			}
			if !result.Approved && strings.TrimSpace(result.CorrectionGuidance) == "" {
				t.Fatalf("rejection lacks actionable correction guidance: %#v", result)
			}
		})
	}
}

func TestRegistryCorrectionGuidanceUsesContextualNamesNotOpaqueIDs(t *testing.T) {
	references := npcRegistryReferences(t, "Mira Thorn")
	validator, err := npcregistryvalidator.New(npcregistryvalidator.Options{}, references)
	if err != nil {
		t.Fatal(err)
	}
	opaqueID := "npc:sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
	result, err := validator.Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCOccurrenceList]{
		References: references,
		Value: dnd.NPCOccurrenceList{Occurrences: []dnd.NPCOccurrence{{
			NPCID: opaqueID, Name: "Mira Thorn", Kind: dnd.NPCOccurrenceKindDialogue,
			SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
		}}},
	})
	if err != nil || result.Approved {
		t.Fatalf("Validate() = %#v, %v; want rejection", result, err)
	}
	if !strings.Contains(result.CorrectionGuidance, "contextual NPC names") {
		t.Fatalf("correction guidance = %q, want contextual-name instruction", result.CorrectionGuidance)
	}
	if strings.Contains(result.CorrectionGuidance, opaqueID) || strings.Contains(result.CorrectionGuidance, "npc:sha256:") {
		t.Fatalf("correction guidance leaked opaque durable ID: %q", result.CorrectionGuidance)
	}
}

func npcRegistryReferences(t *testing.T, name string) contracts.ReferenceSet {
	t.Helper()
	content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: []dnd.NPC{{
		ID: identity.DeriveID(name), Name: name,
		SourceRefs: []source.SourceRef{{SourceID: "prior-session", StartUnitID: 1, EndUnitID: 1}},
	}}})
	if err != nil {
		t.Fatal(err)
	}
	return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		npcregistry.ReferenceSlot: {
			Slot: contracts.ReferenceSlot{Name: npcregistry.ReferenceSlot},
			Items: []contracts.ReferenceItem{{
				SlotName:  npcregistry.ReferenceSlot,
				MediaType: npccodec.MediaType,
				Content:   content,
			}},
		},
	}}
}
