package npcregistry

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
	"testing"

	"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/framework/semanticreconcile"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
)

func TestModuleContractAndIdentity(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 {
		t.Fatal("DecodeOptions() accepted unknown option")
	}
	want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCRegistryKind}
	if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
		t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
	}
	registry := pipeline.NewNormalizerRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v", err)
	}
	if _, err := New(nil, Options{}); err == nil {
		t.Fatal("New(nil, Options{}) error = nil, want nil client rejection")
	}
	normalizer := newNormalizer(t, &recordingNPCNormalizerClient{})
	metadata := normalizer.ManifestMetadata()
	limits, ok := metadata["semantic_reconciliation_limits"].(map[string]any)
	if !ok || metadata["identity_policy"] != identity.Policy || metadata["normalization_policy"] != normalizationPolicy || metadata["prompt_id"] != PromptID || metadata["prompt_version"] != PromptVersion || metadata["response_schema_key"] != string(semanticreconcile.ResponseSchemaKey) || metadata["response_schema_id"] != semanticreconcile.ResponseSchemaID || metadata["response_schema_name"] != semanticreconcile.ResponseSchemaName || metadata["semantic_reconciliation_policy"] != semanticreconcile.Policy || len(limits) != 3 {
		t.Fatalf("metadata = %#v", metadata)
	}
	fingerprints := normalizer.CheckpointFingerprints()
	for _, name := range []string{"prompt", "response_schema", "semantic_reconciliation_policy", "semantic_reconciliation_limits", "identity_policy", "normalization_policy"} {
		if !hasFingerprint(fingerprints, name) {
			t.Fatalf("fingerprints = %#v, want %q", fingerprints, name)
		}
	}
}

func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
	input := dnd.NPCRegistry{NPCs: []dnd.NPC{{
		ID: "wrong", Name: "  Lady\tAsh  ", SourceRefs: []source.SourceRef{
			{SourceID: "b", StartUnitID: 2, EndUnitID: 3},
			{SourceID: "a", StartUnitID: 4, EndUnitID: 4},
			{SourceID: "b", StartUnitID: 2, EndUnitID: 3},
		},
	}}}
	result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
	if err != nil {
		t.Fatalf("Normalize() error = %v", err)
	}
	want := dnd.NPC{ID: identity.DeriveID("Lady Ash"), Name: "Lady Ash", SourceRefs: []source.SourceRef{{SourceID: "a", StartUnitID: 4, EndUnitID: 4}, {SourceID: "b", StartUnitID: 2, EndUnitID: 3}}}
	if !reflect.DeepEqual(result.Value.NPCs[0], want) {
		t.Fatalf("NPC = %#v, want %#v", result.Value.NPCs[0], want)
	}
	for _, reason := range []string{ReasonCodeNPCFieldsNormalized, ReasonCodeSourceReferencesNormalized, ReasonCodeNPCIDRecomputed} {
		if !hasDiagnostic(result.Diagnostics, reason, "npcs[0]", contracts.DiagnosticDispositionObservation) {
			t.Fatalf("diagnostics = %#v, want %s", result.Diagnostics, reason)
		}
	}
}

func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
	doc := &source.SourceDocument{ID: "source", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
	input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Lady Ash", SourceRefs: []source.SourceRef{
		{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
		{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
		{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999},
	}}}}
	result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
	if err != nil {
		t.Fatalf("Normalize() error = %v", err)
	}
	got := result.Value.NPCs[0].SourceRefs
	if got[0].StartUnitID != 30 || got[1].StartUnitID != 10 || got[2].StartUnitID != 999 {
		t.Fatalf("source refs = %#v, want source-document order followed by invalid reference", got)
	}
}

func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T) {
	input := dnd.NPCRegistry{NPCs: []dnd.NPC{
		{Name: " Captain Vale ", SourceRefs: []source.SourceRef{{SourceID: "a", StartUnitID: 1, EndUnitID: 1}}},
		{Name: "captain vale", SourceRefs: []source.SourceRef{{SourceID: "b", StartUnitID: 2, EndUnitID: 2}}},
		{Name: "The Captain", SourceRefs: []source.SourceRef{{SourceID: "c", StartUnitID: 3, EndUnitID: 3}}},
	}}
	result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
	if err != nil {
		t.Fatalf("Normalize() error = %v", err)
	}
	if len(result.Value.NPCs) != 2 || result.Value.NPCs[0].Name != "Captain Vale" || result.Value.NPCs[1].Name != "The Captain" {
		t.Fatalf("NPCs = %#v, want name-only stable consolidation", result.Value.NPCs)
	}
	if refs := result.Value.NPCs[0].SourceRefs; len(refs) != 2 || refs[0].SourceID != "a" || refs[1].SourceID != "b" {
		t.Fatalf("source refs = %#v, want evidence union", refs)
	}
	if !hasDiagnostic(result.Diagnostics, ReasonCodeDuplicateNPCCollapsed, "npcs[0]", contracts.DiagnosticDispositionObservation) {
		t.Fatalf("diagnostics = %#v, want duplicate collapse", result.Diagnostics)
	}
}

func TestNormalizePreservesInvalidCandidatesAndDoesNotAliasInput(t *testing.T) {
	input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
	before := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
	result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
	if err != nil || !reflect.DeepEqual(input, before) {
		t.Fatalf("Normalize() = %#v, %v; input mutated to %#v", result, err, input)
	}
	if result.Value.NPCs[0].Name != "" || result.Value.NPCs[0].SourceRefs[0].EndUnitID != -1 {
		t.Fatalf("candidate = %#v, want invalid semantics preserved", result.Value.NPCs[0])
	}
	result.Value.NPCs[0].SourceRefs[0].SourceID = "changed"
	if input.NPCs[0].SourceRefs[0].SourceID != "source" {
		t.Fatal("output aliases input evidence")
	}
}

func TestNormalizeHandlesNilAndCanceledCalls(t *testing.T) {
	normalizer := newNormalizer(t, &recordingNPCNormalizerClient{})
	if _, err := normalizer.Normalize(context.Background(), contracts.TypedNormalizeRequest[dnd.NPCRegistry]{}); err == nil || !strings.Contains(err.Error(), "source document must not be nil") {
		t.Fatalf("nil source Normalize() error = %v", err)
	}
	result, err := normalizer.Normalize(context.Background(), normalizeRequest(dnd.NPCRegistry{NPCs: nil}))
	if err != nil || result.Value.NPCs != nil {
		t.Fatalf("nil list result = %#v, error = %v", result.Value, err)
	}
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	if _, err := normalizer.Normalize(canceled, normalizeRequest(dnd.NPCRegistry{})); err == nil || !strings.Contains(err.Error(), "context error") {
		t.Fatalf("canceled Normalize() error = %v", err)
	}
}

type recordingNPCNormalizerClient struct {
	response  string
	responses []string
	err       error
	requests  []contracts.StructuredCompletionRequest
}

func (c *recordingNPCNormalizerClient) CompleteStructured(_ context.Context, request contracts.StructuredCompletionRequest, output any) (contracts.StructuredCompletionResponse, error) {
	snapshot, err := contracts.CloneStructuredCompletionRequest(request)
	if err != nil {
		return contracts.StructuredCompletionResponse{}, fmt.Errorf("clone recording request: %w", err)
	}
	c.requests = append(c.requests, snapshot)
	if c.err != nil {
		return contracts.StructuredCompletionResponse{}, c.err
	}
	response := c.response
	if len(c.responses) > 0 {
		responseIndex := len(c.requests) - 1
		if responseIndex >= len(c.responses) {
			responseIndex = len(c.responses) - 1
		}
		response = c.responses[responseIndex]
	}
	if response == "" {
		response = `{"duplicate_groups":[]}`
	}
	content := []byte(response)
	if err := json.Unmarshal(content, output); err != nil {
		return contracts.StructuredCompletionResponse{}, err
	}
	return contracts.StructuredCompletionResponse{Content: content}, nil
}

func newNormalizer(t *testing.T, client contracts.StructuredLLMClient) *Normalizer {
	t.Helper()
	normalizer, err := New(client, Options{})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	return normalizer
}

func normalizeRequest(value dnd.NPCRegistry) contracts.TypedNormalizeRequest[dnd.NPCRegistry] {
	return contracts.TypedNormalizeRequest[dnd.NPCRegistry]{
		Source:      &source.SourceDocument{},
		MergeOutput: contracts.MergeArtifact[dnd.NPCRegistry]{Value: value},
	}
}

func normalizeRequestWithSource(value dnd.NPCRegistry, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.NPCRegistry] {
	request := normalizeRequest(value)
	request.Source = doc
	return request
}

func hasDiagnostic(diagnostics []contracts.ProducerDiagnostic, reason, scope string, disposition contracts.DiagnosticDisposition) bool {
	for _, diagnostic := range diagnostics {
		if diagnostic.ReasonCode != reason || diagnostic.Disposition != disposition {
			continue
		}
		for _, sample := range diagnostic.Samples {
			if sample.Scope == scope {
				return true
			}
		}
	}
	return false
}

func hasFingerprint(fingerprints []pipeline.CheckpointFingerprint, name string) bool {
	for _, fingerprint := range fingerprints {
		if fingerprint.Name == name && fingerprint.Value != "" {
			return true
		}
	}
	return false
}
