package shape

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

func TestValidatorRejectsRequiredShapeValues(t *testing.T) {
	value := validNPCRegistry()
	value.NPCs[0].Name = ""
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
	if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "name must not be empty") {
		t.Fatalf("Validate() = %#v, %v; want bounded shape rejection", result, err)
	}

	missing := dnd.NPCRegistry{}
	result, err = New(Options{}).Validate(context.Background(), requestWithValue(missing))
	if err != nil || result.Approved || result.ReasonCode != ReasonCode {
		t.Fatalf("missing Validate() = %#v, %v; want shape rejection", result, err)
	}
}

func TestValidatorBoundsDiagnosticsAndQuotesUnicode(t *testing.T) {
	value := dnd.NPCRegistry{NPCs: make([]dnd.NPC, 24)}
	long := strings.Repeat("火", 220) + "\n\t"
	for index := range value.NPCs {
		value.NPCs[index] = dnd.NPC{ID: "", Name: long, SourceRefs: []source.SourceRef{}}
	}
	result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
	if err != nil || result.Approved || len([]byte(result.Message)) > diagnosticsMaxMessageBytes || !utf8.ValidString(result.Message) {
		t.Fatalf("Validate() = %#v, %v; want bounded valid UTF-8 rejection", result, err)
	}
	if strings.Count(result.Message, "npcs[") > diagnosticsMaxIssues || !strings.Contains(result.Message, "additional issue(s) omitted") {
		t.Fatalf("message = %q, want bounded quoted 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 shape 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 TestValidatorDoesNotMutateValue(t *testing.T) {
	value := validNPCRegistry()
	before := value
	_, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
	if err != nil || value.NPCs[0].Name != before.NPCs[0].Name {
		t.Fatalf("Validate() mutated value: %#v", value)
	}
}

func TestValidatorUsesProposalGuidanceDuringNormalization(t *testing.T) {
	value := validNPCRegistry()
	value.NPCs[0].ID = "npc:sha256:opaque"
	value.NPCs[0].Name = ""
	req := requestWithValue(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, value.NPCs[0].ID) || strings.Contains(result.CorrectionGuidance, "npcs[") {
		t.Fatalf("CorrectionGuidance = %q, want proposal guidance without IDs or operator paths", result.CorrectionGuidance)
	}
}

func requestWithValue(value dnd.NPCRegistry) contracts.TypedValidationRequest[dnd.NPCRegistry] {
	return contracts.TypedValidationRequest[dnd.NPCRegistry]{Value: value}
}

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

const (
	diagnosticsMaxIssues       = 20
	diagnosticsMaxMessageBytes = 4096
)
