package identity

import (
	"context"
	"fmt"
	"reflect"
	"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 TestValidatorContractAndRegistration(t *testing.T) {
	if _, err := DecodeOptions(nil); err != nil {
		t.Fatalf("DecodeOptions(nil) error = %v, want nil", err)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil || !strings.Contains(err.Error(), "unknown option") {
		t.Fatalf("DecodeOptions() error = %v, want unknown option error", err)
	}
	if got := New(Options{}).CheckpointFingerprints(); len(got) != 2 || got[0].Name != "policy" || got[0].Value != policy || got[1].Name != "correction_policy" || got[1].Value != correctionPolicy {
		t.Fatalf("fingerprints = %#v, want identity policy", got)
	}

	want := pipeline.ValidatorSpec{Key: Key, ExecutionClass: contracts.ExecutionClassDeterministic}
	if got := Spec(); !reflect.DeepEqual(got, want) {
		t.Fatalf("Spec() = %#v, want %#v", got, want)
	}
	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v, want nil", err)
	}
	if registered, ok := registry.Spec(Key); !ok || !reflect.DeepEqual(registered, want) {
		t.Fatalf("registered spec = %#v, ok = %t, want %#v", registered, ok, want)
	}
	if err := Register(nil); err == nil || !strings.Contains(err.Error(), "validator registry") {
		t.Fatalf("Register(nil) error = %v, want registry error", err)
	}
}

func TestValidatorDefersShapeAndRejectsIdentityIssues(t *testing.T) {
	validator := New(Options{})
	shapeInvalid := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "missing evidence"}}}
	result, err := validator.Validate(context.Background(), validationRequest(shapeInvalid))
	if err != nil || !result.Approved {
		t.Fatalf("shape-invalid result = %#v, error = %v, want deferred approval", result, err)
	}

	value := validNPCRegistry(2)
	value.NPCs[0].ID = "not-an-id"
	value.NPCs[1].Name = value.NPCs[0].Name
	value.NPCs[1].ID = value.NPCs[0].ID
	result, err = validator.Validate(context.Background(), validationRequest(value))
	if err != nil || result.Approved || result.ReasonCode != ReasonCode {
		t.Fatalf("identity result = %#v, error = %v, want rejection", result, err)
	}
	if err := contracts.ValidateValidationResult(result); err != nil {
		t.Fatalf("ValidateValidationResult() error = %v for %#v", err, result)
	}
	for _, want := range []string{"invalid_id", "duplicate_canonical_identity", "duplicate_id", "npcs[0]", "npcs[1]"} {
		if !strings.Contains(result.Message, want) {
			t.Fatalf("identity message %q missing %q", result.Message, want)
		}
	}
	if !strings.Contains(result.CorrectionGuidance, "duplicate-group proposals") || !strings.Contains(result.CorrectionGuidance, value.NPCs[0].Name) || strings.Contains(result.CorrectionGuidance, value.NPCs[0].ID) || strings.Contains(result.CorrectionGuidance, "npcs[") {
		t.Fatalf("CorrectionGuidance = %q, want contextual proposal guidance without IDs or operator paths", result.CorrectionGuidance)
	}
}

func TestValidatorBoundsUnicodeDiagnostics(t *testing.T) {
	value := validNPCRegistry(30)
	for index := range value.NPCs {
		value.NPCs[index].ID = fmt.Sprintf("bad-%d", index)
		value.NPCs[index].Name = strings.Repeat("火", 140) + fmt.Sprintf("-%d", index)
	}
	result, err := New(Options{}).Validate(context.Background(), validationRequest(value))
	if err != nil || result.Approved {
		t.Fatalf("bounded result = %#v, error = %v, want rejection", result, err)
	}
	if !utf8.ValidString(result.Message) || len([]rune(result.Message)) == 0 || len([]byte(result.Message)) > 4096 {
		t.Fatalf("bounded message has invalid size/encoding: bytes=%d message=%q", len([]byte(result.Message)), result.Message)
	}
	if !strings.Contains(result.Message, "additional issue(s) omitted") || strings.Contains(result.Message, strings.Repeat("火", 140)) {
		t.Fatalf("bounded message = %q, want omission and truncation", result.Message)
	}
}

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

func validNPCRegistry(count int) dnd.NPCRegistry {
	value := dnd.NPCRegistry{NPCs: make([]dnd.NPC, count)}
	for index := range value.NPCs {
		name := fmt.Sprintf("NPC %d", index)
		value.NPCs[index] = dnd.NPC{ID: "npc:sha256:0000000000000000000000000000000000000000000000000000000000000000", Name: name, SourceRefs: []source.SourceRef{sourceRefForTest()}}
	}
	return value
}

func sourceRefForTest() source.SourceRef {
	return source.SourceRef{SourceID: "source", StartUnitID: 1, EndUnitID: 1}
}
