package diagnostics

import (
	"fmt"
	"strings"
	"testing"
	"unicode/utf8"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

func TestCorrectionsGroupRulesBeforeContextAndHideKeys(t *testing.T) {
	var corrections Corrections
	corrections.Add("internal-kind", "Use a supported kind.", "Affected goblin at source unit 4.")
	corrections.Add("internal-kind", "Use a supported kind.", "Affected ogre at source units 8-9.")
	corrections.Add("internal-kind", "Use a supported kind.", "Affected goblin at source unit 4.")
	corrections.Add("name", "Provide a contextual name.", "Affected unnamed record at source unit 12.")

	guidance := corrections.Guidance("Correct every record")
	if strings.Contains(guidance, "internal-kind") {
		t.Fatalf("Guidance() exposed grouping key: %q", guidance)
	}
	if strings.Count(guidance, "Use a supported kind.") != 1 || strings.Count(guidance, "Affected goblin") != 1 {
		t.Fatalf("Guidance() did not de-duplicate rules and records: %q", guidance)
	}
	if strings.Index(guidance, "Use a supported kind.") > strings.Index(guidance, "Affected goblin") {
		t.Fatalf("Guidance() = %q, want rules before records", guidance)
	}
}

func TestSourceRangeUsesOnlyTranscriptPositions(t *testing.T) {
	refs := []source.SourceRef{
		{SourceID: "opaque-source", StartUnitID: 8, EndUnitID: 10},
		{SourceID: "opaque-source", StartUnitID: 12, EndUnitID: 12},
	}
	got := SourceRange(refs)
	if strings.Contains(got, "opaque-source") || !strings.Contains(got, "8-10") || !strings.Contains(got, "first of 2") {
		t.Fatalf("SourceRange() = %q", got)
	}
}

func TestAggregateEnforcesByteBudgetAndReportsOmissions(t *testing.T) {
	issues := make([]string, MaxIssues)
	for index := range issues {
		issues[index] = fmt.Sprintf("issue-%d-%s", index, strings.Repeat("火", MaxDisplayedRunes))
	}

	message := Aggregate("invalid D&D data", issues)
	if !utf8.ValidString(message) || len([]byte(message)) > MaxMessageBytes {
		t.Fatalf("Aggregate() returned invalid or oversized message: bytes=%d message=%q", len([]byte(message)), message)
	}
	displayed := strings.Count(message, "issue-")
	if displayed == 0 || displayed >= len(issues) {
		t.Fatalf("Aggregate() displayed %d issues, want byte-budget omission", displayed)
	}
	wantOmitted := fmt.Sprintf("%d additional issue(s) omitted", len(issues)-displayed)
	if !strings.Contains(message, wantOmitted) {
		t.Fatalf("Aggregate() = %q, want %q", message, wantOmitted)
	}
}

func TestDataQualityResultGroupsFindingsWithoutWarnings(t *testing.T) {
	result, err := DataQualityResult([]Finding{
		{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
		{Scope: "records[0]", ReasonCode: "not_near_source", Message: "first"},
		{Scope: "records[1]", ReasonCode: "not_near_source", Message: "second"},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !result.Approved || len(result.Diagnostics) != 1 {
		t.Fatalf("result = %#v", result)
	}
	diagnostic := result.Diagnostics[0]
	if diagnostic.Disposition != contracts.DiagnosticDispositionAdvisory || diagnostic.Category != contracts.DiagnosticCategoryDataQuality || diagnostic.OccurrenceCount != 3 || diagnostic.OmittedSampleCount != 1 || len(diagnostic.Samples) != 2 {
		t.Fatalf("diagnostic = %#v", diagnostic)
	}
}

func TestCollectGroupsNormalizationFindingsWithBoundedSamples(t *testing.T) {
	findings := make([]Finding, 5)
	for index := range findings {
		findings[index] = Finding{
			Scope:      fmt.Sprintf("records[%d]", index),
			ReasonCode: "record_normalized",
			Message:    fmt.Sprintf("record %d normalized", index),
		}
	}

	groups, err := Collect(findings, contracts.DiagnosticDispositionObservation, contracts.DiagnosticCategoryNormalization)
	if err != nil {
		t.Fatal(err)
	}
	if len(groups) != 1 {
		t.Fatalf("groups = %#v, want one grouped observation", groups)
	}
	diagnostic := groups[0]
	if diagnostic.Disposition != contracts.DiagnosticDispositionObservation || diagnostic.Category != contracts.DiagnosticCategoryNormalization || diagnostic.OccurrenceCount != 5 || len(diagnostic.Samples) != contracts.MaxDiagnosticSamples || diagnostic.OmittedSampleCount != 2 {
		t.Fatalf("diagnostic = %#v", diagnostic)
	}
}

func TestNormalizationDiagnosticsCombinesQualityAndCleanupWithoutWarnings(t *testing.T) {
	groups, err := NormalizationDiagnostics([]Finding{
		{Scope: "spell_casts[0]", ReasonCode: "spell_name_canonicalized", Message: "canonicalized spell name"},
		{Scope: "spell_casts[1]", ReasonCode: "spell_name_unresolved", Message: "spell was not in the catalog"},
	}, "spell_name_unresolved")
	if err != nil {
		t.Fatal(err)
	}
	if len(groups) != 2 || groups[0].Disposition != contracts.DiagnosticDispositionObservation || groups[1].Disposition != contracts.DiagnosticDispositionAdvisory {
		t.Fatalf("groups = %#v, want cleanup observation and quality advisory", groups)
	}
	for _, group := range groups {
		if group.Disposition == contracts.DiagnosticDispositionWarning {
			t.Fatalf("groups = %#v, want zero warning groups", groups)
		}
	}
}
