package openai

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

type fakeRunService struct {
	req        edgeservice.SubmitRunRequest
	ollamaReq  edgeservice.OllamaAPIRequest
	ollamaResp edgeservice.OllamaAPIView
	events     chan *iop.RunEvent
}

func (s *fakeRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) {
	s.req = req
	return &edgeservice.RunHandle{
		RunDispatch: edgeservice.RunDispatch{
			RunID:      "run-test",
			Target:     req.Target,
			TimeoutSec: 5,
		},
		RunStream: edgeservice.RunStream{
			Events:     s.events,
			NodeEvents: make(chan *iop.EdgeNodeEvent),
		},
	}, nil
}

func (s *fakeRunService) OllamaAPI(_ context.Context, req edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error) {
	s.ollamaReq = req
	if s.ollamaResp.StatusCode == 0 {
		s.ollamaResp.StatusCode = http.StatusOK
	}
	return s.ollamaResp, nil
}

func TestChatCompletionsDispatchesConfiguredOllamaTarget(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: " world"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}

	srv := NewServer(config.EdgeOpenAIConf{
		Adapter:    "ollama",
		Target:     "llama-fixed",
		SessionID:  "cline",
		TimeoutSec: 15,
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"system","content":"brief"},{"role":"user","content":"say hello"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Adapter != "ollama" || fake.req.Target != "llama-fixed" {
		t.Fatalf("dispatch target mismatch: %+v", fake.req)
	}
	if fake.req.SessionID != "cline" || fake.req.TimeoutSec != 15 {
		t.Fatalf("execution config mismatch: %+v", fake.req)
	}
	if !strings.Contains(fake.req.Prompt, "system: brief") || !strings.Contains(fake.req.Prompt, "user: say hello") {
		t.Fatalf("prompt did not include messages: %q", fake.req.Prompt)
	}

	var resp chatCompletionResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp.Choices[0].Message.Content != "hello world" {
		t.Fatalf("content: got %q", resp.Choices[0].Message.Content)
	}
	if resp.Usage == nil || resp.Usage.TotalTokens != 4 {
		t.Fatalf("usage: %+v", resp.Usage)
	}
}

func TestChatCompletionsUsesRequestModelWhenNoConfiguredTarget(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"from-request",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "from-request" {
		t.Fatalf("target: got %q", fake.req.Target)
	}
}

func TestChatCompletionsPassesOllamaOptions(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"from-request",
		"messages":[{"role":"user","content":"hi"}],
		"options":{"temperature":0.2,"top_p":0.9,"num_predict":32,"stop":["END"]},
		"keep_alive":"10m",
		"think":false,
		"format":"json",
		"tools":[{"type":"function","function":{"name":"lookup"}}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	options, ok := fake.req.Input["options"].(map[string]any)
	if !ok {
		t.Fatalf("options not passed: %+v", fake.req.Input)
	}
	if options["temperature"].(float64) != 0.2 || options["top_p"].(float64) != 0.9 || options["num_predict"].(float64) != 32 {
		t.Fatalf("unexpected options: %+v", options)
	}
	if fake.req.Input["keep_alive"] != "10m" || fake.req.Input["think"] != false || fake.req.Input["format"] != "json" {
		t.Fatalf("top-level ollama fields not passed: %+v", fake.req.Input)
	}
	if tools, ok := fake.req.Input["tools"].([]any); !ok || len(tools) != 1 {
		t.Fatalf("tools not passed: %+v", fake.req.Input["tools"])
	}
}

func TestChatCompletionsStreamsSSE(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"stream-model",
		"stream":true,
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
		t.Fatalf("unexpected SSE body:\n%s", body)
	}
}

func TestChatCompletionsStreamingReportsClosedRunStream(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	close(fake.events)

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"stream-model",
		"stream":true,
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, `"message":"run stream closed"`) || !strings.Contains(body, "data: [DONE]") {
		t.Fatalf("unexpected SSE error body:\n%s", body)
	}
}

func TestChatCompletionsReturnsReasoningContentSeparately(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "more"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}

	srv := NewServer(config.EdgeOpenAIConf{
		Adapter: "ollama",
		Target:  "llama-fixed",
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}

	var resp chatCompletionResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp.Choices[0].Message.Content != "answer" {
		t.Fatalf("content: got %q, expected \"answer\"", resp.Choices[0].Message.Content)
	}
	if resp.Choices[0].Message.ReasoningContent != "thinking more" {
		t.Fatalf("reasoning_content: got %q, expected \"thinking more\"", resp.Choices[0].Message.ReasoningContent)
	}
}

func TestChatCompletionsStreamsReasoningSSE(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"stream-model",
		"stream":true,
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, `"reasoning_content":"think"`) || !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
		t.Fatalf("unexpected SSE body:\n%s", body)
	}
}

func TestChatCompletionsStrictOutputNormalizesXMLStyleAgentResponse(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "---\n<thinking>hidden</thinking>\n\n<attempt_completion>\n<result>ok</result>\n</attempt completion>\n```\ntrailing noise"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"agent-model",
		"messages":[{"role":"user","content":"finish"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Input["think"] != false {
		t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
	}
	var resp chatCompletionResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	want := "<attempt_completion>\n<result>ok</result>\n</attempt_completion>"
	if resp.Choices[0].Message.Content != want {
		t.Fatalf("content:\ngot  %q\nwant %q", resp.Choices[0].Message.Content, want)
	}
	if resp.Choices[0].Message.ReasoningContent != "" {
		t.Fatalf("strict output should drop reasoning content: %q", resp.Choices[0].Message.ReasoningContent)
	}
}

func TestChatCompletionsStrictOutputWrapsPlainTextWhenPromptDeclaresCompletionTool(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"agent-model",
		"messages":[
			{"role":"system","content":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>"},
			{"role":"user","content":"finish"}
		]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") || !strings.Contains(fake.req.Prompt, "actual user-facing response") {
		t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
	}
	var resp chatCompletionResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
	if resp.Choices[0].Message.Content != want {
		t.Fatalf("content:\ngot  %q\nwant %q", resp.Choices[0].Message.Content, want)
	}
}

func TestChatCompletionsStrictOutputBuffersStreamingResponse(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "---\n<think>noise</think><read_file><path>a.go</path></read file>"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"agent-model",
		"stream":true,
		"messages":[{"role":"user","content":"read"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if strings.Contains(body, "reasoning_content") || strings.Contains(body, "<think>") || strings.Contains(body, "</read file>") {
		t.Fatalf("strict stream leaked unstable content:\n%s", body)
	}
	if !strings.Contains(body, "read_file") || !strings.Contains(body, "a.go") || !strings.Contains(body, "data: [DONE]") {
		t.Fatalf("unexpected strict SSE body:\n%s", body)
	}
}

func TestChatCompletionsStrictOutputStreamsLiveContent(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion>\n"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "<result>ok</result>\n</attempt_completion>"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"agent-model",
		"stream":true,
		"messages":[{"role":"user","content":"finish"}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if strings.Contains(body, "reasoning_content") || strings.Contains(body, "hidden") {
		t.Fatalf("strict stream leaked reasoning content:\n%s", body)
	}
	if strings.Count(body, `"content":`) != 2 || !strings.Contains(body, "attempt_completion") || !strings.Contains(body, "data: [DONE]") {
		t.Fatalf("strict stream should pass live content deltas:\n%s", body)
	}
}

func TestModelsUsesConfiguredModelsOrTarget(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Target: "fallback-model"}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()
	srv.handleModels(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d", w.Code)
	}
	if !strings.Contains(w.Body.String(), "fallback-model") {
		t.Fatalf("expected target model, got %s", w.Body.String())
	}

	srv = NewServer(config.EdgeOpenAIConf{Models: []string{"a", "b"}}, &fakeRunService{}, nil)
	w = httptest.NewRecorder()
	srv.handleModels(w, req)
	if !strings.Contains(w.Body.String(), `"id":"a"`) || !strings.Contains(w.Body.String(), `"id":"b"`) {
		t.Fatalf("expected configured models, got %s", w.Body.String())
	}
}

func TestOllamaAPIPassthrough(t *testing.T) {
	fake := &fakeRunService{
		ollamaResp: edgeservice.OllamaAPIView{
			StatusCode:  http.StatusAccepted,
			ContentType: "application/json",
			Body:        `{"ok":true}`,
		},
	}
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", TimeoutSec: 15}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(`{"model":"gemma4:26b"}`))
	w := httptest.NewRecorder()

	srv.handleOllamaAPI(w, req)

	if w.Code != http.StatusAccepted {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.ollamaReq.Adapter != "ollama" || fake.ollamaReq.Method != http.MethodPost || fake.ollamaReq.Path != "/api/show" {
		t.Fatalf("passthrough req mismatch: %+v", fake.ollamaReq)
	}
	if fake.ollamaReq.Body != `{"model":"gemma4:26b"}` || fake.ollamaReq.TimeoutSec != 15 {
		t.Fatalf("passthrough body/timeout mismatch: %+v", fake.ollamaReq)
	}
	if w.Body.String() != `{"ok":true}` {
		t.Fatalf("body: got %s", w.Body.String())
	}
}

func TestOllamaAPIPassthroughPreservesConfiguredTarget(t *testing.T) {
	fake := &fakeRunService{
		ollamaResp: edgeservice.OllamaAPIView{
			StatusCode:  http.StatusOK,
			ContentType: "application/json",
			Body:        `{"models":[]}`,
		},
	}
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "gemma4:26b", TimeoutSec: 15}, fake, nil)
	req := httptest.NewRequest(http.MethodGet, "/api/tags", nil)
	w := httptest.NewRecorder()

	srv.handleOllamaAPI(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.ollamaReq.Target != "gemma4:26b" {
		t.Fatalf("passthrough target: got %q, want gemma4:26b", fake.ollamaReq.Target)
	}
}

func TestResponsesDispatchesNonStreamingRequest(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: " world"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}

	srv := NewServer(config.EdgeOpenAIConf{
		Adapter: "ollama",
		Target:  "llama-fixed",
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello"
	}`))
	w := httptest.NewRecorder()

	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "llama-fixed" {
		t.Fatalf("target: got %q, want llama-fixed", fake.req.Target)
	}
	if fake.req.Prompt != "say hello" {
		t.Fatalf("prompt: got %q", fake.req.Prompt)
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp.OutputText != "hello world" {
		t.Fatalf("output_text: got %q", resp.OutputText)
	}
	if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "hello world" {
		t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
	}
}

func TestResponsesRejectsUnsupportedRequests(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)

	cases := []struct {
		name   string
		method string
		body   string
		want   int
	}{
		{"wrong method", http.MethodGet, "", http.StatusMethodNotAllowed},
		{"stream=true", http.MethodPost, `{"model":"m","input":"hi","stream":true}`, http.StatusBadRequest},
		{"empty input", http.MethodPost, `{"model":"m","input":""}`, http.StatusBadRequest},
		{"non-string input", http.MethodPost, `{"model":"m","input":["a","b"]}`, http.StatusBadRequest},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, "/v1/responses", strings.NewReader(tc.body))
			w := httptest.NewRecorder()
			srv.routes().ServeHTTP(w, req)
			if w.Code != tc.want {
				t.Fatalf("got %d want %d body=%s", w.Code, tc.want, w.Body.String())
			}
		})
	}
}

func TestResponsesMetadataContractAndTargetOverride(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"test",
		"metadata":{
			"request_id":"req-001",
			"workspace":"/config/workspace/iop",
			"inference":{"target":"metadata-target"},
			"nomadcode":{"task_id":"task-123","source":"nomadcode"}
		}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "metadata-target" {
		t.Fatalf("target: got %q, want metadata-target", fake.req.Target)
	}
	if fake.req.Workspace != "/config/workspace/iop" {
		t.Fatalf("workspace: got %q", fake.req.Workspace)
	}
	if fake.req.Metadata["request_id"] != "req-001" {
		t.Fatalf("request_id: got %q", fake.req.Metadata["request_id"])
	}
	if _, ok := fake.req.Metadata["workspace"]; ok {
		t.Fatal("workspace should not be copied into run metadata")
	}
	if fake.req.Metadata["inference.target"] != "metadata-target" {
		t.Fatalf("inference.target: got %q", fake.req.Metadata["inference.target"])
	}
	if fake.req.Metadata["nomadcode.task_id"] != "task-123" {
		t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"])
	}
	if fake.req.Metadata["nomadcode.source"] != "nomadcode" {
		t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"])
	}
	if fake.req.Metadata["source"] != "openai-responses" {
		t.Fatalf("source: got %q", fake.req.Metadata["source"])
	}
	if fake.req.Metadata["openai_model"] != "client-model" {
		t.Fatalf("openai_model: got %q", fake.req.Metadata["openai_model"])
	}
	if fake.req.Metadata["openai_stream"] != "false" {
		t.Fatalf("openai_stream: got %q", fake.req.Metadata["openai_stream"])
	}
	if fake.req.Metadata["strict_output"] != "false" {
		t.Fatalf("strict_output: got %q", fake.req.Metadata["strict_output"])
	}
}

func TestChatCompletionsMetadataContractAndWorkspace(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"user","content":"hi"}],
		"metadata":{
			"request_id":"req-chat-001",
			"workspace":"/config/workspace/iop",
			"inference":{"target":"metadata-target"},
			"nomadcode":{"task_id":"task-123","source":"nomadcode"}
		}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "metadata-target" {
		t.Fatalf("target: got %q, want metadata-target", fake.req.Target)
	}
	if fake.req.Workspace != "/config/workspace/iop" {
		t.Fatalf("workspace: got %q", fake.req.Workspace)
	}
	if fake.req.Metadata["request_id"] != "req-chat-001" {
		t.Fatalf("request_id: got %q", fake.req.Metadata["request_id"])
	}
	if fake.req.Metadata["inference.target"] != "metadata-target" {
		t.Fatalf("inference.target: got %q", fake.req.Metadata["inference.target"])
	}
	if fake.req.Metadata["nomadcode.task_id"] != "task-123" {
		t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"])
	}
	if fake.req.Metadata["nomadcode.source"] != "nomadcode" {
		t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"])
	}
	if fake.req.Metadata["source"] != "openai" {
		t.Fatalf("source: got %q", fake.req.Metadata["source"])
	}
	if _, ok := fake.req.Metadata["workspace"]; ok {
		t.Fatal("workspace should not be copied into run metadata")
	}
}

func TestChatCompletionsRejectsCLIMetadata(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"m",
		"messages":[{"role":"user","content":"hi"}],
		"metadata":{"cli":{"flag":"x"}}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
	}
}

func TestResponsesAcceptsFlatNomadCodeMetadataAliases(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"test",
		"metadata":{
			"request_id":"req-flat-001",
			"task_id":"task-flat",
			"source":"plane",
			"inference":{"target":"metadata-target"}
		}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Metadata["nomadcode.task_id"] != "task-flat" {
		t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"])
	}
	if fake.req.Metadata["nomadcode.source"] != "plane" {
		t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"])
	}
	if fake.req.Metadata["source"] != "openai-responses" {
		t.Fatalf("source should remain IOP-owned: got %q", fake.req.Metadata["source"])
	}
}

func TestResponsesStructuredNomadCodeMetadataWinsOverFlatAliases(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"test",
		"metadata":{
			"task_id":"task-flat",
			"source":"plane",
			"nomadcode":{"task_id":"task-structured","source":"manual"}
		}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Metadata["nomadcode.task_id"] != "task-structured" {
		t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"])
	}
	if fake.req.Metadata["nomadcode.source"] != "manual" {
		t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"])
	}
	if fake.req.Metadata["source"] != "openai-responses" {
		t.Fatalf("source should remain IOP-owned: got %q", fake.req.Metadata["source"])
	}
}

func TestResponsesConfiguredTargetWinsOverMetadataTarget(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "config-target"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"test",
		"metadata":{"inference":{"target":"metadata-target"}}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "config-target" {
		t.Fatalf("target: got %q, want config-target", fake.req.Target)
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Model != "client-model" {
		t.Fatalf("model: got %q, want client-model", resp.Model)
	}
}

func TestResponsesRejectsCLIMetadata(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)

	cases := []struct {
		name string
		body string
	}{
		{"cli only", `{"model":"m","input":"hi","metadata":{"cli":{"flag":"x"}}}`},
		{"inference and cli", `{"model":"m","input":"hi","metadata":{"inference":{"target":"t"},"cli":{"flag":"x"}}}`},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(tc.body))
			w := httptest.NewRecorder()
			srv.routes().ServeHTTP(w, req)
			if w.Code != http.StatusBadRequest {
				t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
			}
		})
	}
}

func TestResponsesReturnsNomadCodeCompatibleShape(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !strings.HasPrefix(resp.ID, "resp-") {
		t.Fatalf("id: got %q, want resp- prefix", resp.ID)
	}
	if resp.Model != "client-model" {
		t.Fatalf("model: got %q", resp.Model)
	}
	if resp.OutputText != "answer" {
		t.Fatalf("output_text: got %q", resp.OutputText)
	}
	if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "answer" {
		t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
	}
	if resp.Usage.TotalTokens != 2 {
		t.Fatalf("usage.total_tokens: got %d", resp.Usage.TotalTokens)
	}
}

func TestResponsesUsageDefaultsToZeroObject(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"m",
		"input":"hi"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}

	var body map[string]json.RawMessage
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if _, ok := body["usage"]; !ok {
		t.Fatal("usage field missing from response")
	}
	var usage openAIUsage
	if err := json.Unmarshal(body["usage"], &usage); err != nil {
		t.Fatalf("usage is not an object: %s", body["usage"])
	}
}

func TestResponsesStrictOutputNormalizesAgentResponse(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama", StrictOutput: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"instructions":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>",
		"input":"finish"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Metadata["strict_output"] != "true" {
		t.Fatalf("strict_output metadata: got %q", fake.req.Metadata["strict_output"])
	}
	if fake.req.Input["think"] != false {
		t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
	}
	if !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") {
		t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
	if resp.OutputText != want {
		t.Fatalf("output_text:\ngot  %q\nwant %q", resp.OutputText, want)
	}
	if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != want {
		t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
	}
}

func TestCollectRunResultTimesOut(t *testing.T) {
	handle := &edgeservice.RunHandle{
		RunDispatch: edgeservice.RunDispatch{TimeoutSec: 1},
		RunStream: edgeservice.RunStream{
			Events:     make(chan *iop.RunEvent),
			NodeEvents: make(chan *iop.EdgeNodeEvent),
		},
	}
	ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
	defer cancel()
	_, _, _, err := collectRunResult(ctx, handle.Stream(), handle.WaitTimeout())
	if err == nil {
		t.Fatal("expected timeout error")
	}
}

func TestModelsExposesCatalogRouteModels(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-a", Adapter: "ollama", Target: "llama3"},
			{Model: "model-b", Adapter: "vllm", Target: "qwen"},
		},
	}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()
	srv.handleModels(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d", w.Code)
	}
	body := w.Body.String()
	if !strings.Contains(body, `"id":"model-a"`) || !strings.Contains(body, `"id":"model-b"`) {
		t.Fatalf("expected catalog model IDs, got %s", body)
	}
}

func TestModelsSkipsRoutesWithEmptyTarget(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-with-target", Adapter: "ollama", Target: "llama3"},
			{Model: "model-no-target", Adapter: "ollama", Target: ""},
		},
	}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()
	srv.handleModels(w, req)
	body := w.Body.String()
	if !strings.Contains(body, `"id":"model-with-target"`) {
		t.Fatalf("expected model-with-target in response, got %s", body)
	}
	if strings.Contains(body, "model-no-target") {
		t.Fatalf("model-no-target should be skipped (no target), got %s", body)
	}
}

func TestModelsRouteCatalogWinsOverModelsField(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{
		Models: []string{"legacy-model"},
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "catalog-model", Adapter: "ollama", Target: "llama3"},
		},
	}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()
	srv.handleModels(w, req)
	body := w.Body.String()
	if !strings.Contains(body, `"id":"catalog-model"`) {
		t.Fatalf("expected catalog model in response, got %s", body)
	}
	if strings.Contains(body, "legacy-model") {
		t.Fatalf("legacy model should be hidden when catalog is set, got %s", body)
	}
}

func TestChatCompletionsRouteCatalogDispatches(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-a", Adapter: "ollama", Target: "llama3"},
			{Model: "model-b", Adapter: "vllm", Target: "qwen"},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"model-a",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Adapter != "ollama" || fake.req.Target != "llama3" {
		t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
	}
}

func TestChatCompletionsRouteCatalogDispatchesModelB(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-a", Adapter: "ollama", Target: "llama3"},
			{Model: "model-b", Adapter: "vllm", Target: "qwen"},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"model-b",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Adapter != "vllm" || fake.req.Target != "qwen" {
		t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
	}
}

func TestChatCompletionsCatalogMissFallsToLegacyTarget(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{
		Adapter: "ollama",
		Target:  "legacy-target",
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-a", Adapter: "ollama", Target: "llama3"},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"unknown-model",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "legacy-target" {
		t.Fatalf("expected legacy-target fallback, got %q", fake.req.Target)
	}
}

func TestChatCompletionsEmptyModelReturns400(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for empty model, got %d body=%s", w.Code, w.Body.String())
	}
}

func TestResponsesRouteCatalogDispatchesRoute(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-a", Adapter: "vllm", Target: "qwen"},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"model-a",
		"input":"say hello"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Adapter != "vllm" || fake.req.Target != "qwen" {
		t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
	}
}

func TestResponsesRouteCatalogTakesPriorityOverMetadataTarget(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "model-a", Adapter: "ollama", Target: "route-target"},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"model-a",
		"input":"test",
		"metadata":{"inference":{"target":"metadata-target"}}
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "route-target" {
		t.Fatalf("expected route-target, got %q", fake.req.Target)
	}
}

func TestChatCompletionsEdgePassesNumCtxForAdapterEnforcement(t *testing.T) {
	// Edge passes request options.num_ctx to the service unchanged.
	// The Ollama adapter enforces Edge-owned context_size over this value.
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"from-request",
		"messages":[{"role":"user","content":"hi"}],
		"options":{"num_ctx":8192}
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	options, ok := fake.req.Input["options"].(map[string]any)
	if !ok {
		t.Fatalf("options not passed to service: %+v", fake.req.Input)
	}
	if options["num_ctx"].(float64) != 8192 {
		t.Fatalf("edge should pass num_ctx unchanged for adapter enforcement: got %v", options["num_ctx"])
	}
}

func TestResolveRouteDispatchPreservesWorkspaceRequired(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "codex", Adapter: "cli", Target: "codex", WorkspaceRequired: true},
			{Model: "llama3", Adapter: "ollama", Target: "llama3:8b"},
		},
	}, &fakeRunService{}, nil)

	dispatch, ok := srv.resolveRouteDispatch("codex", "")
	if !ok {
		t.Fatal("expected dispatch to succeed for codex route")
	}
	if !dispatch.WorkspaceRequired {
		t.Fatalf("expected workspace_required=true for codex route, got false")
	}

	dispatch, ok = srv.resolveRouteDispatch("llama3", "")
	if !ok {
		t.Fatal("expected dispatch to succeed for llama3 route")
	}
	if dispatch.WorkspaceRequired {
		t.Fatalf("expected workspace_required=false for llama3 route, got true")
	}
}

func TestResolveRouteDispatchFallbackWorkspaceRequiredFalse(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{
		Adapter: "ollama",
		Target:  "llama3",
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "codex", Adapter: "cli", Target: "codex", WorkspaceRequired: true},
		},
	}, &fakeRunService{}, nil)

	dispatch, ok := srv.resolveRouteDispatch("unknown-model", "")
	if !ok {
		t.Fatal("expected fallback dispatch to succeed")
	}
	if dispatch.WorkspaceRequired {
		t.Fatalf("expected workspace_required=false for fallback route, got true")
	}
}

func TestCollectRunResultFailsWhenEventStreamCloses(t *testing.T) {
	events := make(chan *iop.RunEvent)
	close(events)
	handle := &edgeservice.RunHandle{
		RunDispatch: edgeservice.RunDispatch{TimeoutSec: 60},
		RunStream: edgeservice.RunStream{
			Events:     events,
			NodeEvents: make(chan *iop.EdgeNodeEvent),
		},
	}

	_, _, _, err := collectRunResult(context.Background(), handle.Stream(), handle.WaitTimeout())
	if err == nil {
		t.Fatal("expected closed stream error")
	}
	if !strings.Contains(err.Error(), "run stream closed") {
		t.Fatalf("expected run stream closed error, got %v", err)
	}
}

// workspaceBoundCfg returns a server config with a workspace-required codex route and a
// non-required llama3 route for workspace validation tests.
func workspaceBoundCfg() config.EdgeOpenAIConf {
	return config.EdgeOpenAIConf{
		Adapter: "ollama",
		Target:  "llama3",
		ModelRoutes: []config.OpenAIRouteEntry{
			{Model: "codex", Adapter: "cli", Target: "codex", WorkspaceRequired: true},
			{Model: "llama3", Adapter: "ollama", Target: "llama3"},
		},
	}
}

func TestResponsesWorkspaceRequiredRouteMissingWorkspace400(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"codex","input":"hello"}`
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("missing workspace: want 400, got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "workspace is required") {
		t.Fatalf("expected workspace error, got %s", w.Body.String())
	}
}

func TestResponsesWorkspaceRequiredRouteRelativeWorkspace400(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"codex","input":"hello","metadata":{"workspace":"relative/path"}}`
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("relative workspace: want 400, got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "absolute path") {
		t.Fatalf("expected absolute path error, got %s", w.Body.String())
	}
}

func TestResponsesWorkspaceRequiredRouteAbsoluteWorkspaceOK(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"codex","input":"hello","metadata":{"workspace":"/abs/path"}}`
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("absolute workspace: want 200, got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Workspace != "/abs/path" {
		t.Fatalf("workspace not preserved: got %q", fake.req.Workspace)
	}
}

func TestResponsesNonRequiredRouteNoWorkspaceOK(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"llama3","input":"hello"}`
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("non-required route no workspace: want 200, got %d body=%s", w.Code, w.Body.String())
	}
}

func TestChatCompletionsWorkspaceRequiredRouteMissingWorkspace400(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"codex","messages":[{"role":"user","content":"hi"}]}`
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("missing workspace: want 400, got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "workspace is required") {
		t.Fatalf("expected workspace error, got %s", w.Body.String())
	}
}

func TestChatCompletionsWorkspaceRequiredRouteRelativeWorkspace400(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"codex","messages":[{"role":"user","content":"hi"}],"metadata":{"workspace":"some/relative"}}`
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("relative workspace: want 400, got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "absolute path") {
		t.Fatalf("expected absolute path error, got %s", w.Body.String())
	}
}

func TestChatCompletionsWorkspaceRequiredRouteAbsoluteWorkspaceOK(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"codex","messages":[{"role":"user","content":"hi"}],"metadata":{"workspace":"/abs/workspace"}}`
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("absolute workspace: want 200, got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Workspace != "/abs/workspace" {
		t.Fatalf("workspace not preserved: got %q", fake.req.Workspace)
	}
}

func TestChatCompletionsNonRequiredRouteNoWorkspaceOK(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}
	srv := NewServer(workspaceBoundCfg(), fake, nil)
	body := `{"model":"llama3","messages":[{"role":"user","content":"hi"}]}`
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)
	if w.Code != http.StatusOK {
		t.Fatalf("non-required route no workspace: want 200, got %d body=%s", w.Code, w.Body.String())
	}
}
