package locationregistry

import (
	"context"
	"errors"
	"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/modules/dnd"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
	"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/entityreconcile"
)

func TestModuleContractAndMetadata(t *testing.T) {
	want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.LocationRegistryKind}
	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.Fatal(err)
	}
	if _, err := New(nil, Options{}); err == nil {
		t.Fatal("New() accepted nil client")
	}
	normalizer := newNormalizer(t, &recordingLocationNormalizerClient{})
	metadata := normalizer.ManifestMetadata()
	if metadata["identity_policy"] != identity.Policy || metadata["response_schema_id"] != entityreconcile.ResponseSchemaID || metadata["normalization_policy"] != normalizationPolicy || metadata["semantic_context_radius"] != semanticContextRadius {
		t.Fatalf("metadata = %#v", metadata)
	}
	if got := normalizer.CheckpointFingerprints(); len(got) != 5 || got[2].Value != identity.Policy || got[3].Value != normalizationPolicy || got[4].Value != semanticContextPolicy+":2" {
		t.Fatalf("fingerprints = %#v", got)
	}
}

func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t *testing.T) {
	input := dnd.LocationRegistry{Locations: []dnd.Location{
		{Name: "  The Tavern  ", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
		{Name: "the tavern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
		{Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
		{Name: "The Tavern Cellar", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
	}}
	before := dnd.LocationRegistry{Locations: append([]dnd.Location(nil), input.Locations...)}
	result, err := newNormalizer(t, &recordingLocationNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
	if err != nil || len(result.Value.Locations) != 3 {
		t.Fatalf("Normalize() = %#v, %v; want one exact duplicate removed", result, err)
	}
	if !reflect.DeepEqual(input, before) {
		t.Fatalf("Normalize() mutated input: %#v", input)
	}
	if got := []string{result.Value.Locations[0].Name, result.Value.Locations[1].Name, result.Value.Locations[2].Name}; !reflect.DeepEqual(got, []string{"The Tavern", "The Tavern", "The Tavern Cellar"}) {
		t.Fatalf("locations = %#v, want same names and nested place retained", got)
	}
	if result.Value.Locations[0].ID == result.Value.Locations[1].ID || !hasWarning(result.Warnings, ReasonCodeDuplicateLocationCollapsed) {
		t.Fatalf("result = %#v, want evidence-anchored IDs and exact duplicate warning", result)
	}
}

func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) {
	client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
	doc := semanticDocument()
	input := dnd.LocationRegistry{Locations: []dnd.Location{
		{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
		{Name: "the Greencloak's refuge", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
		{Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
	}}
	result, err := newNormalizer(t, client).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
	if err != nil || result.Retry != nil || len(result.Value.Locations) != 2 {
		t.Fatalf("Normalize() = %#v, %v", result, err)
	}
	merged := result.Value.Locations[0]
	if merged.Name != "the Greencloak's refuge" || merged.ID != identity.DeriveID(merged.Name, merged.SourceRefs) || len(merged.SourceRefs) != 2 || !hasWarning(result.Warnings, ReasonCodeDuplicateLocationCollapsed) {
		t.Fatalf("merged location = %#v, warnings = %#v", merged, result.Warnings)
	}
	encoded := string(client.requests[0].Inputs["candidates"].Content) + string(client.requests[0].Inputs["transcript"].Content)
	if strings.Contains(encoded, doc.ID) || !strings.Contains(encoded, "candidate-000001") || strings.Contains(encoded, merged.ID) {
		t.Fatalf("private inputs = %s", encoded)
	}
}

func TestNormalizeRejectsUnsafeAndOverlappingGroupsWithoutLosingCandidates(t *testing.T) {
	doc := semanticDocument()
	input := dnd.LocationRegistry{Locations: []dnd.Location{
		{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
		{Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
		{Name: "Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
	}}
	client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000001"},{"members":["candidate-000002","candidate-000003"],"canonical":"candidate-000003"}]}`}
	result, err := newNormalizer(t, client).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
	if err != nil || result.Retry == nil || len(result.Value.Locations) != 3 || !strings.Contains(result.Retry.Message, "overlapping_member") {
		t.Fatalf("Normalize() = %#v, %v; want safe retry fallback", result, err)
	}
	if len(result.Retry.FallbackWarnings) != 1 || !strings.Contains(result.Retry.FallbackWarnings[0].Message, "2 proposal group") {
		t.Fatalf("fallback warnings = %#v", result.Retry.FallbackWarnings)
	}
}

func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) {
	doc := semanticDocument()
	input := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}}}
	invalid, err := newNormalizer(t, &recordingLocationNormalizerClient{err: contracts.ErrInvalidStructuredOutput}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
	if err != nil || invalid.Retry == nil || invalid.Retry.ReasonCode != ReasonCodeLocationSemanticProposalInvalid {
		t.Fatalf("invalid result = %#v, %v", invalid, err)
	}
	_, err = newNormalizer(t, &recordingLocationNormalizerClient{err: errors.New("provider unavailable")}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
	if err == nil || !strings.Contains(err.Error(), "provider unavailable") {
		t.Fatalf("provider error = %v", err)
	}
	warnings := make([]contracts.Warning, 25)
	bounded := limitWarningsForRetry(warnings)
	if len(bounded) != 19 || bounded[len(bounded)-1].ReasonCode != ReasonCodeLocationNormalizationWarningsOmitted {
		t.Fatalf("retry warning limit = %#v", bounded)
	}
}

func TestNormalizeOrdersEvidenceAndIsIdempotent(t *testing.T) {
	doc := &source.SourceDocument{ID: "ordered", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
	input := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{
		{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
		{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
		{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
	}}}}
	normalizer := newNormalizer(t, &recordingLocationNormalizerClient{})
	first, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
	if err != nil || !reflect.DeepEqual([]int{first.Value.Locations[0].SourceRefs[0].StartUnitID, first.Value.Locations[0].SourceRefs[1].StartUnitID}, []int{30, 10}) {
		t.Fatalf("first Normalize() = %#v, %v; want document-ordered evidence", first, err)
	}
	second, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(first.Value, doc))
	if err != nil || !reflect.DeepEqual(second.Value, first.Value) {
		t.Fatalf("second Normalize() = %#v, %v; want idempotent value %#v", second, err, first.Value)
	}
}
