package contracts

import (
	"strings"
	"testing"
)

func TestProducerDiagnosticValidationAcceptsClassificationMatrix(t *testing.T) {
	for _, test := range []struct {
		name        string
		disposition DiagnosticDisposition
		category    DiagnosticCategory
	}{
		{name: "configuration warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryConfiguration},
		{name: "degradation warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryDegradation},
		{name: "incomplete validation warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryValidationIncomplete},
		{name: "fallback warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryFallback},
		{name: "quality advisory", disposition: DiagnosticDispositionAdvisory, category: DiagnosticCategoryDataQuality},
		{name: "normalization observation", disposition: DiagnosticDispositionObservation, category: DiagnosticCategoryNormalization},
	} {
		t.Run(test.name, func(t *testing.T) {
			diagnostic := validProducerDiagnostic()
			diagnostic.Disposition = test.disposition
			diagnostic.Category = test.category
			if err := diagnostic.Validate(); err != nil {
				t.Fatalf("Validate() error = %v", err)
			}
		})
	}
}

func TestProducerDiagnosticValidationRejectsInvalidFieldsAndCounts(t *testing.T) {
	tooLongReason := strings.Repeat("r", MaxDiagnosticReasonCodeBytes+1)
	tooLongScope := strings.Repeat("s", MaxDiagnosticScopeBytes+1)
	tooLongMessage := strings.Repeat("m", MaxDiagnosticMessageBytes+1)
	for _, test := range []struct {
		name   string
		mutate func(*ProducerDiagnostic)
	}{
		{name: "invalid classification", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Category = DiagnosticCategoryDataQuality }},
		{name: "blank reason", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = " \t" }},
		{name: "invalid reason UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = string([]byte{0xff}) }},
		{name: "oversized reason", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = tooLongReason }},
		{name: "blank scope", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = "\n" }},
		{name: "invalid scope UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = string([]byte{0xff}) }},
		{name: "oversized scope", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = tooLongScope }},
		{name: "blank message", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = " " }},
		{name: "invalid message UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = string([]byte{0xff}) }},
		{name: "oversized message", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = tooLongMessage }},
		{name: "zero occurrences", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.OccurrenceCount = 0 }},
		{name: "missing samples", mutate: func(diagnostic *ProducerDiagnostic) {
			diagnostic.Samples = nil
			diagnostic.OmittedSampleCount = diagnostic.OccurrenceCount
		}},
		{name: "too many samples", mutate: func(diagnostic *ProducerDiagnostic) {
			diagnostic.OccurrenceCount = 4
			diagnostic.Samples = []DiagnosticSample{{Scope: "one", Message: "one"}, {Scope: "two", Message: "two"}, {Scope: "three", Message: "three"}, {Scope: "four", Message: "four"}}
			diagnostic.OmittedSampleCount = 0
		}},
		{name: "duplicate samples", mutate: func(diagnostic *ProducerDiagnostic) {
			diagnostic.OccurrenceCount = 2
			diagnostic.Samples = []DiagnosticSample{{Scope: "scope", Message: "message"}, {Scope: "scope", Message: "message"}}
			diagnostic.OmittedSampleCount = 0
		}},
		{name: "inconsistent omission", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.OmittedSampleCount = 1 }},
		{name: "producer chunk context", mutate: func(diagnostic *ProducerDiagnostic) { chunkIndex := 0; diagnostic.Samples[0].ChunkIndex = &chunkIndex }},
	} {
		t.Run(test.name, func(t *testing.T) {
			diagnostic := validProducerDiagnostic()
			test.mutate(&diagnostic)
			if err := diagnostic.Validate(); err == nil {
				t.Fatal("Validate() error = nil, want invalid diagnostic error")
			}
		})
	}
}

func TestValidateProducerDiagnosticsEnforcesLocalGroupBound(t *testing.T) {
	diagnostics := make([]ProducerDiagnostic, MaxProducerDiagnosticGroups)
	for index := range diagnostics {
		diagnostics[index] = validProducerDiagnostic()
		diagnostics[index].ReasonCode = "reason-" + string(rune('a'+index))
	}
	if err := ValidateProducerDiagnostics(diagnostics); err != nil {
		t.Fatalf("ValidateProducerDiagnostics() error = %v", err)
	}
	diagnostics = append(diagnostics, validProducerDiagnostic())
	if err := ValidateProducerDiagnostics(diagnostics); err == nil {
		t.Fatal("ValidateProducerDiagnostics() error = nil, want excessive-group error")
	}
}

func TestDiagnosticGroupValidationPreservesChunkIndexZero(t *testing.T) {
	chunkIndex := 0
	group := DiagnosticGroup{
		Disposition:     DiagnosticDispositionAdvisory,
		Category:        DiagnosticCategoryDataQuality,
		ReasonCode:      "unresolved",
		Origin:          DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells", ValidatorKey: "dnd/spells/source-relatedness"},
		OccurrenceCount: 1,
		Samples:         []DiagnosticSample{{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &chunkIndex}},
	}
	if err := group.Validate(); err != nil {
		t.Fatalf("Validate() error = %v", err)
	}
	collection := DiagnosticCollection{Groups: []DiagnosticGroup{group}}
	if err := collection.Validate(); err != nil {
		t.Fatalf("DiagnosticCollection.Validate() error = %v", err)
	}
}

func TestDiagnosticGroupRejectsRepeatedSampleWithEqualChunkIndex(t *testing.T) {
	firstIndex := 0
	secondIndex := 0
	group := DiagnosticGroup{
		Disposition:     DiagnosticDispositionAdvisory,
		Category:        DiagnosticCategoryDataQuality,
		ReasonCode:      "unresolved",
		Origin:          DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"},
		OccurrenceCount: 2,
		Samples: []DiagnosticSample{
			{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &firstIndex},
			{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &secondIndex},
		},
	}
	if err := group.Validate(); err == nil {
		t.Fatal("Validate() error = nil, want duplicate sample error")
	}
}

func TestCloneDiagnosticCollectionOwnsGroupsAndChunkIndex(t *testing.T) {
	chunkIndex := 0
	collection := DiagnosticCollection{Groups: []DiagnosticGroup{{
		Disposition:     DiagnosticDispositionAdvisory,
		Category:        DiagnosticCategoryDataQuality,
		ReasonCode:      "unresolved",
		Origin:          DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"},
		OccurrenceCount: 1,
		Samples:         []DiagnosticSample{{Scope: "spells[0]", Message: "Spell was not found", ChunkIndex: &chunkIndex}},
	}}}
	cloned := CloneDiagnosticCollection(collection)
	collection.Groups[0].Samples[0].Message = "changed"
	*collection.Groups[0].Samples[0].ChunkIndex = 1
	if got := cloned.Groups[0].Samples[0]; got.Message != "Spell was not found" || got.ChunkIndex == nil || *got.ChunkIndex != 0 {
		t.Fatalf("cloned sample = %#v, want independently owned original", got)
	}
}

func TestProjectDiagnosticCollectionPartitionsAndChecksTotals(t *testing.T) {
	warning := validDiagnosticGroup(DiagnosticDispositionWarning, DiagnosticCategoryFallback, "fallback", 2)
	diagnostic := validDiagnosticGroup(DiagnosticDispositionAdvisory, DiagnosticCategoryDataQuality, "quality", 3)
	collection := DiagnosticCollection{
		Groups:                       []DiagnosticGroup{warning, diagnostic},
		Truncated:                    true,
		UnrepresentedOccurrenceCount: 4,
	}
	projection, err := ProjectDiagnosticCollection(collection)
	if err != nil {
		t.Fatal(err)
	}
	if len(projection.Warnings) != 1 || len(projection.Diagnostics) != 1 || projection.WarningOccurrenceCount != 2 || projection.DiagnosticOccurrenceCount != 7 {
		t.Fatalf("projection = %#v, want partitioned exact totals", projection)
	}
	collection.Groups[0].Samples[0].Message = "mutated"
	if projection.Warnings[0].Samples[0].Message != "message" {
		t.Fatal("projection retained caller-owned sample storage")
	}

	overflow := DiagnosticCollection{Groups: []DiagnosticGroup{
		validDiagnosticGroup(DiagnosticDispositionWarning, DiagnosticCategoryFallback, "first", int(^uint(0)>>1)),
		validDiagnosticGroup(DiagnosticDispositionWarning, DiagnosticCategoryFallback, "second", 1),
	}}
	if _, err := ProjectDiagnosticCollection(overflow); err == nil {
		t.Fatal("ProjectDiagnosticCollection() overflow error = nil")
	}
}

func validDiagnosticGroup(disposition DiagnosticDisposition, category DiagnosticCategory, reason string, occurrences int) DiagnosticGroup {
	return DiagnosticGroup{
		Disposition:        disposition,
		Category:           category,
		ReasonCode:         reason,
		Origin:             DiagnosticOrigin{Stage: DiagnosticOriginStageNormalize, StepID: "step", LaneID: "lane", ModuleKey: "module"},
		OccurrenceCount:    occurrences,
		Samples:            []DiagnosticSample{{Scope: "scope", Message: "message"}},
		OmittedSampleCount: occurrences - 1,
	}
}

func validProducerDiagnostic() ProducerDiagnostic {
	return ProducerDiagnostic{
		Disposition:     DiagnosticDispositionWarning,
		Category:        DiagnosticCategoryConfiguration,
		ReasonCode:      "empty_reference",
		OccurrenceCount: 1,
		Samples:         []DiagnosticSample{{Scope: "references.glossary", Message: "Reference is empty"}},
	}
}
