package llm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"testing/fstest"
	"time"

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

func TestPromptKitClientMapsPromptRequestAndUnmarshalsOutput(t *testing.T) {
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client := newTestPromptKitClient(t, fake)

	var out struct {
		OK bool `json:"ok"`
	}
	resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		StageName:     "test-stage",
		PromptID:      "adapter.direct-session",
		PromptVersion: "v1",
		ProfileID:     "explicit-profile",
		SessionID:     " session-123 ",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "sha256:source", "file:///source.json"),
		},
		Vars: map[string]any{"custom": "value"},
	}, &out)
	if err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", err)
	}
	if !out.OK {
		t.Fatalf("decoded output OK = false, want true")
	}
	if resp.Provider != "promptkit" || resp.Model != "explicit-model" || resp.ProfileID != "explicit-profile" {
		t.Fatalf("response metadata = %#v", resp)
	}
	if resp.PromptTokens != 11 || resp.CompletionTokens != 7 || resp.TotalTokens != 18 {
		t.Fatalf("usage = %#v, want mapped token counts", resp)
	}
	if resp.Debug == nil || resp.Debug.Prompt == nil {
		t.Fatalf("debug prompt = nil, want prepared prompt material")
	}
	if resp.Debug.Prompt.PromptID != "adapter.direct-session" ||
		resp.Debug.Prompt.SelectedProfileID != "explicit-profile" ||
		resp.Debug.Prompt.SelectedBackendID != "test-backend" ||
		resp.Debug.Prompt.SessionID != "session-123" {
		t.Fatalf("debug prompt metadata = %#v, want prompt/profile/backend/session", resp.Debug.Prompt)
	}
	if resp.Debug.Prompt.EffectiveModelParams["backend_id"] != "test-backend" ||
		resp.Debug.Prompt.EffectiveModelParams["reasoning_effort"] != "profile-reasoning" {
		t.Fatalf("debug effective model params = %#v, want backend and reasoning", resp.Debug.Prompt.EffectiveModelParams)
	}
	if len(resp.Debug.Prompt.Messages) != 1 || !strings.Contains(resp.Debug.Prompt.Messages[0].Content, `{"source":true}`) {
		t.Fatalf("debug prompt messages = %#v, want rendered input content", resp.Debug.Prompt.Messages)
	}
	if resp.Debug.Response == nil || resp.Debug.Response.Content != `{"ok":true}` {
		t.Fatalf("debug response = %#v, want raw response content", resp.Debug.Response)
	}
	if resp.Debug.Response.Usage.CachedTokens != 5 || resp.Debug.Response.Usage.CacheWriteTokens != 3 {
		t.Fatalf("debug usage = %#v, want cached token counts", resp.Debug.Response.Usage)
	}
	if resp.Debug.Response.EffectiveModelParams["backend_id"] != "test-backend" ||
		resp.Debug.Response.EffectiveModelParams["reasoning_effort"] != "profile-reasoning" {
		t.Fatalf("debug response effective model params = %#v, want backend and reasoning", resp.Debug.Response.EffectiveModelParams)
	}
	debugJSON, err := json.Marshal(resp.Debug)
	if err != nil {
		t.Fatalf("marshal debug material: %v", err)
	}
	if strings.Contains(string(debugJSON), "secret-token") || strings.Contains(string(debugJSON), "sk-") {
		t.Fatalf("debug material contains secret material: %s", debugJSON)
	}
	gotReq := fake.lastRequest()
	if gotReq.Prompt.SessionID != "session-123" {
		t.Fatalf("session id = %q, want session-123", gotReq.Prompt.SessionID)
	}
	if gotReq.Target.Model != "explicit-model" {
		t.Fatalf("model = %q, want explicit-model", gotReq.Target.Model)
	}
	if gotReq.Target.BackendID != "test-backend" {
		t.Fatalf("backend id = %q, want test-backend", gotReq.Target.BackendID)
	}
	if len(gotReq.Prompt.Messages) != 1 ||
		!strings.Contains(gotReq.Prompt.Messages[0].Content, `{"source":true}`) ||
		!strings.Contains(gotReq.Prompt.Messages[0].Content, "value") {
		t.Fatalf("rendered messages = %#v, want transcript input and variable content", gotReq.Prompt.Messages)
	}
	if gotReq.StructuredOutput == nil {
		t.Fatalf("structured output = nil, want JSON schema")
	}
	manifests := client.LLMProfileManifests()
	if len(manifests) != 1 ||
		manifests[0].ID != "explicit-profile" ||
		manifests[0].Provider != "promptkit" ||
		manifests[0].Model != "explicit-model" ||
		manifests[0].BackendID != "test-backend" ||
		manifests[0].ReasoningEffort != "profile-reasoning" {
		t.Fatalf("profile manifests = %#v", manifests)
	}
}

func TestPromptKitClientAppendsSemanticCorrectionAfterRenderedPrompt(t *testing.T) {
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client := newTestPromptKitClient(t, fake)
	request := contracts.StructuredCompletionRequest{
		PromptID:  "adapter.direct-session",
		ProfileID: "explicit-profile",
		SessionID: "correction-session",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
		Vars: map[string]any{"custom": "value"},
	}

	var ordinary map[string]any
	if _, err := client.CompleteStructured(context.Background(), request, &ordinary); err != nil {
		t.Fatalf("ordinary CompleteStructured() error = %v", err)
	}
	ordinaryMessages := append([]promptkit.RenderedMessage(nil), fake.lastRequest().Prompt.Messages...)
	if len(ordinaryMessages) != 1 {
		t.Fatalf("ordinary rendered messages = %#v, want only the declared prompt message", ordinaryMessages)
	}

	correction, err := contracts.NewSemanticCorrection([]byte(`{"previous":"response"}`), "Return the corrected JSON object.")
	if err != nil {
		t.Fatalf("NewSemanticCorrection() error = %v", err)
	}
	request.Correction = correction
	var corrected map[string]any
	if _, err := client.CompleteStructured(context.Background(), request, &corrected); err != nil {
		t.Fatalf("corrected CompleteStructured() error = %v", err)
	}
	correctedMessages := fake.lastRequest().Prompt.Messages
	if len(correctedMessages) != len(ordinaryMessages)+2 {
		t.Fatalf("corrected message count = %d, want %d", len(correctedMessages), len(ordinaryMessages)+2)
	}
	if !reflect.DeepEqual(correctedMessages[:len(ordinaryMessages)], ordinaryMessages) {
		t.Fatalf("ordinary rendered prefix changed: got %#v, want %#v", correctedMessages[:len(ordinaryMessages)], ordinaryMessages)
	}
	if got, want := correctedMessages[len(ordinaryMessages)], (promptkit.RenderedMessage{Role: promptkit.RoleAssistant, Content: `{"previous":"response"}`}); got != want {
		t.Fatalf("assistant correction message = %#v, want %#v", got, want)
	}
	if got, want := correctedMessages[len(ordinaryMessages)+1], (promptkit.RenderedMessage{Role: promptkit.RoleUser, Content: "Return the corrected JSON object."}); got != want {
		t.Fatalf("user correction message = %#v, want %#v", got, want)
	}
}

func TestPromptKitClientRejectsInvalidCorrectionsBeforePromptPreparation(t *testing.T) {
	const sensitiveResponse = "assistant-response-must-not-appear-in-errors"
	for _, correction := range []*contracts.SemanticCorrection{
		{AssistantResponse: []byte(sensitiveResponse), UserGuidance: " \t"},
		{AssistantResponse: bytes.Repeat([]byte(sensitiveResponse), contracts.MaxAssistantResponseBytes/len(sensitiveResponse)+1), UserGuidance: "Use a smaller response."},
	} {
		fake := &fakePromptKitLLM{content: `{"ok":true}`}
		client := newTestPromptKitClient(t, fake)
		var out map[string]any
		_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{Correction: correction}, &out)
		if err == nil || !strings.Contains(err.Error(), "correction") {
			t.Fatalf("CompleteStructured() error = %v, want correction validation failure", err)
		}
		if strings.Contains(err.Error(), sensitiveResponse) {
			t.Fatalf("correction validation error leaked response content: %v", err)
		}
		if got := atomic.LoadInt32(&fake.calls); got != 0 {
			t.Fatalf("provider calls = %d, want no provider call after invalid correction", got)
		}
	}
}

func TestPromptKitClientUsesOnePreparedSnapshotForDebugAndGeneration(t *testing.T) {
	const initialPrompt = `id: snapshot.test
version: "v1"
default_profile: snapshot-profile
inputs:
  - name: transcript
    required: true
    content_type: application/json
messages:
  - role: user
    content: "Snapshot A: {{ input \"transcript\" }}"
output:
  format: json
  validation_mode: json_schema
  schema_path: adapter.schema.json
  repair_attempts: 0
`
	const updatedPrompt = `id: snapshot.test
version: "v1"
default_profile: snapshot-profile
inputs:
  - name: transcript
    required: true
    content_type: application/json
messages:
  - role: user
    content: "Snapshot B: {{ input \"transcript\" }}"
output:
  format: json
  validation_mode: json_schema
  schema_path: adapter.schema.json
  repair_attempts: 0
`
	source := &switchingPromptFS{files: fstest.MapFS{
		"snapshot.test.yaml": {Data: []byte(initialPrompt)},
	}}
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets: newTestPromptKitAssets(t),
		EngineOptions: []promptkit.Option{
			promptkit.WithPromptFS(source, "."),
			promptkit.WithBackend(promptkit.Backend{ID: "snapshot-backend", Endpoint: "http://promptkit.test/v1"}),
			promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
				ID:        "snapshot-profile",
				BackendID: "snapshot-backend",
				Model:     "snapshot-model",
			})),
			promptkit.WithLLMClient(fake),
		},
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
	}

	opened := source.holdNextPromptRead()
	defer source.resumePromptRead()
	type completion struct {
		response contracts.StructuredCompletionResponse
		err      error
	}
	completed := make(chan completion, 1)
	go func() {
		var out struct {
			OK bool `json:"ok"`
		}
		response, callErr := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
			PromptID: "snapshot.test",
			Inputs: contracts.LLMInputSet{
				"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
			},
		}, &out)
		completed <- completion{response: response, err: callErr}
	}()
	select {
	case <-opened:
	case <-time.After(time.Second):
		t.Fatal("PromptKit did not read the prompt source")
	}
	source.replacePrompt([]byte(updatedPrompt))
	source.resumePromptRead()

	result := <-completed
	if result.err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", result.err)
	}
	if result.response.Debug == nil || result.response.Debug.Prompt == nil || len(result.response.Debug.Prompt.Messages) != 1 {
		t.Fatalf("debug prompt = %#v, want one prepared message", result.response.Debug)
	}
	debugContent := result.response.Debug.Prompt.Messages[0].Content
	generatedContent := fake.lastRequest().Prompt.Messages[0].Content
	if debugContent != generatedContent {
		t.Fatalf("debug content = %q, generation content = %q, want one snapshot", debugContent, generatedContent)
	}
	if !strings.Contains(debugContent, "Snapshot A") || strings.Contains(debugContent, "Snapshot B") {
		t.Fatalf("snapshot content = %q, want the source read before it changed", debugContent)
	}
}

func TestPromptKitClientRetainsSessionPromptVariable(t *testing.T) {
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client := newTestPromptKitClient(t, fake)

	var out map[string]any
	_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		SessionID: " canonical-session ",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
		Vars: map[string]any{
			"custom":     "value",
			"session_id": "caller-session",
		},
	}, &out)
	if err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", err)
	}
	gotReq := fake.lastRequest()
	if gotReq.Prompt.SessionID != "canonical-session" {
		t.Fatalf("session id = %q, want canonical-session", gotReq.Prompt.SessionID)
	}
	if len(gotReq.Prompt.Messages) != 1 ||
		!strings.Contains(gotReq.Prompt.Messages[0].Content, "Session: canonical-session") ||
		strings.Contains(gotReq.Prompt.Messages[0].Content, "caller-session") {
		t.Fatalf("rendered messages = %#v, want canonical session compatibility variable", gotReq.Prompt.Messages)
	}
}

func TestPromptKitClientDoesNotInventDirectSession(t *testing.T) {
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client := newTestPromptKitClient(t, fake)

	var out map[string]any
	resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID: "adapter.direct-session",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
		Vars: map[string]any{"custom": "value"},
	}, &out)
	if err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", err)
	}
	if got := fake.lastRequest().Prompt.SessionID; got != "" {
		t.Fatalf("session id = %q, want empty", got)
	}
	if resp.Debug == nil || resp.Debug.Prompt == nil || resp.Debug.Prompt.SessionID != "" {
		t.Fatalf("debug prompt = %#v, want no effective session", resp.Debug)
	}
}

func TestPromptKitClientAppliesReasoningEffortOverride(t *testing.T) {
	tests := []struct {
		name              string
		override          func() *string
		mutateAfterCreate bool
		want              string
	}{
		{
			name: "inherit",
			want: "profile-reasoning",
		},
		{
			name:     "replace",
			override: func() *string { value := "focused"; return &value },
			want:     "focused",
		},
		{
			name:     "clear",
			override: func() *string { value := ""; return &value },
			want:     "",
		},
		{
			name:              "defensive copy",
			override:          func() *string { value := "original"; return &value },
			mutateAfterCreate: true,
			want:              "original",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fake := &fakePromptKitLLM{content: `{"ok":true}`}
			var override *string
			if tt.override != nil {
				override = tt.override()
			}
			client := newTestPromptKitClientWithReasoning(t, fake, override)
			if tt.mutateAfterCreate {
				*override = "mutated"
			}

			var out map[string]any
			_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
				PromptID: "adapter.direct-session",
				Inputs: contracts.LLMInputSet{
					"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
				},
				Vars: map[string]any{"custom": "value"},
			}, &out)
			if err != nil {
				t.Fatalf("CompleteStructured() error = %v, want nil", err)
			}
			if got := fake.lastRequest().Target.ReasoningEffort; got != tt.want {
				t.Fatalf("reasoning effort = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestNewPromptKitClientReportsAssetAndEngineConstructionFailures(t *testing.T) {
	t.Run("assets", func(t *testing.T) {
		registry := NewAssetRegistry()
		for _, root := range []string{"one", "two"} {
			if err := registry.RegisterPromptFS(fstest.MapFS{
				root + "/prompt.yaml": {Data: []byte("id: duplicate")},
			}, root); err != nil {
				t.Fatalf("RegisterPromptFS() error = %v", err)
			}
		}
		if _, err := NewPromptKitClient(PromptKitClientConfig{Assets: registry}); err == nil ||
			!strings.Contains(err.Error(), "duplicate asset path") {
			t.Fatalf("NewPromptKitClient() error = %v, want asset construction failure", err)
		}
	})

	t.Run("engine", func(t *testing.T) {
		if _, err := NewPromptKitClient(PromptKitClientConfig{
			Assets:        newTestPromptKitAssets(t),
			EngineOptions: []promptkit.Option{promptkit.WithProfileFile("")},
		}); err == nil || !strings.Contains(err.Error(), "create PromptKit engine") {
			t.Fatalf("NewPromptKitClient() error = %v, want engine construction failure", err)
		}
	})
}

func TestPromptKitClientCheckpointFingerprintTracksProfileSource(t *testing.T) {
	profilePath := filepath.Join(t.TempDir(), "profiles.yml")
	const credentialEnvironment = "PROMPTKIT_TEST_API_KEY"
	writeProfile := func(model string) {
		t.Helper()
		content := "id: checkpoint-profile\nendpoint: http://promptkit.test/v1\nmodel: " + model + "\napi_key_env: " + credentialEnvironment + "\n"
		if err := os.WriteFile(profilePath, []byte(content), 0o600); err != nil {
			t.Fatal(err)
		}
	}
	fingerprintFor := func() CheckpointFingerprint {
		t.Helper()
		client, err := NewPromptKitClient(PromptKitClientConfig{
			Assets:      newTestPromptKitAssets(t),
			ProfileFile: profilePath,
		})
		if err != nil {
			t.Fatal(err)
		}
		values, err := client.LLMCheckpointFingerprints()
		if err != nil {
			t.Fatal(err)
		}
		if len(values) != 1 || values[0].Name != promptKitProfileFingerprintName {
			t.Fatalf("checkpoint fingerprints = %#v, want one profile-source identity", values)
		}
		return values[0]
	}

	writeProfile("model-one")
	first := fingerprintFor()
	repeated := fingerprintFor()
	if first != repeated {
		t.Fatalf("profile-source fingerprint = %#v then %#v for unchanged source", first, repeated)
	}
	writeProfile("model-two")
	second := fingerprintFor()
	if first == second {
		t.Fatalf("profile-source fingerprint = %#v for both profile models", first)
	}
	if strings.TrimSpace(first.Value) == "" {
		t.Fatal("profile-source fingerprint is empty")
	}
	if strings.Contains(first.Value, profilePath) ||
		strings.Contains(first.Value, "model-one") ||
		strings.Contains(first.Value, credentialEnvironment) {
		t.Fatalf("profile-source fingerprint exposes source details: %#v", first)
	}

	client, err := NewPromptKitClient(PromptKitClientConfig{Assets: newTestPromptKitAssets(t)})
	if err != nil {
		t.Fatal(err)
	}
	copy, err := client.LLMCheckpointFingerprints()
	if err != nil {
		t.Fatal(err)
	}
	copy[0].Value = "mutated"
	fresh, err := client.LLMCheckpointFingerprints()
	if err != nil {
		t.Fatal(err)
	}
	if fresh[0].Value == "mutated" {
		t.Fatal("LLMCheckpointFingerprints exposed mutable backing storage")
	}
	secondClient, err := NewPromptKitClient(PromptKitClientConfig{Assets: newTestPromptKitAssets(t)})
	if err != nil {
		t.Fatal(err)
	}
	secondBuiltin, err := secondClient.LLMCheckpointFingerprints()
	if err != nil {
		t.Fatal(err)
	}
	if len(secondBuiltin) != 1 || fresh[0] != secondBuiltin[0] {
		t.Fatalf("built-in profile fingerprints = %#v and %#v, want deterministic identity", fresh, secondBuiltin)
	}
	if strings.TrimSpace(fresh[0].Value) == "" {
		t.Fatal("built-in profile fingerprint is empty")
	}

	t.Run("directory layout", func(t *testing.T) {
		profileDir := t.TempDir()
		firstPath := filepath.Join(profileDir, "first-profile.yaml")
		secondPath := filepath.Join(profileDir, "second-profile.yaml")
		content := []byte("id: directory-profile\nendpoint: http://promptkit.test/v1\nmodel: directory-model\n")
		if err := os.WriteFile(firstPath, content, 0o600); err != nil {
			t.Fatal(err)
		}
		first, err := promptKitProfileFingerprint(profileDir, "", "")
		if err != nil {
			t.Fatal(err)
		}
		if err := os.Rename(firstPath, secondPath); err != nil {
			t.Fatal(err)
		}
		second, err := promptKitProfileFingerprint(profileDir, "", "")
		if err != nil {
			t.Fatal(err)
		}
		if first == second {
			t.Fatalf("profile-source fingerprint = %#v after source filename changed", first)
		}
		if strings.Contains(first.Value, firstPath) || strings.Contains(second.Value, secondPath) {
			t.Fatalf("profile-source fingerprint exposes source path: %#v, %#v", first, second)
		}
	})

	t.Run("inherited parent", func(t *testing.T) {
		profileDir := t.TempDir()
		parentPath := filepath.Join(profileDir, "parent.yaml")
		leafPath := filepath.Join(profileDir, "leaf.yaml")
		if err := os.WriteFile(parentPath, []byte("id: parent\nendpoint: http://promptkit.test/v1\nmodel: parent-one\n"), 0o600); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(leafPath, []byte("id: leaf\nbase_profile: parent\nmodel: leaf-model\n"), 0o600); err != nil {
			t.Fatal(err)
		}
		first, err := promptKitProfileFingerprint(profileDir, "", "")
		if err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(parentPath, []byte("id: parent\nendpoint: http://promptkit.test/v1\nmodel: parent-two\n"), 0o600); err != nil {
			t.Fatal(err)
		}
		second, err := promptKitProfileFingerprint(profileDir, "", "")
		if err != nil {
			t.Fatal(err)
		}
		if first == second || strings.Contains(first.Value, "parent-one") || strings.Contains(first.Value, parentPath) || strings.Contains(first.Value, leafPath) {
			t.Fatalf("inherited profile fingerprint = %#v then %#v", first, second)
		}
	})
}

func TestPromptKitProfileFingerprintReadErrorsDoNotExposeSourcePaths(t *testing.T) {
	for _, source := range []struct {
		name        string
		profileDir  string
		profileFile string
	}{
		{
			name:        "file",
			profileFile: filepath.Join(t.TempDir(), "missing-profile.yaml"),
		},
		{
			name:       "directory",
			profileDir: filepath.Join(t.TempDir(), "missing-profiles"),
		},
	} {
		t.Run(source.name, func(t *testing.T) {
			_, err := promptKitProfileFingerprint(source.profileDir, source.profileFile, "")
			if err == nil {
				t.Fatal("promptKitProfileFingerprint() error = nil, want source read failure")
			}
			if (source.profileDir != "" && strings.Contains(err.Error(), source.profileDir)) ||
				(source.profileFile != "" && strings.Contains(err.Error(), source.profileFile)) {
				t.Fatalf("fingerprint error exposes profile source path: %q", err)
			}
		})
	}
}

func TestPromptKitClientUsesFallbackProfilesForExecutionAndInspection(t *testing.T) {
	assets := newTestPromptKitAssets(t)
	const profileID = "fallback-profile"
	if err := assets.RegisterFallbackProfileFS(fstest.MapFS{
		"profiles/fallback.yaml": {Data: []byte("id: " + profileID + "\nendpoint: http://promptkit.test/v1\nmodel: fallback-model\n")},
	}, "profiles"); err != nil {
		t.Fatalf("RegisterFallbackProfileFS() error = %v, want nil", err)
	}
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets:        assets,
		EngineOptions: []promptkit.Option{promptkit.WithLLMClient(fake)},
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
	}
	var out map[string]any
	response, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		ProfileID: profileID,
		SessionID: "fallback-profile-test",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out)
	if err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", err)
	}
	if response.ProfileID != profileID || response.Model != "fallback-model" {
		t.Fatalf("completion response = %#v, want fallback profile", response)
	}

	inspector, err := NewPromptKitProfileInspector(PromptKitProfileInspectorConfig{Assets: assets})
	if err != nil {
		t.Fatalf("NewPromptKitProfileInspector() error = %v, want nil", err)
	}
	inspection, err := inspector.InspectProfile(context.Background(), profileID)
	if err != nil {
		t.Fatalf("InspectProfile() error = %v, want nil", err)
	}
	if inspection.ProfileID != profileID || inspection.Model != "fallback-model" {
		t.Fatalf("profile inspection = %#v, want fallback profile", inspection)
	}
}

func TestPromptKitClientUsesInheritedFilesystemProfileForInspectionAndExecution(t *testing.T) {
	profileDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(profileDir, "base.yaml"), []byte("id: base\nendpoint: http://promptkit.test/v1\nmodel: base-model\nreasoning_effort: medium\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(profileDir, "leaf.yaml"), []byte("id: inherited-profile\nbase_profile: base\nmodel: leaf-model\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client, err := NewPromptKitClient(PromptKitClientConfig{Assets: newTestPromptKitAssets(t), ProfileDir: profileDir, EngineOptions: []promptkit.Option{promptkit.WithLLMClient(fake)}})
	if err != nil {
		t.Fatal(err)
	}
	inspector, err := NewPromptKitProfileInspector(PromptKitProfileInspectorConfig{Source: PromptKitProfileSourceConfig{ProfileDir: profileDir}})
	if err != nil {
		t.Fatal(err)
	}
	inspection, err := inspector.InspectProfile(context.Background(), "inherited-profile")
	if err != nil {
		t.Fatal(err)
	}
	if inspection.ProfileID != "inherited-profile" || inspection.Model != "leaf-model" {
		t.Fatalf("inspection = %#v, want resolved leaf target", inspection)
	}
	var out map[string]any
	response, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{PromptID: "adapter.test", ProfileID: "inherited-profile", SessionID: "inheritance-test", Inputs: contracts.LLMInputSet{"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", "")}}, &out)
	if err != nil {
		t.Fatal(err)
	}
	if response.ProfileID != inspection.ProfileID || response.Model != inspection.Model || fake.lastRequest().Target.ReasoningEffort != "medium" {
		t.Fatalf("response=%#v target=%#v inspection=%#v", response, fake.lastRequest().Target, inspection)
	}
}

func TestPromptKitProfileInspectorExposesRakestrawhomeBuiltIn(t *testing.T) {
	inspector, err := NewPromptKitProfileInspector(PromptKitProfileInspectorConfig{})
	if err != nil {
		t.Fatal(err)
	}
	inspection, err := inspector.InspectProfile(context.Background(), "rakestrawhome-gemma-4-31b")
	if err != nil {
		t.Fatal(err)
	}
	if inspection.BackendID != promptkit.BackendRakestrawHome || inspection.Model != "google/gemma-4-31b-it" {
		t.Fatalf("inspection = %#v", inspection)
	}
}

func TestPromptKitClientAllowsMissingOptionalFilesystemCredential(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got := r.Header.Get("Authorization"); got != "" {
			t.Fatalf("Authorization = %q, want omitted", got)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"{\"ok\":true}"}}]}`))
	}))
	defer server.Close()
	t.Setenv("NOTARIUS_OPTIONAL_PROFILE_KEY", "")
	profilePath := filepath.Join(t.TempDir(), "optional.yaml")
	if err := os.WriteFile(profilePath, []byte("id: optional-profile\nendpoint: "+server.URL+"/v1\nmodel: optional-model\napi_key_env: NOTARIUS_OPTIONAL_PROFILE_KEY\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	client, err := NewPromptKitClient(PromptKitClientConfig{Assets: newTestPromptKitAssets(t), ProfileFile: profilePath})
	if err != nil {
		t.Fatal(err)
	}
	var out map[string]any
	if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{PromptID: "adapter.test", ProfileID: "optional-profile", SessionID: "optional-credential-test", Inputs: contracts.LLMInputSet{"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", "")}}, &out); err != nil {
		t.Fatal(err)
	}
}

func TestPromptKitClientMapsHTTPGenerationErrorToApplicationBoundary(t *testing.T) {
	const credential = "selected-test-credential"
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusServiceUnavailable)
		_, _ = w.Write([]byte(`{"error":{"code":"temporary","type":"provider_error","message":"marker ` + credential + ` Bearer bearer-secret"}}`))
	}))
	defer server.Close()
	t.Setenv("NOTARIUS_GENERATION_TEST_KEY", credential)
	profilePath := filepath.Join(t.TempDir(), "profile.yaml")
	if err := os.WriteFile(profilePath, []byte("id: generation-profile\nendpoint: "+server.URL+"/v1\nmodel: test\napi_key_env: NOTARIUS_GENERATION_TEST_KEY\n"), 0o600); err != nil {
		t.Fatal(err)
	}
	client, err := NewPromptKitClient(PromptKitClientConfig{Assets: newTestPromptKitAssets(t), ProfileFile: profilePath})
	if err != nil {
		t.Fatal(err)
	}
	var out map[string]any
	response, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{PromptID: "adapter.test", ProfileID: "generation-profile", SessionID: "generation-error-test", Inputs: contracts.LLMInputSet{"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", "")}}, &out)
	if !errors.Is(err, contracts.ErrLLMGeneration) {
		t.Fatalf("error = %v", err)
	}
	var generation *contracts.LLMGenerationError
	if !errors.As(err, &generation) || generation.StatusCode() != http.StatusServiceUnavailable {
		t.Fatalf("generation error = %#v", generation)
	}
	if strings.Contains(err.Error(), credential) || strings.Contains(err.Error(), "marker") {
		t.Fatalf("ordinary error leaked provider detail: %v", err)
	}
	if response.Debug == nil || response.Debug.Response == nil || response.Debug.Response.ProviderError == nil {
		t.Fatalf("debug = %#v", response.Debug)
	}
	debug := response.Debug.Response.ProviderError
	if debug.StatusCode != http.StatusServiceUnavailable || debug.Code != "temporary" || strings.Contains(debug.Message, credential) || strings.Contains(debug.Message, "bearer-secret") || !strings.Contains(debug.Message, "marker") {
		t.Fatalf("debug provider error = %#v", debug)
	}
}

func TestPromptKitClientRejectsInvalidRepairAttemptOverride(t *testing.T) {
	for _, attempts := range []int{-1, 4} {
		t.Run("invalid", func(t *testing.T) {
			fake := &fakePromptKitLLM{content: `{"ok":true}`}
			client := newTestPromptKitClient(t, fake)
			var out map[string]any
			_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{PromptID: "adapter.test", StructuredOutputRepairAttempts: &attempts}, &out)
			if err == nil || fake.calls != 0 {
				t.Fatalf("error=%v calls=%d", err, fake.calls)
			}
		})
	}
}

func TestPromptKitClientCheckpointFingerprintTracksFallbackProfileAssets(t *testing.T) {
	fingerprintFor := func(content string) CheckpointFingerprint {
		t.Helper()
		assets := newTestPromptKitAssets(t)
		if err := assets.RegisterFallbackProfileFS(fstest.MapFS{
			"profiles/fallback.yaml": {Data: []byte(content)},
		}, "profiles"); err != nil {
			t.Fatal(err)
		}
		client, err := NewPromptKitClient(PromptKitClientConfig{Assets: assets})
		if err != nil {
			t.Fatal(err)
		}
		fingerprints, err := client.LLMCheckpointFingerprints()
		if err != nil {
			t.Fatal(err)
		}
		if len(fingerprints) != 1 || fingerprints[0].Name != promptKitProfileFingerprintName {
			t.Fatalf("checkpoint fingerprints = %#v, want profile source identity", fingerprints)
		}
		return fingerprints[0]
	}

	first := fingerprintFor("id: fallback\nendpoint: http://promptkit.test/v1\nmodel: model-one\n")
	second := fingerprintFor("id: fallback\nendpoint: http://promptkit.test/v1\nmodel: model-two\n")
	if first == second {
		t.Fatalf("checkpoint fingerprints = %#v and %#v, want fallback asset change", first, second)
	}
	if strings.Contains(first.Value, "model-one") || strings.Contains(first.Value, "fallback.yaml") {
		t.Fatalf("checkpoint fingerprint leaked fallback source details: %#v", first)
	}
}

func TestPromptKitClientCheckpointFingerprintTracksLocalBackendTarget(t *testing.T) {
	const (
		firstEndpoint  = "http://localhost:8000/v1"
		secondEndpoint = "https://inference.example.test/v1"
	)
	fingerprintsFor := func(endpoint string, concurrencyLimit int) []CheckpointFingerprint {
		t.Helper()
		client, err := NewPromptKitClient(PromptKitClientConfig{
			Assets: newTestPromptKitAssets(t),
			LocalBackend: &PromptKitLocalBackendConfig{
				Endpoint:         endpoint,
				ConcurrencyLimit: concurrencyLimit,
			},
		})
		if err != nil {
			t.Fatal(err)
		}
		values, err := client.LLMCheckpointFingerprints()
		if err != nil {
			t.Fatal(err)
		}
		return values
	}

	baseline := fingerprintsFor(firstEndpoint, 0)
	if len(baseline) != 2 ||
		baseline[0].Name != promptKitProfileFingerprintName ||
		baseline[1].Name != promptKitLocalBackendFingerprintName {
		t.Fatalf("checkpoint fingerprints = %#v, want profile source then local backend target", baseline)
	}
	endpointChanged := fingerprintsFor(secondEndpoint, 0)
	if baseline[0] != endpointChanged[0] || baseline[1] == endpointChanged[1] {
		t.Fatalf("endpoint fingerprints = %#v and %#v, want only local target to change", baseline, endpointChanged)
	}
	concurrencyChanged := fingerprintsFor(firstEndpoint, 4)
	if !reflect.DeepEqual(baseline, concurrencyChanged) {
		t.Fatalf("concurrency fingerprints = %#v, want %#v", concurrencyChanged, baseline)
	}
	for _, values := range [][]CheckpointFingerprint{baseline, endpointChanged} {
		for _, fingerprint := range values {
			if strings.Contains(fingerprint.Value, firstEndpoint) ||
				strings.Contains(fingerprint.Value, secondEndpoint) {
				t.Fatalf("checkpoint fingerprint exposes endpoint: %#v", fingerprint)
			}
		}
	}

	localBackend := &PromptKitLocalBackendConfig{
		Endpoint:         " " + firstEndpoint + " ",
		ConcurrencyLimit: 0,
	}
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets:       newTestPromptKitAssets(t),
		LocalBackend: localBackend,
	})
	if err != nil {
		t.Fatal(err)
	}
	localBackend.Endpoint = secondEndpoint
	copy, err := client.LLMCheckpointFingerprints()
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(copy, baseline) {
		t.Fatalf("fingerprints after input mutation = %#v, want retained target %#v", copy, baseline)
	}
	copy[0].Value = "mutated-profile"
	copy[1].Value = "mutated-target"
	fresh, err := client.LLMCheckpointFingerprints()
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(fresh, baseline) {
		t.Fatalf("fingerprints after returned-slice mutation = %#v, want %#v", fresh, baseline)
	}
}

func TestPromptKitClientUsesPromptDefaultProfileWhenRequestProfileEmpty(t *testing.T) {
	fake := &fakePromptKitLLM{content: `{"ok":true}`}
	client := newTestPromptKitClient(t, fake)

	var out map[string]any
	if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		SessionID: "session-123",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out); err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", err)
	}
	if got := fake.lastRequest().Target.Model; got != "default-model" {
		t.Fatalf("model = %q, want prompt default profile model", got)
	}
	manifests := client.LLMProfileManifests()
	if len(manifests) != 1 || manifests[0].BackendID != "" || manifests[0].ReasoningEffort != "profile-reasoning" {
		t.Fatalf("endpoint-only profile manifests = %#v, want omitted backend and effective reasoning", manifests)
	}
}

func TestPromptKitClientUsesConfiguredLocalBackend(t *testing.T) {
	var providerCalls atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		providerCalls.Add(1)
		if r.URL.Path != "/v1/chat/completions" {
			t.Errorf("provider path = %q, want /v1/chat/completions", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{
			"choices": [{"message": {"role": "assistant", "content": "{\"ok\":true}"}}],
			"usage": {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7}
		}`))
	}))
	defer server.Close()

	profilePath := filepath.Join(t.TempDir(), "profiles.yml")
	if err := os.WriteFile(profilePath, []byte(`id: local-profile
backend: local
model: local-model
`), 0o600); err != nil {
		t.Fatal(err)
	}
	localBackend := &PromptKitLocalBackendConfig{
		Endpoint:         " " + server.URL + "/v1 ",
		ConcurrencyLimit: 2,
	}
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets:       newTestPromptKitAssets(t),
		ProfileFile:  profilePath,
		LocalBackend: localBackend,
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
	}
	localBackend.Endpoint = "http://127.0.0.1:1/v1"

	request := contracts.StructuredCompletionRequest{
		PromptID:  "adapter.direct-session",
		ProfileID: "local-profile",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
		Vars: map[string]any{"custom": "value"},
	}
	var out map[string]any
	response, err := client.CompleteStructured(context.Background(), request, &out)
	if err != nil {
		t.Fatalf("CompleteStructured() error = %v, want nil", err)
	}
	if providerCalls.Load() != 1 {
		t.Fatalf("provider calls = %d, want 1", providerCalls.Load())
	}
	if response.ProfileID != "local-profile" || response.Model != "local-model" {
		t.Fatalf("response metadata = %#v", response)
	}
	if response.Debug == nil || response.Debug.Prompt == nil ||
		response.Debug.Prompt.SelectedBackendID != promptkit.BackendLocal {
		t.Fatalf("response debug prompt = %#v, want local backend", response.Debug)
	}
	manifests := client.LLMProfileManifests()
	if len(manifests) != 1 || manifests[0].BackendID != promptkit.BackendLocal {
		t.Fatalf("profile manifests = %#v, want local backend", manifests)
	}

	clientWithoutRegistration, err := NewPromptKitClient(PromptKitClientConfig{
		Assets:      newTestPromptKitAssets(t),
		ProfileFile: profilePath,
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() without local registration error = %v, want nil", err)
	}
	_, err = clientWithoutRegistration.CompleteStructured(context.Background(), request, &out)
	if err == nil ||
		!strings.Contains(err.Error(), "prepare PromptKit prompt") ||
		!strings.Contains(err.Error(), promptkit.BackendLocal) {
		t.Fatalf("CompleteStructured() without registration error = %v, want preparation failure with local backend context", err)
	}
	for _, sentinel := range []error{promptkit.ErrProfileLoad, promptkit.ErrInvalidRequest, promptkit.ErrPromptNotFound} {
		if errors.Is(err, sentinel) {
			t.Fatalf("preparation failure exposes PromptKit sentinel %v: %v", sentinel, err)
		}
	}
	if providerCalls.Load() != 1 {
		t.Fatalf("provider calls after missing-registration failure = %d, want 1", providerCalls.Load())
	}
}

func TestLLMProfileRecorderDistinguishesEffectiveTargets(t *testing.T) {
	recorder := NewLLMProfileRecorder()
	for _, profile := range []artifacts.LLMProfileManifest{
		{ID: "profile", Provider: "promptkit", Model: "model", BackendID: "backend-b", ReasoningEffort: "low"},
		{ID: " profile ", Provider: " promptkit ", Model: " model ", BackendID: " backend-a ", ReasoningEffort: " low "},
		{ID: "profile", Provider: "promptkit", Model: "model", BackendID: "backend-a", ReasoningEffort: "high"},
		{ID: "profile", Provider: "promptkit", Model: "model", BackendID: "backend-a", ReasoningEffort: "low"},
	} {
		recorder.Record(profile)
	}
	want := []artifacts.LLMProfileManifest{
		{ID: "profile", Provider: "promptkit", Model: "model", BackendID: "backend-a", ReasoningEffort: "high"},
		{ID: "profile", Provider: "promptkit", Model: "model", BackendID: "backend-a", ReasoningEffort: "low"},
		{ID: "profile", Provider: "promptkit", Model: "model", BackendID: "backend-b", ReasoningEffort: "low"},
	}
	if got := recorder.Manifests(); !reflect.DeepEqual(got, want) {
		t.Fatalf("profile manifests = %#v, want %#v", got, want)
	}
}

func TestPromptKitClientValidationFailureReturnsError(t *testing.T) {
	attempts := 1
	fake := &fakePromptKitLLM{responses: []promptkit.GenerateResponse{{Content: `{"bad":true}`}, {Content: `{"bad":true}`}}}
	client := newTestPromptKitClient(t, fake)

	var out map[string]any
	resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:                       "adapter.test",
		SessionID:                      "session-123",
		StructuredOutputRepairAttempts: &attempts,
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out)
	if err == nil || !errors.Is(err, contracts.ErrInvalidStructuredOutput) || !strings.Contains(err.Error(), "validation failed") {
		t.Fatalf("CompleteStructured() error = %v, want validation failure", err)
	}
	if got := atomic.LoadInt32(&fake.calls); got != 2 {
		t.Fatalf("provider calls = %d, want exhausted repair budget", got)
	}
	if got := string(resp.Content); got != `{"bad":true}` {
		t.Fatalf("response content = %q, want raw failed output", got)
	}
	if resp.Debug == nil || resp.Debug.Response == nil || resp.Debug.Response.Content != `{"bad":true}` {
		t.Fatalf("debug response = %#v, want raw failed output", resp.Debug)
	}
	if resp.Debug.Prompt == nil || len(resp.Debug.Prompt.Messages) == 0 {
		t.Fatalf("debug prompt = %#v, want prepared prompt material", resp.Debug.Prompt)
	}
}

func TestPromptKitClientRepairsStructuredOutputAndReportsCumulativeUsage(t *testing.T) {
	attempts := 1
	fake := &fakePromptKitLLM{responses: []promptkit.GenerateResponse{
		{Content: `{"bad":true}`, Usage: promptkit.TokenUsage{PromptTokens: 3, CompletionTokens: 5, TotalTokens: 8}},
		{Content: `{"ok":true}`, Usage: promptkit.TokenUsage{PromptTokens: 7, CompletionTokens: 11, TotalTokens: 18}},
	}}
	client := newTestPromptKitClient(t, fake)
	correction, err := contracts.NewSemanticCorrection([]byte(`{"bad":true}`), "Return the required ok field.")
	if err != nil {
		t.Fatalf("NewSemanticCorrection() error = %v", err)
	}

	var out struct {
		OK bool `json:"ok"`
	}
	response, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:                       "adapter.test",
		StructuredOutputRepairAttempts: &attempts,
		SessionID:                      "repair-test",
		Correction:                     correction,
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out)
	if err != nil || !out.OK {
		t.Fatalf("CompleteStructured() = (%#v, %v), want repaired success", response, err)
	}
	if got := atomic.LoadInt32(&fake.calls); got != 2 {
		t.Fatalf("provider calls = %d, want initial generation and one repair", got)
	}
	requests := fake.requestsSnapshot()
	if len(requests) != 2 || len(requests[0].Prompt.Messages) < 3 {
		t.Fatalf("repair requests = %#v, want correction messages on the initial prepared request", requests)
	}
	messages := requests[0].Prompt.Messages
	if got, want := messages[len(messages)-2], (promptkit.RenderedMessage{Role: promptkit.RoleAssistant, Content: `{"bad":true}`}); got != want {
		t.Fatalf("repair assistant correction = %#v, want %#v", got, want)
	}
	if got, want := messages[len(messages)-1], (promptkit.RenderedMessage{Role: promptkit.RoleUser, Content: "Return the required ok field."}); got != want {
		t.Fatalf("repair user correction = %#v, want %#v", got, want)
	}
	if response.RepairAttempts != 1 || response.PromptTokens != 10 || response.CompletionTokens != 16 || response.TotalTokens != 26 {
		t.Fatalf("response repair and usage = %#v, want one repair and PromptKit cumulative usage", response)
	}
	if response.Debug == nil || response.Debug.Prompt == nil || response.Debug.Response == nil {
		t.Fatalf("debug = %#v, want prompt and response details", response.Debug)
	}
	if response.Debug.Prompt.OutputContract["repair_attempts"] != float64(1) ||
		response.Debug.Response.Validation["repair_attempts"] != float64(1) ||
		response.Debug.Response.Content != `{"ok":true}` ||
		response.Debug.Response.Usage.TotalTokens != 26 {
		t.Fatalf("debug repair result = %#v, want configured contract and repaired response", response.Debug)
	}
}

func TestPromptKitClientDecodeFailureReturnsRawResponse(t *testing.T) {
	client := newTestPromptKitClient(t, &fakePromptKitLLM{content: `{"ok":true}`})

	var out []any
	resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		SessionID: "session-123",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out)
	if err == nil || !errors.Is(err, contracts.ErrInvalidStructuredOutput) || !strings.Contains(err.Error(), "decode PromptKit structured output") {
		t.Fatalf("CompleteStructured() error = %v, want decode failure", err)
	}
	if got := string(resp.Content); got != `{"ok":true}` {
		t.Fatalf("response content = %q, want raw decode-failed output", got)
	}
	if resp.Debug == nil || resp.Debug.Response == nil || resp.Debug.Response.Content != `{"ok":true}` {
		t.Fatalf("debug response = %#v, want raw decode-failed output", resp.Debug)
	}
}

func TestPromptKitClientProviderFailureIncludesContextAndRedactsBearerToken(t *testing.T) {
	providerErr := &credentialBearingProviderError{}
	client := newTestPromptKitClient(t, &fakePromptKitLLM{err: providerErr})

	var out map[string]any
	resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		SessionID: "session-123",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out)
	if err == nil {
		t.Fatalf("CompleteStructured() error = nil, want provider error")
	}
	if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
		t.Fatalf("provider error = %v, must not be classified as invalid structured output", err)
	}
	if !strings.Contains(err.Error(), `run PromptKit prompt "adapter.test"`) {
		t.Fatalf("error = %q, want operation context", err.Error())
	}
	if strings.Contains(err.Error(), "secret-token") || !strings.Contains(err.Error(), "Bearer [REDACTED]") {
		t.Fatalf("error = %q, want redacted bearer token", err.Error())
	}
	for current := err; current != nil; current = errors.Unwrap(current) {
		if strings.Contains(current.Error(), "secret-token") {
			t.Fatalf("error chain exposes credential: %v", err)
		}
	}
	if !errors.Is(err, contracts.ErrLLMGeneration) {
		t.Fatalf("provider failure = %v, want application generation classification", err)
	}
	var recoveredProviderErr *credentialBearingProviderError
	if errors.As(err, &recoveredProviderErr) {
		t.Fatalf("provider error escaped the adapter: %v", err)
	}
	if errors.Is(err, promptkit.ErrLLMGenerate) {
		t.Fatalf("provider failure exposes PromptKit generation sentinel: %v", err)
	}
	if resp.Debug == nil || resp.Debug.Prompt == nil || resp.Debug.Response != nil {
		t.Fatalf("debug material = %#v, want prepared prompt without provider details", resp.Debug)
	}
}

func TestPromptKitClientTranslatesBackendCapacityExhaustion(t *testing.T) {
	queueCapacity := 0
	fake := &fakePromptKitLLM{
		err:   errors.New("provider failed with Bearer secret-token"),
		block: make(chan struct{}),
	}
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets: newTestPromptKitAssets(t),
		EngineOptions: []promptkit.Option{
			promptkit.WithBackend(promptkit.Backend{
				ID:               "limited-backend",
				Endpoint:         "http://127.0.0.1:1/v1",
				ConcurrencyLimit: 1,
				QueueCapacity:    &queueCapacity,
			}),
			promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
				ID:        "limited-profile",
				BackendID: "limited-backend",
				Model:     "limited-model",
			})),
			promptkit.WithLLMClient(fake),
		},
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
	}
	defer func() {
		select {
		case <-fake.block:
		default:
			close(fake.block)
		}
	}()

	request := contracts.StructuredCompletionRequest{
		PromptID:  "adapter.direct-session",
		ProfileID: "limited-profile",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
		Vars: map[string]any{"custom": "value"},
	}
	firstResult := make(chan error, 1)
	go func() {
		var out map[string]any
		_, callErr := client.CompleteStructured(context.Background(), request, &out)
		firstResult <- callErr
	}()
	waitForAtomicAtLeast(t, &fake.calls, 1)

	var out map[string]any
	response, capacityErr := client.CompleteStructured(context.Background(), request, &out)
	if len(response.Content) != 0 {
		t.Fatalf("capacity response = %#v, want empty", response)
	}
	if !errors.Is(capacityErr, contracts.ErrLLMCapacityExceeded) {
		t.Fatalf("capacity error = %v, want ErrLLMCapacityExceeded", capacityErr)
	}
	if errors.Is(capacityErr, contracts.ErrInvalidStructuredOutput) {
		t.Fatalf("capacity error = %v, must not be invalid structured output", capacityErr)
	}
	if errors.Is(capacityErr, promptkit.ErrCapacityExceeded) {
		t.Fatalf("capacity error exposes PromptKit sentinel: %v", capacityErr)
	}
	if !strings.Contains(capacityErr.Error(), `run PromptKit prompt "adapter.direct-session"`) ||
		!strings.Contains(capacityErr.Error(), `backend "limited-backend"`) ||
		!strings.Contains(capacityErr.Error(), "backend capacity exceeded") {
		t.Fatalf("capacity error = %q, want prompt, backend, and upstream diagnostic context", capacityErr)
	}
	var upstreamCapacityErr *promptkit.CapacityError
	if errors.As(capacityErr, &upstreamCapacityErr) {
		t.Fatalf("capacity error exposes PromptKit capacity type: %v", capacityErr)
	}
	if calls := atomic.LoadInt32(&fake.calls); calls != 1 {
		t.Fatalf("provider calls after capacity rejection = %d, want 1", calls)
	}

	close(fake.block)
	firstErr := <-firstResult
	if firstErr == nil || strings.Contains(firstErr.Error(), "secret-token") ||
		!strings.Contains(firstErr.Error(), "Bearer [REDACTED]") {
		t.Fatalf("admitted provider error = %v, want redacted diagnostic", firstErr)
	}
	if calls := atomic.LoadInt32(&fake.calls); calls != 1 {
		t.Fatalf("provider calls after release = %d, want no adapter retry", calls)
	}
}

func TestPromptKitClientContextCancellationIsRespected(t *testing.T) {
	attempts := 1
	for _, test := range []struct {
		name     string
		attempts *int
	}{
		{name: "during preparation"},
		{name: "during override inspection", attempts: &attempts},
	} {
		t.Run(test.name, func(t *testing.T) {
			ctx, cancel := context.WithCancel(context.Background())
			cancel()
			client := newTestPromptKitClient(t, &fakePromptKitLLM{content: `{"ok":true}`})

			var out map[string]any
			_, err := client.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
				PromptID:                       "adapter.test",
				StructuredOutputRepairAttempts: test.attempts,
				Inputs: contracts.LLMInputSet{
					"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
				},
			}, &out)
			if !errors.Is(err, context.Canceled) || errors.Is(err, contracts.ErrInvalidStructuredOutput) {
				t.Fatalf("CompleteStructured() error = %v, want context canceled", err)
			}
		})
	}
}

func TestPromptKitClientForwardsConfiguredTransportTimeout(t *testing.T) {
	var remaining time.Duration
	transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
		deadline, ok := req.Context().Deadline()
		if !ok {
			t.Fatal("outbound request context has no deadline")
		}
		remaining = time.Until(deadline)
		return &http.Response{
			StatusCode: http.StatusOK,
			Status:     "200 OK",
			Header:     make(http.Header),
			Body: io.NopCloser(strings.NewReader(
				`{"choices":[{"message":{"content":"{\"ok\":true}"}}]}`,
			)),
			Request: req,
		}, nil
	})
	const configuredTimeout = 2 * time.Second
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets:     newTestPromptKitAssets(t),
		Timeout:    configuredTimeout,
		HTTPClient: &http.Client{Transport: transport},
		EngineOptions: []promptkit.Option{
			promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
				ID:       "default-profile",
				Endpoint: "http://promptkit.test/v1",
				Model:    "default-model",
			})),
		},
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() error = %v", err)
	}
	var out map[string]any
	if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		SessionID: "session-123",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out); err != nil {
		t.Fatalf("CompleteStructured() error = %v", err)
	}
	if remaining < configuredTimeout-time.Second || remaining > configuredTimeout {
		t.Fatalf("transport deadline remaining = %v, want near %v", remaining, configuredTimeout)
	}
}

func TestPromptKitClientClassifiesEmptyStructuredCompletion(t *testing.T) {
	client := newTestPromptKitClient(t, &fakePromptKitLLM{allowEmpty: true})

	var out map[string]any
	_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
		PromptID:  "adapter.test",
		SessionID: "session-123",
		Inputs: contracts.LLMInputSet{
			"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
		},
	}, &out)
	if !errors.Is(err, contracts.ErrInvalidStructuredOutput) {
		t.Fatalf("CompleteStructured() error = %v, want invalid structured output", err)
	}
}

func TestScheduledPromptKitClientBoundsConcurrentCalls(t *testing.T) {
	fake := &fakePromptKitLLM{
		content: `{"ok":true}`,
		block:   make(chan struct{}),
	}
	client := newTestPromptKitClient(t, fake)
	scheduler, err := NewScheduler(1)
	if err != nil {
		t.Fatalf("NewScheduler() error = %v, want nil", err)
	}
	scheduled := NewScheduledClient(client, scheduler)

	var wg sync.WaitGroup
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			var out map[string]any
			_, callErr := scheduled.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
				PromptID:  "adapter.test",
				SessionID: "session-123",
				Inputs: contracts.LLMInputSet{
					"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
				},
			}, &out)
			if callErr != nil {
				t.Errorf("CompleteStructured() error = %v, want nil", callErr)
			}
		}()
	}
	waitForAtomicAtLeast(t, &fake.calls, 1)
	time.Sleep(20 * time.Millisecond)
	if got := atomic.LoadInt32(&fake.maxInFlight); got > 1 {
		t.Fatalf("max in-flight calls = %d, want <= 1", got)
	}
	close(fake.block)
	wg.Wait()
}

func TestScheduledPromptKitClientHoldsPermitAcrossStructuredOutputRepair(t *testing.T) {
	attempts := 1
	fake := &fakePromptKitLLM{
		responses: []promptkit.GenerateResponse{
			{Content: `{"bad":true}`},
			{Content: `{"ok":true}`},
			{Content: `{"ok":true}`},
		},
		block: make(chan struct{}),
	}
	scheduler, err := NewScheduler(1)
	if err != nil {
		t.Fatalf("NewScheduler() error = %v", err)
	}
	scheduled := NewScheduledClient(newTestPromptKitClient(t, fake), scheduler)

	firstDone := make(chan error, 1)
	go func() {
		var out map[string]any
		_, callErr := scheduled.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
			PromptID:                       "adapter.test",
			SessionID:                      "repair-session",
			StructuredOutputRepairAttempts: &attempts,
			Inputs: contracts.LLMInputSet{
				"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
			},
		}, &out)
		firstDone <- callErr
	}()
	waitForAtomicAtLeast(t, &fake.calls, 1)

	secondDone := make(chan error, 1)
	go func() {
		var out map[string]any
		_, callErr := scheduled.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
			PromptID:  "adapter.test",
			SessionID: "queued-session",
			Inputs: contracts.LLMInputSet{
				"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
			},
		}, &out)
		secondDone <- callErr
	}()
	close(fake.block)
	if err := <-firstDone; err != nil {
		t.Fatalf("repaired completion error = %v", err)
	}
	if err := <-secondDone; err != nil {
		t.Fatalf("queued completion error = %v", err)
	}

	requests := fake.requestsSnapshot()
	if len(requests) != 3 || requests[1].Prompt.SessionID != "repair-session" || requests[2].Prompt.SessionID != "queued-session" {
		t.Fatalf("generation order = %#v, want repair before queued completion", requests)
	}
	if got := atomic.LoadInt32(&fake.maxInFlight); got > 1 {
		t.Fatalf("max in-flight calls = %d, want one scheduled logical completion", got)
	}
}

func TestPromptKitClientValidatesRequest(t *testing.T) {
	client := newTestPromptKitClient(t, &fakePromptKitLLM{content: `{"ok":true}`})
	var out map[string]any
	if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{}, &out); err == nil || !strings.Contains(err.Error(), "prompt_id") {
		t.Fatalf("missing prompt id error = %v, want prompt_id validation", err)
	}
	if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{PromptID: "adapter.test"}, nil); err == nil || !strings.Contains(err.Error(), "non-nil pointer") {
		t.Fatalf("nil output error = %v, want output validation", err)
	}
}

func newTestPromptKitClient(t *testing.T, fake *fakePromptKitLLM) *PromptKitClient {
	return newTestPromptKitClientWithReasoning(t, fake, nil)
}

func newTestPromptKitClientWithReasoning(t *testing.T, fake *fakePromptKitLLM, reasoningEffort *string) *PromptKitClient {
	t.Helper()
	registry := newTestPromptKitAssets(t)
	client, err := NewPromptKitClient(PromptKitClientConfig{
		Assets:          registry,
		ReasoningEffort: reasoningEffort,
		EngineOptions: []promptkit.Option{
			promptkit.WithBackend(promptkit.Backend{
				ID:       "test-backend",
				Endpoint: "http://127.0.0.1:1/v1",
			}),
			promptkit.WithProfiles(
				promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
					ID:              "default-profile",
					Endpoint:        "http://127.0.0.1:1/v1",
					Model:           "default-model",
					ReasoningEffort: "profile-reasoning",
				}),
				promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
					ID:              "explicit-profile",
					BackendID:       "test-backend",
					Model:           "explicit-model",
					ReasoningEffort: "profile-reasoning",
				}),
			),
			promptkit.WithLLMClient(fake),
		},
	})
	if err != nil {
		t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
	}
	return client
}

func newTestPromptKitAssets(t *testing.T) *AssetRegistry {
	t.Helper()
	registry := NewAssetRegistry()
	if err := registry.RegisterPromptFS(fstest.MapFS{
		"adapter.test.yaml": {Data: []byte(`id: adapter.test
version: "v1"
default_profile: default-profile
session_id: "{{ .session_id }}"
inputs:
  - name: transcript
    required: true
    content_type: application/json
messages:
  - role: user
    content: "Transcript: {{ input \"transcript\" }} Custom: {{ index . \"custom\" }} Session: {{ .session_id }}"
output:
  format: json
  validation_mode: json_schema
  schema_path: adapter.schema.json
  repair_attempts: 0
`)},
		"adapter.direct-session.yaml": {Data: []byte(`id: adapter.direct-session
version: "v1"
default_profile: default-profile
inputs:
  - name: transcript
    required: true
    content_type: application/json
messages:
  - role: user
    content: "Transcript: {{ input \"transcript\" }} Custom: {{ index . \"custom\" }}"
output:
  format: json
  validation_mode: json_schema
  schema_path: adapter.schema.json
  repair_attempts: 0
`)},
	}, "."); err != nil {
		t.Fatalf("RegisterPromptFS() error = %v", err)
	}
	if err := registry.RegisterSchemaFS(fstest.MapFS{
		"adapter.schema.json": {Data: []byte(`{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}}`)},
	}, "."); err != nil {
		t.Fatalf("RegisterSchemaFS() error = %v", err)
	}
	return registry
}

type switchingPromptFS struct {
	mu            sync.Mutex
	files         fstest.MapFS
	pauseNextOpen bool
	opened        chan struct{}
	resume        chan struct{}
}

func (f *switchingPromptFS) Open(name string) (fs.File, error) {
	f.mu.Lock()
	file, err := f.files.Open(name)
	pause := f.pauseNextOpen && name == "snapshot.test.yaml"
	resume := f.resume
	if pause {
		f.pauseNextOpen = false
		close(f.opened)
	}
	f.mu.Unlock()
	if pause {
		<-resume
	}
	return file, err
}

func (f *switchingPromptFS) ReadDir(name string) ([]fs.DirEntry, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.files.ReadDir(name)
}

func (f *switchingPromptFS) holdNextPromptRead() <-chan struct{} {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.pauseNextOpen = true
	f.opened = make(chan struct{})
	f.resume = make(chan struct{})
	return f.opened
}

func (f *switchingPromptFS) replacePrompt(content []byte) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.files["snapshot.test.yaml"] = &fstest.MapFile{Data: append([]byte(nil), content...)}
}

func (f *switchingPromptFS) resumePromptRead() {
	f.mu.Lock()
	resume := f.resume
	f.resume = nil
	f.mu.Unlock()
	if resume != nil {
		close(resume)
	}
}

type fakePromptKitLLM struct {
	content     string
	responses   []promptkit.GenerateResponse
	allowEmpty  bool
	err         error
	block       chan struct{}
	mu          sync.Mutex
	last        promptkit.GenerateRequest
	requests    []promptkit.GenerateRequest
	calls       int32
	inFlight    int32
	maxInFlight int32
}

type credentialBearingProviderError struct{}

func (*credentialBearingProviderError) Error() string {
	return "provider failed with Bearer secret-token"
}

func (f *fakePromptKitLLM) Generate(ctx context.Context, req promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
	f.mu.Lock()
	f.last = req
	f.requests = append(f.requests, req)
	f.mu.Unlock()
	call := atomic.AddInt32(&f.calls, 1)
	current := atomic.AddInt32(&f.inFlight, 1)
	for {
		seen := atomic.LoadInt32(&f.maxInFlight)
		if current <= seen || atomic.CompareAndSwapInt32(&f.maxInFlight, seen, current) {
			break
		}
	}
	defer atomic.AddInt32(&f.inFlight, -1)
	if f.block != nil {
		select {
		case <-f.block:
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}
	if f.err != nil {
		return nil, f.err
	}
	if len(f.responses) > 0 {
		index := int(call - 1)
		if index >= len(f.responses) {
			return nil, fmt.Errorf("unexpected provider call %d", call)
		}
		response := f.responses[index]
		return &response, nil
	}
	content := f.content
	if content == "" && !f.allowEmpty {
		content = `{"ok":true}`
	}
	if !f.allowEmpty && !json.Valid([]byte(content)) {
		return nil, errors.New("test fake must return JSON content")
	}
	return &promptkit.GenerateResponse{
		Content: content,
		Usage: promptkit.TokenUsage{
			PromptTokens:     11,
			CompletionTokens: 7,
			TotalTokens:      18,
			CachedTokens:     5,
			CacheWriteTokens: 3,
		},
	}, nil
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

func (f *fakePromptKitLLM) lastRequest() promptkit.GenerateRequest {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.last
}

func (f *fakePromptKitLLM) requestsSnapshot() []promptkit.GenerateRequest {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]promptkit.GenerateRequest(nil), f.requests...)
}
