package sourcerelatedness

import (
	"context"
	"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 TestValidatorApprovesWithoutAdvisoryWhenSpellAppearsInCitedText(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithSpell(validDocument(), "Cure Wounds", 2))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Approved = false, want true")
	}
	if len(result.Diagnostics) != 0 {
		t.Fatalf("Diagnostics = %#v, want none", result.Diagnostics)
	}
}

func TestValidatorWarnsWhenSpellDoesNotAppearInCitedText(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithSpell(validDocument(), "Fire Bolt", 1))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Approved = false, want true")
	}
	if len(result.Diagnostics) != 1 {
		t.Fatalf("Diagnostics = %#v, want one advisory", result.Diagnostics)
	}
	if result.Diagnostics[0].ReasonCode != ReasonCode || result.Diagnostics[0].Disposition != contracts.DiagnosticDispositionAdvisory {
		t.Fatalf("diagnostic = %#v", result.Diagnostics[0])
	}
}

func TestValidatorMatchesCaseInsensitiveUnicodeMultiwordSpellAcrossCitations(t *testing.T) {
	value := dnd.SpellList{SpellCasts: []dnd.SpellCast{{
		Caster: "Aria", Spell: "Tasha's Hideous Laughter",
		SourceRefs: []source.SourceRef{
			{SourceID: "session", StartUnitID: 2, EndUnitID: 2},
			{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
			{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
		},
	}}}
	doc := &source.SourceDocument{
		ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session",
		Units: []source.SourceUnit{
			{ID: 1, Kind: "message", Text: "Tasha’s"},
			{ID: 2, Kind: "message", Text: "hideous laughter fills the room."},
		},
	}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{Source: doc, Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("Validate() = %#v, %v; want normalized multiword spell approval", result, err)
	}
}

func TestValidatorDoesNotMatchShortSpellNameSubstring(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithSpell(&source.SourceDocument{
		ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session",
		Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party said nothing."}},
	}, "Aid", 1))
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("Validate() = %#v, %v; want short-name boundary advisory", result, err)
	}
}

func TestValidatorBoundsHighCardinalitySpellFindings(t *testing.T) {
	count := contracts.MaxDiagnosticSamples + 4
	casts := make([]dnd.SpellCast, count)
	for index := range casts {
		casts[index] = dnd.SpellCast{Caster: "Aria", Spell: "Missing Spell", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
	}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{
		Source: &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "The party waits."}}},
		Value:  dnd.SpellList{SpellCasts: casts},
	})
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("Validate() = %#v, %v", result, err)
	}
	diagnostic := result.Diagnostics[0]
	if diagnostic.OccurrenceCount != count || len(diagnostic.Samples) != contracts.MaxDiagnosticSamples || diagnostic.OmittedSampleCount != count-contracts.MaxDiagnosticSamples {
		t.Fatalf("diagnostic = %#v", diagnostic)
	}
}

func TestValidatorIgnoresInvalidCitations(t *testing.T) {
	request := requestWithSpell(validDocument(), "Cure Wounds", 2)
	request.Value.SpellCasts[0].SourceRefs[0].StartUnitID = 99
	result, err := New(Options{}).Validate(context.Background(), request)
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("Validate() = %#v, %v; want approval without relatedness advisory", result, err)
	}
}

func TestValidatorApprovesEmptySpellListWithoutAdvisory(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{Source: validDocument(), Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{}}})
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Approved = false, want true")
	}
}

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 relatedness 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 requestWithSpell(doc *source.SourceDocument, name string, unitID int) contracts.TypedValidationRequest[dnd.SpellList] {
	return contracts.TypedValidationRequest[dnd.SpellList]{Source: doc, Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{
		Caster: "Aria", Spell: name,
		SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: unitID, EndUnitID: unitID}},
	}}}}
}

func validDocument() *source.SourceDocument {
	return &source.SourceDocument{
		ID:     "session",
		Kind:   "transcript",
		Format: "application/json",
		Digest: "sha256:session",
		Units: []source.SourceUnit{
			{ID: 1, Kind: "message", Text: "Borin draws his dagger."},
			{ID: 2, Kind: "message", Text: "Aria casts Cure Wounds on Borin."},
		},
	}
}
