package openai

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

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

// fakeRunResultWithTimeout implements edgeservice.RunResult with a directly
// configurable WaitTimeout, so tests can exercise the run-timeout cancel path
// without waiting out the real DefaultTimeoutSec-derived duration.
type fakeRunResultWithTimeout struct {
	dispatch    edgeservice.RunDispatch
	stream      edgeservice.RunStream
	waitTimeout time.Duration
}

func (f *fakeRunResultWithTimeout) Dispatch() edgeservice.RunDispatch { return f.dispatch }
func (f *fakeRunResultWithTimeout) Stream() edgeservice.RunStream     { return f.stream }
func (f *fakeRunResultWithTimeout) Close()                            {}
func (f *fakeRunResultWithTimeout) WaitTimeout() time.Duration        { return f.waitTimeout }

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 TestChatCompletionContextCancelSendsCancelRun(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(config.EdgeOpenAIConf{
		Adapter:    "ollama",
		Target:     "llama-fixed",
		SessionID:  "cline",
		TimeoutSec: 15,
	}, fake, nil)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"user","content":"hi"}]
	}`)).WithContext(ctx)
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusRequestTimeout {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	calls := fake.cancelCallsSnapshot()
	if len(calls) != 1 {
		t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
	}
	if calls[0].RunID != "run-test" {
		t.Fatalf("unexpected cancel request: %+v", calls[0])
	}
}
func TestResponsesContextCancelSendsCancelRun(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(config.EdgeOpenAIConf{
		Adapter:    "ollama",
		Target:     "llama-fixed",
		SessionID:  "cline",
		TimeoutSec: 15,
	}, fake, nil)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello"
	}`)).WithContext(ctx)
	w := httptest.NewRecorder()

	srv.handleResponses(w, req)

	if w.Code != http.StatusRequestTimeout {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	calls := fake.cancelCallsSnapshot()
	if len(calls) != 1 {
		t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
	}
	if calls[0].RunID != "run-test" {
		t.Fatalf("unexpected cancel request: %+v", calls[0])
	}
}
func TestStreamChatCompletionContextCancelSendsCancelRun(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent)}
	srv := NewServer(config.EdgeOpenAIConf{
		Adapter:    "ollama",
		Target:     "llama-fixed",
		SessionID:  "cline",
		TimeoutSec: 15,
	}, fake, nil)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"stream":true,
		"messages":[{"role":"user","content":"hi"}]
	}`)).WithContext(ctx)
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	calls := fake.cancelCallsSnapshot()
	if len(calls) != 1 {
		t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
	}
	if calls[0].RunID != "run-test" {
		t.Fatalf("unexpected cancel request: %+v", calls[0])
	}
}
func TestStreamChatCompletionTimeoutSendsCancelRun(t *testing.T) {
	fake := &fakeRunService{}
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)

	handle := &fakeRunResultWithTimeout{
		dispatch: edgeservice.RunDispatch{
			RunID:     "run-timeout",
			NodeID:    "node-1",
			Adapter:   "ollama",
			Target:    "llama3",
			SessionID: "cline",
		},
		stream: edgeservice.RunStream{
			Events:     make(chan *iop.RunEvent),
			NodeEvents: make(chan *iop.EdgeNodeEvent),
		},
		waitTimeout: 50 * time.Millisecond,
	}

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
	w := httptest.NewRecorder()

	srv.streamChatCompletion(w, newTestChatDispatchContext(req, chatCompletionRequest{Model: "llama3"}), handle)

	calls := fake.cancelCallsSnapshot()
	if len(calls) != 1 {
		t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
	}
	if calls[0].RunID != "run-timeout" || calls[0].NodeRef != "node-1" {
		t.Fatalf("unexpected cancel request: %+v", calls[0])
	}
	if !strings.Contains(w.Body.String(), "run timed out") {
		t.Fatalf("expected timeout SSE error, got %s", w.Body.String())
	}
}
func TestChatCompletionCompleteDoesNotSendCancelRun(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	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())
	}
	if calls := fake.cancelCallsSnapshot(); len(calls) != 0 {
		t.Fatalf("expected no CancelRun calls for a normal completion, got %d: %+v", len(calls), calls)
	}
}
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: "ollama", Target: "qwen", MaxQueue: 5, QueueTimeoutMS: 2000},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"metadata":{},
		"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 != "ollama" || fake.req.Target != "qwen" {
		t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
	}
	if fake.req.ModelGroupKey != "model-b" {
		t.Fatalf("model group key: got %q, want model-b", fake.req.ModelGroupKey)
	}
	if fake.req.MaxQueue != 5 || fake.req.QueueTimeoutMS != 2000 {
		t.Fatalf("queue policy mismatch: MaxQueue=%d, QueueTimeoutMS=%d", fake.req.MaxQueue, fake.req.QueueTimeoutMS)
	}
}
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: "ollama", 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 != "ollama" || fake.req.Target != "qwen" {
		t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
	}
	if fake.req.ModelGroupKey != "model-a" {
		t.Fatalf("model group key: got %q, want model-a", fake.req.ModelGroupKey)
	}
}
func TestResponsesRouteCatalogDispatchesQueuePolicy(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", MaxQueue: 3, QueueTimeoutMS: 1500},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"model-a",
		"input":"test"
	}`))
	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)
	}
	if fake.req.ModelGroupKey != "model-a" {
		t.Fatalf("model group key: got %q, want model-a", fake.req.ModelGroupKey)
	}
	if fake.req.MaxQueue != 3 || fake.req.QueueTimeoutMS != 1500 {
		t.Fatalf("queue policy mismatch: MaxQueue=%d, QueueTimeoutMS=%d", fake.req.MaxQueue, fake.req.QueueTimeoutMS)
	}
}

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)
	}
}
