package sourcerefs

import (
	"context"
	"fmt"
	"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 TestValidatorApprovesValidSourceReferences(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), validNPCRegistry()))
	if err != nil || !result.Approved {
		t.Fatalf("Validate() = %#v, %v; want approval", result, err)
	}
}

func TestValidatorRejectsInvalidSourceReferences(t *testing.T) {
	value := validNPCRegistry()
	value.NPCs[0].SourceRefs = []source.SourceRef{
		{SourceID: "foreign", StartUnitID: 1, EndUnitID: 1},
		{SourceID: "session", StartUnitID: 2, EndUnitID: 1},
		{SourceID: "session", StartUnitID: 99, EndUnitID: 99},
	}
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), value))
	if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "npcs[0].source_refs[0]") {
		t.Fatalf("Validate() = %#v, %v; want source-reference rejection", result, err)
	}
}

func TestValidatorEnforcesCurrentChunkEvidenceDuringExtraction(t *testing.T) {
	doc := validDocument()
	value := validNPCRegistry()
	req := requestWithValue(doc, value)
	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.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn"}}}
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), value))
	if err != nil || !result.Approved || result.ReasonCode != "" || result.Message != "" {
		t.Fatalf("Validate() = %#v, %v; want shape deferral", result, err)
	}
}

func TestValidatorUsesProposalGuidanceDuringNormalization(t *testing.T) {
	value := validNPCRegistry()
	value.NPCs[0].SourceRefs = []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}
	req := requestWithValue(validDocument(), value)
	req.Stage = string(pipeline.StageNormalize)
	result, err := New(Options{}).Validate(context.Background(), req)
	if err != nil || result.Approved {
		t.Fatalf("Validate() = %#v, %v; want rejection", result, err)
	}
	if !strings.Contains(result.CorrectionGuidance, "duplicate-group proposals") || !strings.Contains(result.CorrectionGuidance, "Mira Thorn") || strings.Contains(result.CorrectionGuidance, "npcs[") {
		t.Fatalf("CorrectionGuidance = %q, want contextual proposal guidance", result.CorrectionGuidance)
	}
}

func TestValidatorBoundsDiagnosticsAndHandlesMissingDocument(t *testing.T) {
	value := validNPCRegistry()
	value.NPCs[0].SourceRefs = make([]source.SourceRef, 24)
	for index := range value.NPCs[0].SourceRefs {
		value.NPCs[0].SourceRefs[index] = source.SourceRef{SourceID: strings.Repeat("火", 220) + "\n\t", StartUnitID: index + 1, EndUnitID: index + 1}
	}
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(nil, value))
	if err != nil || result.Approved || len([]byte(result.Message)) > 4096 || !utf8.ValidString(result.Message) {
		t.Fatalf("Validate() = %#v, %v; want bounded missing-document rejection", result, err)
	}
	if !strings.Contains(result.Message, "additional issue(s) omitted") || !strings.Contains(result.Message, fmt.Sprintf("npcs[0].source_refs[%d]", 19)) {
		t.Fatalf("message = %q, want 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().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 requestWithValue(doc *source.SourceDocument, value dnd.NPCRegistry) contracts.TypedValidationRequest[dnd.NPCRegistry] {
	return contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: doc, Value: value}
}

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: "Mira Thorn enters."},
		{ID: 2, Kind: "message", Text: "The ranger watches."},
	}}
}

func validNPCRegistry() dnd.NPCRegistry {
	return dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: "candidate", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 2}}}}}
}
