package server

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/mem9-ai/drive9/pkg/backend"
	"github.com/mem9-ai/drive9/pkg/meta"
	"github.com/mem9-ai/drive9/pkg/metrics"
	"github.com/mem9-ai/drive9/pkg/tenant"
)

func TestAdminExtractConfigProviderValidation(t *testing.T) {
	t.Run("full trio validates exact candidate before persistence", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		var calls int
		var got backend.OpenAIImageTextExtractorConfig
		rt.server.imageExtractProviderValidator = func(_ context.Context, cfg backend.OpenAIImageTextExtractorConfig) error {
			calls++
			got = cfg
			if _, err := rt.meta.GetTenantExtractConfig(context.Background(), rt.tenantID, meta.ExtractMediaTypeImage); !errors.Is(err, meta.ErrNotFound) {
				t.Fatalf("config became visible before validation: %v", err)
			}
			return nil
		}
		ts := httptest.NewServer(rt.server)
		defer ts.Close()

		resp := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image",
			`{"enabled":true,"api_base":"https://vision.example.com/v1","api_key":"tenant-key","model":"vision-model"}`,
			legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200; body=%v", resp.StatusCode, decodeExtractConfigBody(t, resp))
		}
		_ = decodeExtractConfigBody(t, resp)
		if calls != 1 || got.BaseURL != "https://vision.example.com/v1" || got.APIKey != "tenant-key" || got.Model != "vision-model" {
			t.Fatalf("validation = calls:%d config:%+v", calls, got)
		}
		row := getPersistedExtractConfig(t, rt)
		if row.APIBase != got.BaseURL || row.Model != got.Model || decryptExtractKey(t, rt, row.APIKeyCipher) != got.APIKey {
			t.Fatalf("persisted row does not match validated candidate: %+v", row)
		}
	})

	t.Run("provider rejection is safe and leaves row unchanged", func(t *testing.T) {
		const providerSecret = "provider-secret-detail-must-not-escape"
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		seedExtractConfigRow(t, rt, true, "https://old.example.com", "old-key", "old-model", "old-prompt")
		before := getPersistedExtractConfig(t, rt)
		providerErr := imageProviderHTTPFailure(t, http.StatusUnauthorized, providerSecret)
		rt.server.imageExtractProviderValidator = func(context.Context, backend.OpenAIImageTextExtractorConfig) error {
			return providerErr
		}
		ts := httptest.NewServer(rt.server)
		defer ts.Close()

		resp := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image",
			`{"api_base":"https://new.example.com","api_key":"new-key","model":"new-model"}`,
			legacyExtractHeaders())
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		errText := fmt.Sprint(out["error"])
		if !strings.Contains(errText, "image extraction provider validation failed: authentication rejected") || strings.Contains(errText, providerSecret) {
			t.Fatalf("unsafe or unexpected error = %q", errText)
		}
		after := getPersistedExtractConfig(t, rt)
		if after.APIBase != before.APIBase || after.Model != before.Model || after.Prompt != before.Prompt ||
			!after.UpdatedAt.Equal(before.UpdatedAt) || decryptExtractKey(t, rt, after.APIKeyCipher) != "old-key" {
			t.Fatalf("failed validation changed row: before=%+v after=%+v", before, after)
		}
		recorder := httptest.NewRecorder()
		metrics.WritePrometheus(recorder)
		metric := `drive9_service_operations_total{component="image_extract",operation="provider_validate",result="auth_rejected"}`
		if !strings.Contains(recorder.Body.String(), metric) || strings.Contains(recorder.Body.String(), providerSecret) {
			t.Fatalf("provider validation metric missing or unsafe: %s", recorder.Body.String())
		}
	})

	t.Run("availability and timeout map to gateway statuses", func(t *testing.T) {
		tests := []struct {
			providerStatus int
			wantStatus     int
		}{
			{http.StatusInternalServerError, http.StatusBadGateway},
			{http.StatusGatewayTimeout, http.StatusGatewayTimeout},
		}
		for _, tt := range tests {
			t.Run(http.StatusText(tt.providerStatus), func(t *testing.T) {
				rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
				providerErr := imageProviderHTTPFailure(t, tt.providerStatus, "provider detail")
				rt.server.imageExtractProviderValidator = func(context.Context, backend.OpenAIImageTextExtractorConfig) error {
					return providerErr
				}
				ts := httptest.NewServer(rt.server)
				defer ts.Close()
				resp := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image",
					`{"enabled":true,"api_base":"https://new.example.com","api_key":"new-key","model":"new-model"}`,
					legacyExtractHeaders())
				if resp.StatusCode != tt.wantStatus {
					t.Fatalf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
				}
				_ = decodeExtractConfigBody(t, resp)
				if _, err := rt.meta.GetTenantExtractConfig(context.Background(), rt.tenantID, meta.ExtractMediaTypeImage); !errors.Is(err, meta.ErrNotFound) {
					t.Fatalf("failed validation persisted config: %v", err)
				}
			})
		}
	})

	t.Run("partial provider update is rejected and non-provider changes do not probe", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		seedExtractConfigRow(t, rt, true, "https://old.example.com", "old-key", "old-model", "old-prompt")
		var calls int
		rt.server.imageExtractProviderValidator = func(context.Context, backend.OpenAIImageTextExtractorConfig) error {
			calls++
			return nil
		}
		ts := httptest.NewServer(rt.server)
		defer ts.Close()

		partial := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image", `{"model":"new-model"}`, legacyExtractHeaders())
		if partial.StatusCode != http.StatusBadRequest {
			t.Fatalf("partial provider status = %d, want 400", partial.StatusCode)
		}
		_ = decodeExtractConfigBody(t, partial)
		prompt := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image", `{"prompt":"new-prompt"}`, legacyExtractHeaders())
		if prompt.StatusCode != http.StatusOK {
			t.Fatalf("prompt status = %d, want 200", prompt.StatusCode)
		}
		_ = decodeExtractConfigBody(t, prompt)
		disable := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image", `{"enabled":false}`, legacyExtractHeaders())
		if disable.StatusCode != http.StatusOK {
			t.Fatalf("disable status = %d, want 200", disable.StatusCode)
		}
		_ = decodeExtractConfigBody(t, disable)
		if calls != 0 {
			t.Fatalf("provider validation calls = %d, want 0", calls)
		}
	})
}

func TestAdminExtractConfigMasksExactlyFourCharKey(t *testing.T) {
	rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
	ts := httptest.NewServer(rt.server)
	defer ts.Close()

	putResp := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image",
		`{"enabled":true,"api_base":"https://api.example.com","api_key":"abcd","model":"model-1"}`, legacyExtractHeaders())
	if putResp.StatusCode != http.StatusOK {
		t.Fatalf("PUT status = %d, want 200", putResp.StatusCode)
	}
	out := decodeExtractConfigBody(t, putResp)
	if out["api_key"] != maskedAPIKeySuffix {
		t.Fatalf("api_key = %v, want fixed fully masked value", out["api_key"])
	}

	getResp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, "image", "", legacyExtractHeaders())
	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("GET status = %d, want 200", getResp.StatusCode)
	}
	out = decodeExtractConfigBody(t, getResp)
	if out["api_key"] != maskedAPIKeySuffix {
		t.Fatalf("GET api_key = %v, want fixed fully masked value", out["api_key"])
	}
}
