package sourcerefs

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 TestValidatorApprovesValidSourceRefs(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), source.SourceRef{SourceID: "session", StartUnitID: 1, EndUnitID: 2}))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Validate() = %#v, want approved", result)
	}
}

func TestValidatorRejectsInvalidSourceRefs(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), source.SourceRef{SourceID: "session", StartUnitID: 99, EndUnitID: 99}))
	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)
	}
	for _, want := range []string{"Cure Wounds", "Aria", "source unit 99", "complete replacement"} {
		if !strings.Contains(result.CorrectionGuidance, want) {
			t.Fatalf("CorrectionGuidance = %q, want %q", result.CorrectionGuidance, want)
		}
	}
	if strings.Contains(result.CorrectionGuidance, "spell_casts[") || strings.Contains(result.CorrectionGuidance, ReasonCode) {
		t.Fatalf("CorrectionGuidance exposed internal diagnostics: %q", result.CorrectionGuidance)
	}
}

func TestValidatorRejectsMissingSourceDocument(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(nil, source.SourceRef{SourceID: "session", StartUnitID: 1, EndUnitID: 1}))
	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 TestValidatorEnforcesCurrentChunkEvidenceDuringExtraction(t *testing.T) {
	doc := validDocument()
	req := requestWithValue(doc, source.SourceRef{SourceID: doc.ID, StartUnitID: 1, EndUnitID: 2})
	req.Stage = string(pipeline.StageExtract)
	req.Chunk = &source.Chunk{SourceID: doc.ID, Units: append([]source.SourceUnit(nil), doc.Units...)}
	result, err := New(Options{}).Validate(context.Background(), req)
	if err != nil || !result.Approved {
		t.Fatalf("in-chunk extraction evidence = %#v, %v; want approval", result, err)
	}

	req.Chunk = &source.Chunk{SourceID: doc.ID, Units: []source.SourceUnit{{ID: 1}}}
	result, err = New(Options{}).Validate(context.Background(), req)
	if err != nil || result.Approved || !strings.Contains(result.Message, "outside the current extraction chunk") {
		t.Fatalf("off-chunk extraction evidence = %#v, %v; want rejection", result, err)
	}

	req.Chunk = nil
	if _, err = New(Options{}).Validate(context.Background(), req); err == nil || !strings.Contains(err.Error(), "requires the current extraction chunk") {
		t.Fatalf("missing extraction chunk error = %v", err)
	}

	req.Stage = string(pipeline.StageNormalize)
	result, err = New(Options{}).Validate(context.Background(), req)
	if err != nil || !result.Approved {
		t.Fatalf("document-wide normalization evidence = %#v, %v; want approval", result, err)
	}
}

func TestValidatorDefersMalformedShape(t *testing.T) {
	value := dnd.SpellList{SpellCasts: []dnd.SpellCast{{Spell: "Cure Wounds"}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{Source: validDocument(), Value: value})
	if err != nil || !result.Approved || result.ReasonCode != "" || result.Message != "" {
		t.Fatalf("Validate() = %#v, %v; want shape deferral", result, err)
	}
}

func TestValidatorAggregatesAndBoundsInvalidSourceReferences(t *testing.T) {
	value := requestWithValue(validDocument(), source.SourceRef{SourceID: "session", StartUnitID: 1, EndUnitID: 1}).Value
	value.SpellCasts[0].SourceRefs = make([]source.SourceRef, 24)
	for index := range value.SpellCasts[0].SourceRefs {
		value.SpellCasts[0].SourceRefs[index] = source.SourceRef{SourceID: "session", StartUnitID: 99 + index, EndUnitID: 99 + index}
	}
	value.SpellCasts[0].SourceRefs[0].SourceID = strings.Repeat("火", 220) + "\n\t"
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.SpellList]{Source: validDocument(), Value: value})
	if err != nil || result.Approved || result.ReasonCode != ReasonCode || len([]byte(result.Message)) > 4096 || !utf8.ValidString(result.Message) {
		t.Fatalf("Validate() = %#v, %v; want bounded aggregate rejection", result, err)
	}
	if strings.Count(result.Message, "spell_casts[") != 20 || !strings.Contains(result.Message, "source_refs[19]") || !strings.Contains(result.Message, "additional issue(s) omitted") || !strings.Contains(result.Message, "…") {
		t.Fatalf("message = %q, want all bounded aggregate diagnostics", result.Message)
	}
}

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 source-reference 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(doc *source.SourceDocument, ref source.SourceRef) contracts.TypedValidationRequest[dnd.SpellList] {
	return contracts.TypedValidationRequest[dnd.SpellList]{Source: doc, Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{
		Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{ref},
	}}}}
}

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: "Aria raises her holy symbol."},
			{ID: 2, Kind: "message", Text: "Aria casts Cure Wounds on Borin."},
		},
	}
}
