package sourcerefs

import (
	"context"
	"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 TestValidatorApprovesValidSourceReferences(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: validCombatTurnList()})
	if err != nil || !result.Approved || result.ReasonCode != "" {
		t.Fatalf("Validate() = %#v, %v; want approval", result, err)
	}
}

func TestValidatorRejectsInvalidSourceIdentityExistenceAndOrder(t *testing.T) {
	tests := []struct {
		name string
		ref  source.SourceRef
		want string
	}{
		{name: "missing document", ref: source.SourceRef{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, want: "source document must not be nil"},
		{name: "wrong source identity", ref: source.SourceRef{SourceID: "other", StartUnitID: 1, EndUnitID: 1}, want: "does not match document"},
		{name: "missing start", ref: source.SourceRef{SourceID: "session", StartUnitID: 99, EndUnitID: 99}, want: "start_unit_id 99 was not found"},
		{name: "backward range", ref: source.SourceRef{SourceID: "session", StartUnitID: 3, EndUnitID: 1}, want: "appears after end_unit_id"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			value := validCombatTurnList()
			value.CombatTurns[0].SourceRefs[0] = test.ref
			req := contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value}
			if test.name == "missing document" {
				req.Source = nil
			}
			result, err := New(Options{}).Validate(context.Background(), req)
			if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, test.want) {
				t.Fatalf("Validate() = %#v, %v; want source-reference rejection containing %q", result, err, test.want)
			}
			if err := contracts.ValidateValidationResult(result); err != nil {
				t.Fatalf("ValidateValidationResult() error = %v for %#v", err, result)
			}
		})
	}
}

func TestValidatorEnforcesCurrentChunkEvidenceDuringExtraction(t *testing.T) {
	doc := validDocument()
	value := validCombatTurnList()
	value.CombatTurns[0].SourceRefs[0].EndUnitID = 2
	req := contracts.TypedValidationRequest[dnd.CombatTurnList]{
		Stage:  string(pipeline.StageExtract),
		Source: doc,
		Chunk:  &source.Chunk{SourceID: doc.ID, Units: append([]source.SourceUnit(nil), doc.Units...)},
		Value:  value,
	}
	result, err := New(Options{}).Validate(context.Background(), req)
	if err != nil || !result.Approved {
		t.Fatalf("in-chunk extraction evidence = %#v, %v; want approval", result, err)
	}

	req.Chunk = &source.Chunk{SourceID: doc.ID, Units: []source.SourceUnit{{ID: 1}}}
	result, err = New(Options{}).Validate(context.Background(), req)
	if err != nil || result.Approved || !strings.Contains(result.Message, "outside the current extraction chunk") {
		t.Fatalf("off-chunk extraction evidence = %#v, %v; want rejection", result, err)
	}

	req.Chunk = nil
	if _, err = New(Options{}).Validate(context.Background(), req); err == nil || !strings.Contains(err.Error(), "requires the current extraction chunk") {
		t.Fatalf("missing extraction chunk error = %v", err)
	}

	req.Stage = string(pipeline.StageNormalize)
	result, err = New(Options{}).Validate(context.Background(), req)
	if err != nil || !result.Approved {
		t.Fatalf("document-wide normalization evidence = %#v, %v; want approval", result, err)
	}
}

func TestValidatorDefersMalformedShape(t *testing.T) {
	value := dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{Actor: "Aria"}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value})
	if err != nil || !result.Approved {
		t.Fatalf("shape deferral = %#v, %v; want approval without diagnostics", result, err)
	}
}

func TestValidatorBoundsAggregateDiagnostics(t *testing.T) {
	value := validCombatTurnList()
	value.CombatTurns[0].SourceRefs = make([]source.SourceRef, 24)
	for index := range value.CombatTurns[0].SourceRefs {
		value.CombatTurns[0].SourceRefs[index] = source.SourceRef{SourceID: "session", StartUnitID: 99 + index, EndUnitID: 99 + index}
	}
	value.CombatTurns[0].SourceRefs[0].SourceID = strings.Repeat("火", 220) + "\n\t"
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.CombatTurnList]{Source: validDocument(), Value: value})
	if err != nil || result.Approved || result.ReasonCode != ReasonCode || len([]byte(result.Message)) > 4096 || !utf8.ValidString(result.Message) {
		t.Fatalf("Validate() = %#v, %v; want bounded aggregate rejection", result, err)
	}
	if strings.Count(result.Message, "combat_turns[0].source_refs[") != 20 || !strings.Contains(result.Message, "source_refs[19]") || !strings.Contains(result.Message, "additional issue(s) omitted") || !strings.Contains(result.Message, "…") {
		t.Fatalf("message = %q, want all bounded aggregate diagnostics", result.Message)
	}
}

func TestSpecRegisterOptionsAndPolicy(t *testing.T) {
	if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Name != "policy" || got[0].Value != policy {
		t.Fatalf("CheckpointFingerprints() = %#v, want source-reference policy", got)
	}
	if Spec().Key != Key || Spec().ExecutionClass != contracts.ExecutionClassDeterministic {
		t.Fatalf("Spec() = %#v, want deterministic source-reference validator", Spec())
	}
	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v", err)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
		t.Fatal("DecodeOptions() accepted unknown option")
	}
}

func validCombatTurnList() dnd.CombatTurnList {
	return dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{
		Actor: "Aria", TurnKind: dnd.CombatTurnKindTurn,
		SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
	}}}
}

func validDocument() *source.SourceDocument {
	return &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{
		{ID: 1, Kind: "message", Text: "Aria attacks."},
		{ID: 2, Kind: "message", Text: "The goblin reels."},
		{ID: 3, Kind: "message", Text: "Borin retreats."},
	}}
}
