package openai

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

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 TestResponsesGenericMetadataContract(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",
			"task_id":"task-123",
			"custom":"value"
		}
	}`))
	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 != "client-model" {
		t.Fatalf("target: got %q, want client-model", 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["task_id"] != "task-123" {
		t.Fatalf("task_id: got %q", fake.req.Metadata["task_id"])
	}
	if fake.req.Metadata["custom"] != "value" {
		t.Fatalf("custom: got %q", fake.req.Metadata["custom"])
	}
	if fake.req.Metadata["openai_model"] != "client-model" {
		t.Fatalf("openai_model: got %q", fake.req.Metadata["openai_model"])
	}
	if fake.req.ModelGroupKey != "client-model" {
		t.Fatalf("model group key: got %q, want client-model", fake.req.ModelGroupKey)
	}
	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",
			"task_id":"task-123"
		}
	}`))
	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 != "client-model" {
		t.Fatalf("target: got %q, want client-model", 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["task_id"] != "task-123" {
		t.Fatalf("task_id: got %q", fake.req.Metadata["task_id"])
	}
	if fake.req.ModelGroupKey != "client-model" {
		t.Fatalf("model group key: got %q, want client-model", fake.req.ModelGroupKey)
	}
	if _, ok := fake.req.Metadata["workspace"]; ok {
		t.Fatal("workspace should not be copied into run metadata")
	}
}

func TestChatCompletionsRejectsObjectMetadata(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 TestChatCompletionsRejectsSourceMetadata(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":{"source":"manual"}
	}`))
	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())
	}
	if !strings.Contains(w.Body.String(), "metadata.source is not supported") {
		t.Fatalf("expected source unsupported error, got %s", w.Body.String())
	}
}

func TestChatCompletionsRejectsNonStringMetadataValue(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":{"attempt":2}
	}`))
	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())
	}
	if !strings.Contains(w.Body.String(), "metadata.attempt must be a string") {
		t.Fatalf("expected string metadata error, got %s", w.Body.String())
	}
}

func TestChatCompletionsRejectsObjectWorkspaceMetadata(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":{
			"workspace": {
				"path": "/home/user/workspace",
				"source_branch": "develop"
			}
		}
	}`))
	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())
	}
	if !strings.Contains(w.Body.String(), "metadata.workspace must be a string") {
		t.Fatalf("expected workspace string error, got %s", w.Body.String())
	}
}

func TestResponsesPreservesGenericTaskMetadata(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"
		}
	}`))
	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["task_id"] != "task-flat" {
		t.Fatalf("task_id: got %q", fake.req.Metadata["task_id"])
	}
}

func TestResponsesRejectsNonStringMetadata(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 target", `{"model":"m","input":"hi","metadata":{"inference":{"target":"t"}}}`},
		{"nomadcode metadata", `{"model":"m","input":"hi","metadata":{"nomadcode":{"task_id":"t"}}}`},
		{"source metadata", `{"model":"m","input":"hi","metadata":{"source":"manual"}}`},
		{"number metadata", `{"model":"m","input":"hi","metadata":{"attempt":2}}`},
		{"boolean metadata", `{"model":"m","input":"hi","metadata":{"urgent":true}}`},
	}

	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 TestResponsesRejectsObjectWorkspaceMetadata(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"m",
		"input":"hi",
		"metadata":{
			"workspace": {
				"path": "/home/user/workspace",
				"source_branch": "develop"
			}
		}
	}`))
	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())
	}
	if !strings.Contains(w.Body.String(), "metadata.workspace must be a string") {
		t.Fatalf("expected workspace string error, got %s", w.Body.String())
	}
}

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 TestResponsesSurfacesDistinctRunFailures(t *testing.T) {
	cases := []struct {
		name      string
		runError  string
		want      []string
		forbidAny []string
	}{
		{
			name:     "missing workspace path from node",
			runError: "cli adapter: workspace not found: /abs/missing",
			want:     []string{`"type":"run_error"`, "cli adapter: workspace not found: /abs/missing"},
		},
		{
			name:     "inaccessible workspace path from node",
			runError: "cli adapter: workspace inaccessible: /abs/private: permission denied",
			want:     []string{`"type":"run_error"`, "cli adapter: workspace inaccessible: /abs/private"},
		},
		{
			name:      "agent process exit failure",
			runError:  "command failed: exit status 7",
			want:      []string{`"type":"run_error"`, "command failed: exit status 7"},
			forbidAny: []string{"workspace not found", "workspace inaccessible", "workspace is not a directory"},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			fake := &fakeRunService{events: make(chan *iop.RunEvent, 1)}
			fake.events <- &iop.RunEvent{Type: "error", Error: tc.runError}
			srv := NewServer(workspaceBoundCfg(), fake, nil)
			body := `{"model":"codex","input":"hello","metadata":{"workspace":"/abs/workspace"}}`
			req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
			w := httptest.NewRecorder()
			srv.handleResponses(w, req)

			if w.Code != http.StatusBadGateway {
				t.Fatalf("run failure: want 502, got %d body=%s", w.Code, w.Body.String())
			}
			for _, want := range tc.want {
				if !strings.Contains(w.Body.String(), want) {
					t.Fatalf("expected body to contain %q, got %s", want, w.Body.String())
				}
			}
			for _, forbidden := range tc.forbidAny {
				if strings.Contains(w.Body.String(), forbidden) {
					t.Fatalf("body should not contain %q, got %s", forbidden, 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())
	}
}
