package shared

import (
	"reflect"
	"strings"
	"testing"

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

func TestPromptInputsIdentity(t *testing.T) {
	source := contracts.NewLLMInputMaterial("source", "application/json", []byte("source text"), "sha256:source", "file:///source.json")
	baseReferences := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		"players":  slotWithContent("players", "Alice: Aria"),
		"party":    slotWithContent("party", "Aria: cleric"),
		"glossary": slotWithContent("glossary", "Brightmantle: temple"),
	}}
	orderedReferences := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		"players": {Items: []contracts.ReferenceItem{
			referenceItem("players", "file:///b.txt", "sha256:bbb", "second"),
			referenceItem("players", "file:///a.txt", "sha256:aaa", "first"),
		}},
	}}
	reversedReferences := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		"players": {Items: []contracts.ReferenceItem{
			referenceItem("players", "file:///a.txt", "sha256:aaa", "first"),
			referenceItem("players", "file:///b.txt", "sha256:bbb", "second"),
		}},
	}}
	rosterReferences := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		"party":  {},
		"roster": slotWithContent("roster", "Legacy roster text"),
	}}
	explicitPartyReferences := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		"party":  slotWithContent("party", "Current party text"),
		"roster": slotWithContent("roster", "Legacy roster text"),
	}}

	tests := []struct {
		name       string
		references contracts.ReferenceSet
		other      *contracts.ReferenceSet
		want       map[string]string
	}{
		{
			name:       "identical materials produce deeply equal inputs",
			references: baseReferences,
			other:      &baseReferences,
			want: map[string]string{
				"transcript": "source text",
				"players":    "Alice: Aria",
				"party":      "Aria: cleric",
				"glossary":   "Brightmantle: temple",
			},
		},
		{
			name:       "reference insertion order does not change bytes",
			references: orderedReferences,
			other:      &reversedReferences,
			want:       map[string]string{"players": "Reference 1\nOrigin-URI: file:///a.txt\nDigest: sha256:aaa\n\nfirst\n\nReference 2\nOrigin-URI: file:///b.txt\nDigest: sha256:bbb\n\nsecond"},
		},
		{
			name:       "roster becomes canonical party input",
			references: rosterReferences,
			want:       map[string]string{"party": "Legacy roster text"},
		},
		{
			name:       "explicit party wins over roster",
			references: explicitPartyReferences,
			want:       map[string]string{"party": "Current party text"},
		},
		{
			name:       "missing optional slots use placeholders",
			references: contracts.ReferenceSet{},
			want: map[string]string{
				"players":  " ",
				"party":    " ",
				"glossary": " ",
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			inputs := PromptInputs(source, test.references)
			if test.other != nil {
				other := PromptInputs(source, *test.other)
				if !reflect.DeepEqual(inputs, other) {
					t.Fatalf("PromptInputs() differs for equivalent references:\nfirst=%#v\nsecond=%#v", inputs, other)
				}
			}
			for name, want := range test.want {
				got, ok := inputs[name]
				if !ok {
					t.Fatalf("PromptInputs() missing %q: %#v", name, inputs)
				}
				if got := string(got.Content); got != want {
					t.Fatalf("%s content = %q, want %q", name, got, want)
				}
			}
			if _, ok := inputs["roster"]; ok {
				t.Fatalf("PromptInputs() included roster input: %#v", inputs)
			}
		})
	}
}

func TestTranscriptPromptMaterialClonesSource(t *testing.T) {
	source := contracts.NewLLMInputMaterial("source", "text/plain", []byte("source text"), "sha256:source", "file:///source.txt")
	got := TranscriptPromptMaterial(source)

	if got.Name != "transcript" {
		t.Fatalf("Name = %q, want transcript", got.Name)
	}
	if got.MediaType != source.MediaType || got.Digest != source.Digest || got.OriginURI != source.OriginURI || got.SizeBytes != source.SizeBytes {
		t.Fatalf("TranscriptPromptMaterial() = %#v, want cloned metadata from %#v", got, source)
	}
	source.Content[0] = 'X'
	if string(got.Content) != "source text" {
		t.Fatalf("TranscriptPromptMaterial() reused content slice: %q", got.Content)
	}
}

func TestReferencePromptMaterialUsesTextPlainAndSingleReferenceMetadata(t *testing.T) {
	slot := contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{{
		Content: []byte("single reference"),
		Digest:  "sha256:reference",
		Origin:  contracts.ReferenceOrigin{Type: "file", URI: "file:///reference.md"},
	}}}

	got := ReferencePromptMaterial("party", slot)
	if got.Name != "party" || got.MediaType != "text/plain" {
		t.Fatalf("ReferencePromptMaterial() name/media = %q/%q, want party/text/plain", got.Name, got.MediaType)
	}
	if got.Digest != "sha256:reference" || got.OriginURI != "file:///reference.md" {
		t.Fatalf("ReferencePromptMaterial() digest/origin = %q/%q, want single reference metadata", got.Digest, got.OriginURI)
	}
	if string(got.Content) != "single reference" {
		t.Fatalf("ReferencePromptMaterial() content = %q, want raw single reference", got.Content)
	}
}

func TestReferencePromptMaterialOmitsAggregateMetadata(t *testing.T) {
	got := ReferencePromptMaterial("party", contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{
		{Content: []byte("one"), Digest: "sha256:one", Origin: contracts.ReferenceOrigin{URI: "file:///one.md"}},
		{Content: []byte("two"), Digest: "sha256:two", Origin: contracts.ReferenceOrigin{URI: "file:///two.md"}},
	}})

	if got.Digest != "" || got.OriginURI != "" {
		t.Fatalf("ReferencePromptMaterial() digest/origin = %q/%q, want empty aggregate metadata", got.Digest, got.OriginURI)
	}
}

func TestReferencePromptInputRendering(t *testing.T) {
	if got := string(ReferencePromptInput(contracts.ResolvedReferenceSlot{})); got != " " {
		t.Fatalf("empty rendering = %q, want single space", got)
	}
	if got := string(ReferencePromptInput(slotWithContent("party", "single reference"))); got != "single reference" {
		t.Fatalf("single rendering = %q, want raw content", got)
	}

	slot := contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{
		{
			SlotName:  "party",
			MediaType: "text/plain",
			Content:   []byte("second"),
			Digest:    "sha256:bbb",
			Origin:    contracts.ReferenceOrigin{Type: "file", URI: "file:///b.txt"},
			SizeBytes: 6,
		},
		{
			SlotName:  "party",
			MediaType: "text/plain",
			Content:   []byte("first"),
			Digest:    "sha256:aaa",
			Origin:    contracts.ReferenceOrigin{Type: "file", URI: "file:///a.txt"},
			SizeBytes: 5,
		},
	}}
	first := string(ReferencePromptInput(slot))
	second := string(ReferencePromptInput(slot))
	if first != second {
		t.Fatalf("ReferencePromptInput() was not deterministic:\nfirst=%q\nsecond=%q", first, second)
	}
	if !strings.Contains(first, "Reference 1\nOrigin-Type: file\nOrigin-URI: file:///a.txt\nDigest: sha256:aaa\nMedia-Type: text/plain\nSize-Bytes: 5\n\nfirst") {
		t.Fatalf("first reference block = %q, want sorted first reference metadata", first)
	}
	if strings.Index(first, "first") > strings.Index(first, "second") {
		t.Fatalf("references were not sorted deterministically: %q", first)
	}
}

func TestReferencePromptInputOrdersRenderedMetadataDeterministically(t *testing.T) {
	base := contracts.ReferenceItem{
		SlotName:  "party",
		MediaType: "text/plain",
		Content:   []byte("same content"),
		Digest:    "sha256:same",
		Origin:    contracts.ReferenceOrigin{Type: "file", URI: "file:///same.txt"},
		SizeBytes: 12,
	}
	tests := []struct {
		name  string
		first contracts.ReferenceItem
		last  contracts.ReferenceItem
	}{
		{
			name:  "origin type",
			first: referenceItemWithMetadata(base, "archive", "text/plain", 12),
			last:  referenceItemWithMetadata(base, "file", "text/plain", 12),
		},
		{
			name:  "media type",
			first: referenceItemWithMetadata(base, "file", "application/json", 12),
			last:  referenceItemWithMetadata(base, "file", "text/plain", 12),
		},
		{
			name:  "size",
			first: referenceItemWithMetadata(base, "file", "text/plain", 11),
			last:  referenceItemWithMetadata(base, "file", "text/plain", 12),
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			forward := ReferencePromptInput(contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{test.first, test.last}})
			reversed := ReferencePromptInput(contracts.ResolvedReferenceSlot{Items: []contracts.ReferenceItem{test.last, test.first}})
			if !reflect.DeepEqual(forward, reversed) {
				t.Fatalf("rendered bytes depend on insertion order:\nforward=%q\nreversed=%q", forward, reversed)
			}
		})
	}
}

func slotWithContent(name string, content string) contracts.ResolvedReferenceSlot {
	return contracts.ResolvedReferenceSlot{
		Slot: contracts.ReferenceSlot{Name: name},
		Items: []contracts.ReferenceItem{{
			SlotName: name,
			Content:  []byte(content),
		}},
	}
}

func referenceItem(slotName, uri, digest, content string) contracts.ReferenceItem {
	return contracts.ReferenceItem{
		SlotName: slotName,
		Content:  []byte(content),
		Digest:   digest,
		Origin:   contracts.ReferenceOrigin{URI: uri},
	}
}

func referenceItemWithMetadata(base contracts.ReferenceItem, originType, mediaType string, size int64) contracts.ReferenceItem {
	base.Origin.Type = originType
	base.MediaType = mediaType
	base.SizeBytes = size
	return base
}
