package openai

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

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

func TestRoutesRequireBearerTokenWhenConfigured(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{
		BearerToken: "secret-token",
		Models:      []string{"model-a"},
	}, &fakeRunService{}, nil)

	for _, tc := range []struct {
		name       string
		auth       string
		wantStatus int
	}{
		{name: "missing", wantStatus: http.StatusUnauthorized},
		{name: "wrong", auth: "Bearer wrong", wantStatus: http.StatusUnauthorized},
		{name: "valid", auth: "Bearer secret-token", wantStatus: http.StatusOK},
	} {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
			if tc.auth != "" {
				req.Header.Set("Authorization", tc.auth)
			}
			w := httptest.NewRecorder()

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

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

func TestOpenAIRoutesDoNotAcceptAnthropicAPIKey(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{BearerToken: "secret-token"}, &fakeRunService{}, nil)

	for _, tc := range []struct {
		name    string
		method  string
		path    string
		headers map[string]string
	}{
		{
			name:    "models without Anthropic version",
			method:  http.MethodGet,
			path:    "/v1/models",
			headers: map[string]string{"X-Api-Key": "secret-token"},
		},
		{
			name:   "chat with unrelated Anthropic version",
			method: http.MethodPost,
			path:   "/v1/chat/completions",
			headers: map[string]string{
				"X-Api-Key":            "secret-token",
				anthropicVersionHeader: anthropicSupportedVersion,
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, tc.path, nil)
			for key, value := range tc.headers {
				req.Header.Set(key, value)
			}
			w := httptest.NewRecorder()

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

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

func TestHealthzDoesNotRequireBearerToken(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{BearerToken: "secret-token"}, &fakeRunService{}, nil)
	req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
	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())
	}
}

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

// TestOpenAIModelsUsesRefreshedCatalog verifies that a provider-pool catalog
// applied via SetModelCatalog (config refresh apply) is reflected in /v1/models
// on the next request, replacing the previous catalog snapshot.
func TestOpenAIModelsUsesRefreshedCatalog(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-old"}})

	req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
	w := httptest.NewRecorder()
	srv.handleModels(w, req)
	if body := w.Body.String(); !strings.Contains(body, `"id":"model-old"`) {
		t.Fatalf("expected initial catalog model-old, got %s", body)
	}

	// Refresh the catalog: the next request must show the new model and not the old.
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-new"}})
	w = httptest.NewRecorder()
	srv.handleModels(w, req)
	body := w.Body.String()
	if !strings.Contains(body, `"id":"model-new"`) {
		t.Fatalf("expected refreshed catalog model-new, got %s", body)
	}
	if strings.Contains(body, `"id":"model-old"`) {
		t.Fatalf("stale catalog model-old still present after refresh, got %s", body)
	}
}

// TestOpenAIProviderPoolRouteUsesRefreshedCatalog verifies that provider-pool
// route resolution reads the refreshed catalog snapshot, so a model added by a
// config refresh is routed via the provider pool.
func TestOpenAIProviderPoolRouteUsesRefreshedCatalog(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
	if srv.findProviderPoolEntry("model-new") != nil {
		t.Fatal("expected no provider-pool entry before catalog is set")
	}
	srv.SetModelCatalog([]config.ModelCatalogEntry{{
		ID:        "model-new",
		Providers: map[string]string{"prov-a": "served-a"},
	}})
	dispatch, ok := srv.resolveRouteDispatch("model-new")
	if !ok || !dispatch.ProviderPool {
		t.Fatalf("expected provider-pool dispatch for refreshed model, got ok=%v dispatch=%+v", ok, dispatch)
	}
}

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 TestUnmanagedSingleRequestPresetFailsClosed(t *testing.T) {
	markedPreset := config.ExecutionPreset{
		ID:           "preset-marked",
		Selector:     config.ExecutionModelBinding{Model: "plan-model", Options: map[string]any{"reasoning_effort": "high"}},
		AllowedModes: []string{config.ModeLight},
		Routes: map[string]config.ExecutionRoute{
			config.ModeLight: {Stages: []config.ExecutionRouteStage{
				{Role: "plan", Model: "plan-model", Options: map[string]any{"reasoning_effort": "high"}},
				{Role: "work", Model: "work-model"},
				{Role: "review", Model: "review-model", Options: map[string]any{"reasoning_effort": "high"}},
			}},
		},
		SingleRequest: &config.ExecutionSingleRequestPolicy{
			WorkspaceRef: "ws-ref",
			Limits:       config.ExecutionSingleRequestLimits{WallClockMS: 30 * 60 * 1000, StageTimeoutMS: 10 * 60 * 1000, MaxToolIterations: 64, MaxOutputBytes: 16 * 1024 * 1024},
			Stages: config.ExecutionSingleRequestStages{
				Plan:   config.ExecutionSingleRequestStageConfig{Model: "plan-model", Options: map[string]any{"reasoning_effort": "high"}},
				Work:   config.ExecutionSingleRequestStageConfig{Model: "work-model"},
				Review: config.ExecutionSingleRequestStageConfig{Model: "review-model", Options: map[string]any{"reasoning_effort": "high"}},
			},
		},
	}
	// A legacy, unmarked preset (no single_request policy) must keep resolving and
	// being advertised: fail-closed applies only to marked presets.
	legacyPreset := config.ExecutionPreset{
		ID:           "preset-legacy",
		Selector:     config.ExecutionModelBinding{Model: "provider-model-a"},
		AllowedModes: []string{config.ModeDirect},
		Routes:       map[string]config.ExecutionRoute{config.ModeDirect: {}},
	}

	srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
	srv.SetExecutionPresets([]config.ExecutionPreset{markedPreset, legacyPreset})
	srv.SetModelCatalog([]config.ModelCatalogEntry{
		{ID: "virtual-marked", ExecutionPreset: "preset-marked"},
		{ID: "virtual-legacy", ExecutionPreset: "preset-legacy"},
		{ID: "plan-model", Providers: map[string]string{"prov-1": "served-plan"}},
		{ID: "work-model", Providers: map[string]string{"prov-1": "served-work"}},
		{ID: "review-model", Providers: map[string]string{"prov-1": "served-review"}},
		{ID: "provider-model-a", Providers: map[string]string{"prov-1": "served-a"}},
	})

	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":"virtual-marked"`) {
		t.Fatalf("marked single-request preset must be omitted from unmanaged /v1/models, got %s", body)
	}
	if !strings.Contains(body, `"id":"virtual-legacy"`) {
		t.Fatalf("unmarked legacy preset must remain listed, got %s", body)
	}

	if _, ok := srv.resolveRouteDispatch("virtual-marked"); ok {
		t.Fatal("expected marked preset to fail closed at resolveRouteDispatch")
	}
	disp, ok := srv.resolveRouteDispatch("virtual-legacy")
	if !ok || !disp.IsPreset || disp.PresetID != "preset-legacy" || disp.ExternalModelID != "virtual-legacy" {
		t.Fatalf("expected unmarked legacy preset to resolve, got ok=%v disp=%+v", ok, disp)
	}
	if disp.SingleRequest != nil {
		t.Fatalf("legacy preset must not carry a single-request admission: %+v", disp.SingleRequest)
	}
}

func TestLegacyVirtualPresetModelResolution(t *testing.T) {
	preset := config.ExecutionPreset{
		ID: "preset-legacy-1",
		Selector: config.ExecutionModelBinding{
			Model: "provider-model-a",
		},
		AllowedModes: []string{config.ModeDirect},
		Routes: map[string]config.ExecutionRoute{
			config.ModeDirect: {},
		},
	}

	srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
	srv.SetExecutionPresets([]config.ExecutionPreset{preset})

	srv.SetModelCatalog([]config.ModelCatalogEntry{
		{
			ID:              "virtual-legacy",
			ExecutionPreset: "preset-legacy-1",
		},
		{
			ID:        "provider-model-a",
			Providers: map[string]string{"prov-1": "served-a"},
		},
	})

	// 1. /v1/models lists virtual-legacy
	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(), `"id":"virtual-legacy"`) {
		t.Fatalf("expected virtual-legacy in /v1/models, got %s", w.Body.String())
	}

	// 2. Dispatch resolution succeeds
	disp, ok := srv.resolveRouteDispatch("virtual-legacy")
	if !ok || !disp.IsPreset || disp.PresetID != "preset-legacy-1" || disp.ExternalModelID != "virtual-legacy" {
		t.Fatalf("resolveRouteDispatch virtual-legacy unexpected: ok=%v disp=%+v", ok, disp)
	}

	// 3. When canonical reference "provider-model-a" is missing from catalog, virtual-legacy is filtered out
	srv.SetModelCatalog([]config.ModelCatalogEntry{
		{
			ID:              "virtual-legacy",
			ExecutionPreset: "preset-legacy-1",
		},
	})
	w = httptest.NewRecorder()
	srv.handleModels(w, req)
	if strings.Contains(w.Body.String(), `"id":"virtual-legacy"`) {
		t.Fatalf("expected virtual-legacy to be filtered out when reference is missing, got %s", w.Body.String())
	}
	if _, ok := srv.resolveRouteDispatch("virtual-legacy"); ok {
		t.Fatalf("expected resolveRouteDispatch to fail when reference is missing")
	}
}
