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 TestValidatorUsesDocumentOrderAndUnicodeComparisonForActor(t *testing.T) {
	value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
		Actor: "O'Rin Thorn", TurnKind: dnd.CombatTurnKindTurn,
		SourceRefs: []source.SourceRef{
			{SourceID: "session", StartUnitID: 2, 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: "o’rin\u2003thorn advances."},
		{ID: 2, Kind: "message", Text: "O’rin attacks the goblin."},
	}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: doc, Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("Validate() = %#v, %v; want Unicode-related approval without a finding", result, err)
	}
}

func TestValidatorWarnsOncePerTurnForUnrelatedActor(t *testing.T) {
	value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
		Actor: "Missing\nName", TurnKind: dnd.CombatTurnKindReaction,
		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.CombatTurnList]{Source: relatednessDocument(), Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("Validate() = %#v, %v; want one advisory for the turn", result, err)
	}
	diagnostic := result.Diagnostics[0]
	sample := diagnostic.Samples[0]
	if diagnostic.Disposition != contracts.DiagnosticDispositionAdvisory || diagnostic.Category != contracts.DiagnosticCategoryDataQuality || diagnostic.ReasonCode != ReasonCode || sample.Scope != "combat_turns[0]" || !strings.Contains(sample.Message, `Missing\nName`) || strings.Contains(sample.Message, "Missing\nName") || !utf8.ValidString(sample.Message) {
		t.Fatalf("diagnostic = %#v, want one safely quoted advisory", diagnostic)
	}
}

func TestValidatorLimitsUnrelatedActorAdvisories(t *testing.T) {
	turns := make([]dnd.CombatTurn, contracts.MaxDiagnosticSamples+2)
	for index := range turns {
		turns[index] = dnd.CombatTurn{
			Actor:      "Missing Actor",
			TurnKind:   dnd.CombatTurnKindTurn,
			SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
		}
	}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{
		Source: relatednessDocument(),
		Value:  dnd.CombatTurnList{CombatTurns: turns},
	})
	if err != nil || !result.Approved {
		t.Fatalf("Validate() = %#v, %v", result, err)
	}
	if len(result.Diagnostics) != 1 || result.Diagnostics[0].OccurrenceCount != len(turns) || len(result.Diagnostics[0].Samples) != contracts.MaxDiagnosticSamples || result.Diagnostics[0].OmittedSampleCount != len(turns)-contracts.MaxDiagnosticSamples {
		t.Fatalf("diagnostics = %#v", result.Diagnostics)
	}
}

func TestValidatorDoesNotMatchShortActorSubstring(t *testing.T) {
	value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
		Actor: "Art", TurnKind: dnd.CombatTurnKindTurn,
		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: "The cart attacks."}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: doc, Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 || !strings.Contains(result.Diagnostics[0].Samples[0].Message, "actor") {
		t.Fatalf("Validate() = %#v, %v; want short-actor boundary advisory", result, err)
	}
}

func TestValidatorDefersMalformedShapeAndInvalidRanges(t *testing.T) {
	invalidShape := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{Actor: "Aria"}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{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 := validCombatTurnList()
	invalidRange.CombatTurns[0].SourceRefs[0].StartUnitID = 99
	result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: relatednessDocument(), Value: invalidRange})
	if err != nil || !result.Approved || len(result.Diagnostics) != 0 {
		t.Fatalf("invalid-range deferral = %#v, %v; want approval without finding", result, err)
	}
}

func TestValidatorIgnoresReferenceMaterialAndRegistersPolicy(t *testing.T) {
	value := validCombatTurnList()
	references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Aria attacks")}}}}}
	doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party waits."}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: doc, References: references, Value: value})
	if err != nil || !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("reference-only relatedness = %#v, %v; want advisory from transcript-only evidence", result, err)
	}
	if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != "dnd.combat_turns.validator.source_relatedness.v2" {
		t.Fatalf("CheckpointFingerprints() = %#v, want relatedness policy", got)
	}
	if 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", err)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
		t.Fatal("DecodeOptions() accepted unknown option")
	}
}

func validCombatTurnList() dnd.CombatTurnList {
	return dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
		Actor: "Aria", TurnKind: dnd.CombatTurnKindTurn,
		SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
	}}}
}

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."}}}
}
