package scenedescriptions

import (
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"

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

func TestExtractMapsExactlyOneSceneToTheChunk(t *testing.T) {
	client := &fakeSceneDescriptionsLLMClient{response: extractionResponse{
		Kind: dnd.SceneKindCombat, Title: "  Fight at the Watchtower  ", Summary: "  Bandits attack the party.  ",
	}}
	request := extractionRequest()
	result, err := newExtractor(t, client).Extract(context.Background(), request)
	if err != nil {
		t.Fatalf("Extract() error = %v, want nil", err)
	}
	want := dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{
		ID: request.Chunk.ID, SourceRef: request.Chunk.Ref, Kind: dnd.SceneKindCombat,
		Title: "Fight at the Watchtower", Summary: "Bandits attack the party.",
	}}}
	if !reflect.DeepEqual(result.Value, want) {
		t.Fatalf("Value = %#v, want %#v", result.Value, want)
	}
	if len(client.requests) != 1 {
		t.Fatalf("LLM calls = %d, want 1", len(client.requests))
	}
	completed := client.requests[0]
	if completed.StageName != Key || completed.PromptID != PromptID || completed.PromptVersion != SchemaVersion || completed.ProfileID != "profile-scene-descriptions" || completed.SessionID != "session-123" {
		t.Fatalf("LLM request identity = %#v", completed)
	}
	transcript := completed.Inputs["transcript"]
	if transcript.Name != "transcript" || transcript.MediaType != "application/json" || transcript.Digest != "sha256:chunk" || transcript.OriginURI != "file:///session-alpha.json" || string(transcript.Content) != string(request.Chunk.Content) {
		t.Fatalf("transcript input = %#v, want chunk-scoped material", transcript)
	}
}

func TestExtractPassesOptionalReferencesAndUsesEmptyPlaceholders(t *testing.T) {
	client := &fakeSceneDescriptionsLLMClient{response: extractionResponse{Kind: dnd.SceneKindNarrative, Title: "Arrival", Summary: "The party arrives."}}
	req := extractionRequest()
	if _, err := newExtractor(t, client).Extract(context.Background(), req); err != nil {
		t.Fatalf("Extract() error = %v, want nil", err)
	}
	for _, slot := range []string{"players", "party", "glossary"} {
		if got := string(client.requests[0].Inputs[slot].Content); got != " " {
			t.Fatalf("empty %s input = %q, want explicit placeholder", slot, got)
		}
	}

	req.References = contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
		"players":  {Slot: contracts.ReferenceSlot{Name: "players"}, Items: []contracts.ReferenceItem{{SlotName: "players", Content: []byte("Dana: Mira")}}},
		"party":    {Slot: contracts.ReferenceSlot{Name: "party"}, Items: []contracts.ReferenceItem{{SlotName: "party", Content: []byte("Mira: ranger")}}},
		"glossary": {Slot: contracts.ReferenceSlot{Name: "glossary"}, Items: []contracts.ReferenceItem{{SlotName: "glossary", Content: []byte("Greencloak: local title")}}},
	}}
	if _, err := newExtractor(t, client).Extract(context.Background(), req); err != nil {
		t.Fatalf("Extract() error = %v, want nil", err)
	}
	inputs := client.requests[1].Inputs
	if string(inputs["players"].Content) != "Dana: Mira" || string(inputs["party"].Content) != "Mira: ranger" || string(inputs["glossary"].Content) != "Greencloak: local title" {
		t.Fatalf("reference inputs = %#v", inputs)
	}
	if strings.Contains(string(inputs["transcript"].Content), "local title") {
		t.Fatal("transcript input contains reference content")
	}
}

func TestExtractReturnsSemanticallyInvalidResponseForDeterministicValidation(t *testing.T) {
	schema, err := loadResponseSchema()
	if err != nil {
		t.Fatalf("loadResponseSchema() error = %v", err)
	}
	if err := validateJSONSchema(t, map[string]any{"kind": "narrative", "title": " ", "summary": ""}, schema.JSONSchema); err != nil {
		t.Fatalf("semantic candidate rejected by private schema: %v", err)
	}
	client := &fakeSceneDescriptionsLLMClient{response: extractionResponse{
		Kind: dnd.SceneKindNarrative, Title: " ", Summary: "",
	}}
	result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
	if err != nil {
		t.Fatalf("Extract() error = %v, want nil", err)
	}
	scene := result.Value.Scenes[0]
	if scene.Kind != dnd.SceneKindNarrative || scene.Title != "" || scene.Summary != "" {
		t.Fatalf("scene = %#v, want semantic candidates returned for deterministic validation", scene)
	}
}

func TestExtractValidatesRequestsAndSurfacesProviderFailures(t *testing.T) {
	request := extractionRequest()
	extractor := newExtractor(t, &fakeSceneDescriptionsLLMClient{response: extractionResponse{Kind: dnd.SceneKindMeta, Title: "Table talk", Summary: "The group discusses rules."}})
	var nilExtractor *Extractor
	for _, test := range []struct {
		name      string
		extractor *Extractor
		req       contracts.TypedExtractionRequest
		want      string
	}{
		{name: "nil extractor", extractor: nilExtractor, req: request, want: "extractor"},
		{name: "nil LLM client", extractor: &Extractor{}, req: request, want: "LLM client"},
		{name: "wrapped preflight failure", extractor: extractor, req: mismatchedSourceInputRequest(request), want: "must match chunk"},
	} {
		t.Run(test.name, func(t *testing.T) {
			if _, err := test.extractor.Extract(context.Background(), test.req); err == nil || !strings.Contains(err.Error(), "dnd scene descriptions") || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("Extract() error = %v, want contextual validation error", err)
			}
		})
	}
	_, err := newExtractor(t, &fakeSceneDescriptionsLLMClient{err: errors.New("provider unavailable")}).Extract(context.Background(), request)
	if err == nil || !strings.Contains(err.Error(), "dnd scene descriptions") || !strings.Contains(err.Error(), "provider unavailable") {
		t.Fatalf("provider Extract() error = %v, want contextual provider error", err)
	}
}

func TestMapResponseUsesOnlyChunkIdentityAndRange(t *testing.T) {
	chunk := &source.Chunk{ID: "source:chunk:7", Ref: source.SourceRef{SourceID: "source", StartUnitID: 12, EndUnitID: 14}}
	got := mapResponse(extractionResponse{Kind: dnd.SceneKindRecap, Title: " Recap ", Summary: " Summary "}, chunk)
	want := dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{
		ID: "source:chunk:7", SourceRef: source.SourceRef{SourceID: "source", StartUnitID: 12, EndUnitID: 14},
		Kind: dnd.SceneKindRecap, Title: "Recap", Summary: "Summary",
	}}}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("mapResponse() = %#v, want %#v", got, want)
	}
}
