package sourcerefs

import (
	"context"
	"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"
)

func TestValidatorAcceptsCurrentDocumentAndChunkEvidence(t *testing.T) {
	value := validEventList()
	chunk := &source.Chunk{SourceID: "session", Units: []source.SourceUnit{{ID: 1}, {ID: 2}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Stage: string(pipeline.StageExtract), Source: document(), Chunk: chunk, Value: value})
	if err != nil || !result.Approved {
		t.Fatalf("Validate() = %#v, %v", result, err)
	}
}

func TestValidatorRejectsInvalidOrOutOfChunkEvidence(t *testing.T) {
	for _, test := range []struct {
		name    string
		ref     source.SourceRef
		chunk   *source.Chunk
		want    string
		missing bool
	}{
		{"wrong source", source.SourceRef{SourceID: "other", StartUnitID: 1, EndUnitID: 1}, nil, "does not match document", false},
		{"unknown unit", source.SourceRef{SourceID: "session", StartUnitID: 99, EndUnitID: 99}, nil, "was not found", false},
		{"backward range", source.SourceRef{SourceID: "session", StartUnitID: 3, EndUnitID: 1}, nil, "appears after", false},
		{"outside chunk", source.SourceRef{SourceID: "session", StartUnitID: 2, EndUnitID: 3}, &source.Chunk{SourceID: "session", Units: []source.SourceUnit{{ID: 1}, {ID: 2}}}, "outside the current extraction chunk", false},
		{"missing chunk", source.SourceRef{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, nil, "current extraction chunk", true},
	} {
		t.Run(test.name, func(t *testing.T) {
			value := validEventList()
			value.Events[0].SourceRefs[0] = test.ref
			request := contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document(), Value: value}
			if test.chunk != nil || test.missing {
				request.Stage = string(pipeline.StageExtract)
				request.Chunk = test.chunk
			}
			result, err := New(Options{}).Validate(context.Background(), request)
			if test.missing {
				if err == nil || !strings.Contains(err.Error(), test.want) {
					t.Fatalf("Validate() error = %v; want %q", err, test.want)
				}
				return
			}
			if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, test.want) {
				t.Fatalf("Validate() = %#v, %v; want %q", result, err, test.want)
			}
		})
	}
}

func TestValidatorDefersShapeFailureAndRegistersStrictly(t *testing.T) {
	malformed := dnd.EnemyEventList{Events: []dnd.EnemyEvent{{Name: "Ashfang"}}}
	result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.EnemyEventList]{Source: document(), Value: malformed})
	if err != nil || !result.Approved {
		t.Fatalf("shape deferral = %#v, %v", result, err)
	}
	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatal(err)
	}
	if Spec().Key != Key || Spec().ExecutionClass != contracts.ExecutionClassDeterministic {
		t.Fatalf("Spec() = %#v", Spec())
	}
	if got := New(Options{}).CheckpointFingerprints(); len(got) != 1 || got[0].Value != policy {
		t.Fatalf("CheckpointFingerprints() = %#v", got)
	}
	if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
		t.Fatal("DecodeOptions() accepted unknown option")
	}
}

func validEventList() dnd.EnemyEventList {
	return dnd.EnemyEventList{Events: []dnd.EnemyEvent{{Name: "Ashfang", Kind: dnd.EnemyEventKindEngaged, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 2}}}}}
}

func document() *source.SourceDocument {
	return &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1, Text: "Ashfang attacks."}, {ID: 2, Text: "Ashfang retreats."}, {ID: 3, Text: "Later."}}}
}
