package sourcerelatedness

import (
	"context"
	"strings"
	"testing"
	"unicode/utf8"

	"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 TestValidatorMatchesCanonicalNamesWithUnicodeVariants(t *testing.T) {
	value := dnd.NPCRegistry{NPCs: []dnd.NPC{
		{ID: "one", Name: "O'Rin Thorn", SourceRefs: []source.SourceRef{
			{SourceID: "session", StartUnitID: 2, EndUnitID: 2},
			{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
			{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
		}},
		{ID: "two", Name: "The Greencloak", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
	}}
	doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{
		{ID: 1, Kind: "message", Text: "  o’rin\u2003thorn appears."},
		{ID: 2, Kind: "message", Text: "The greencloak watches."},
	}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: doc, Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("Validate() = %#v, %v; want canonical-name relatedness approval", result, err)
	}
}

func TestValidatorWarnsAtMostOncePerNPCForUnrelatedCitations(t *testing.T) {
	value := dnd.NPCRegistry{NPCs: []dnd.NPC{
		{ID: "one", Name: "Missing\nName", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
	}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("Validate() = %#v, %v; want one advisory", result, err)
	}
	diagnostic := result.Diagnostics[0]
	sample := diagnostic.Samples[0]
	if sample.Scope != "npcs[0]" || diagnostic.ReasonCode != ReasonCode || !strings.Contains(sample.Message, `Missing\nName`) || strings.Contains(sample.Message, "Missing\nName") || !utf8.ValidString(sample.Message) {
		t.Fatalf("diagnostic = %#v, want safely quoted advisory", diagnostic)
	}
}

func TestValidatorDoesNotMatchShortNameSubstring(t *testing.T) {
	value := dnd.NPCRegistry{NPCs: []dnd.NPC{{
		ID: "one", Name: "Art", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
	}}}
	doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "A cart rolls past."}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: doc, Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("Validate() = %#v, %v; want short-name boundary advisory", result, err)
	}
}

func TestValidatorDefersMalformedShapeAndInvalidRangesDoNotPanic(t *testing.T) {
	invalidShape := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn"}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: invalidShape})
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("shape deferral = %#v, %v; want approval without finding", result, err)
	}
	invalidRange := dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: "one", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}}
	result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: invalidRange})
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("invalid-range relatedness = %#v, %v; want approval without finding", result, err)
	}
}

func TestValidatorUsesOnlyTranscriptEvidenceAndRegistersPolicy(t *testing.T) {
	value := dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: "one", Name: "Opaque NPC", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
	references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Opaque NPC")}}}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), References: references, Value: value})
	if err != nil || len(result.Diagnostics) != 1 {
		t.Fatalf("reference-only relatedness = %#v, %v; want advisory", result, err)
	}
	if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != "dnd.npc_registry.validator.source_relatedness.v2" {
		t.Fatalf("CheckpointFingerprints() = %#v, want local policy", got)
	}
	if Spec().ExecutionClass != contracts.ExecutionClassDeterministic {
		t.Fatalf("Spec() = %#v, want deterministic validator", Spec())
	}
	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v", err)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
		t.Fatal("DecodeOptions() accepted unknown option")
	}
}

func relatednessDocument() *source.SourceDocument {
	return &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party waits."}}}
}
