package server

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

	"github.com/mem9-ai/drive9/pkg/backend"
	"github.com/mem9-ai/drive9/pkg/innerauth"
	"github.com/mem9-ai/drive9/pkg/meta"
	"github.com/mem9-ai/drive9/pkg/tenant"
	"github.com/mem9-ai/drive9/pkg/tenant/token"
	"github.com/mem9-ai/drive9/pkg/testtidb"
)

type extractConfigRuntime struct {
	meta     *meta.Store
	pool     *tenant.Pool
	tenantID string
	prov     *quotaTestProvisioner
	server   *Server
}

func newExtractConfigRuntime(t *testing.T, provider, orgID string, dispatcher *innerauth.Dispatcher) *extractConfigRuntime {
	t.Helper()
	db := newTenantDeleteDBInfo(t)
	testtidb.ResetMetaDB(t, db.Meta.DB())
	t.Cleanup(func() {
		testtidb.ResetMetaDB(t, db.Meta.DB())
	})

	tenantID := token.NewID()
	tokenSecret := make([]byte, 32)
	if _, err := rand.Read(tokenSecret); err != nil {
		t.Fatal(err)
	}
	dbPassCipher, err := db.Pool.Encrypt(context.Background(), []byte(db.DBPass))
	if err != nil {
		t.Fatal(err)
	}
	now := time.Now().UTC()
	if err := db.Meta.InsertTenant(context.Background(), &meta.Tenant{
		ID:               tenantID,
		Status:           meta.TenantActive,
		Kind:             meta.TenantKindLive,
		DBHost:           db.DBHost,
		DBPort:           db.DBPort,
		DBUser:           db.DBUser,
		DBPasswordCipher: dbPassCipher,
		DBName:           db.DBName,
		DBTLS:            false,
		Provider:         provider,
		ClusterID:        "cluster-extract-1",
		SchemaVersion:    1,
		CreatedAt:        now,
		UpdatedAt:        now,
	}); err != nil {
		t.Fatal(err)
	}
	if provider == tenant.ProviderTiDBCloudNative {
		if err := db.Meta.UpsertTenantTiDBCloudOrgBinding(context.Background(), &meta.TenantTiDBCloudOrgBinding{
			TenantID: tenantID, OrganizationID: orgID, ClusterID: "cluster-extract-1",
			CreatedAt: now, UpdatedAt: now,
		}); err != nil {
			t.Fatal(err)
		}
	}
	prov := &quotaTestProvisioner{provider: provider, clusterExists: true}
	server := NewWithConfig(Config{Meta: db.Meta, Pool: db.Pool, Provisioner: prov,
		TokenSecret: tokenSecret, InnerAuth: dispatcher})
	server.imageExtractProviderValidator = func(context.Context, backend.OpenAIImageTextExtractorConfig) error {
		return nil
	}
	t.Cleanup(server.Close)
	return &extractConfigRuntime{meta: db.Meta, pool: db.Pool, tenantID: tenantID, prov: prov, server: server}
}

func imageProviderHTTPFailure(t *testing.T, status int, detail string) error {
	t.Helper()
	provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(status)
		_, _ = io.WriteString(w, `{"error":{"message":"`+detail+`"}}`)
	}))
	t.Cleanup(provider.Close)
	extractor, err := backend.NewOpenAIImageTextExtractor(backend.OpenAIImageTextExtractorConfig{
		BaseURL: provider.URL,
		APIKey:  "provider-test-key",
		Model:   "provider-test-model",
		Prompt:  "provider test prompt",
		Client:  provider.Client(),
	})
	if err != nil {
		t.Fatal(err)
	}
	_, _, err = extractor.ExtractImageText(context.Background(), backend.ImageExtractRequest{Data: []byte("png")})
	if err == nil {
		t.Fatal("expected provider failure")
	}
	return err
}

// bindSharedExtractTenant places the runtime tenant into a shared DB pool
// owned by orgID (mirrors the shared fixtures in quota_test.go).
func bindSharedExtractTenant(t *testing.T, rt *extractConfigRuntime, orgID string) {
	t.Helper()
	ctx := context.Background()
	if _, err := rt.meta.DB().ExecContext(ctx, `UPDATE tenants SET cluster_id = '' WHERE id = ?`, rt.tenantID); err != nil {
		t.Fatalf("clear tenant cluster id: %v", err)
	}
	spendingTarget := meta.MaxTiDBCloudSpendingLimit
	dbID, err := rt.meta.CreateManagedSharedDBPool(ctx, &meta.SharedDB{
		TiDBCloudOrganizationID: orgID, ProvisioningKey: make([]byte, 32),
		CloudProvider: "aws", Region: "us-east-1", MaxTenants: 100, SpendingLimit: &spendingTarget,
	})
	if err != nil {
		t.Fatalf("CreateManagedSharedDBPool: %v", err)
	}
	fsID, err := rt.meta.ResolveFsID(ctx, rt.tenantID)
	if err != nil {
		t.Fatalf("ResolveFsID: %v", err)
	}
	if err := rt.meta.UpsertTenantPlacement(ctx, &meta.TenantPlacement{
		FsID: fsID, DbID: dbID, Placement: meta.PlacementShared, SchemaShape: meta.SchemaShapeShared,
	}); err != nil {
		t.Fatalf("UpsertTenantPlacement: %v", err)
	}
	if err := rt.meta.IncrSharedDBTenantCount(ctx, dbID, 1); err != nil {
		t.Fatalf("IncrSharedDBTenantCount: %v", err)
	}
}

func legacyExtractHeaders() map[string]string {
	return map[string]string{quotaPublicKeyHeader: "public-1", quotaPrivateKeyHeader: "private-1"}
}

func doExtractConfigRequestE(ts *httptest.Server, method, tenantID, mediaType, body string, headers map[string]string) (*http.Response, error) {
	var reader io.Reader
	if body != "" {
		reader = strings.NewReader(body)
	}
	req, err := http.NewRequest(method, ts.URL+"/v1/admin/tenants/"+tenantID+"/extract-config/"+mediaType, reader)
	if err != nil {
		return nil, err
	}
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	return http.DefaultClient.Do(req)
}

func doExtractConfigRequest(t *testing.T, ts *httptest.Server, method, tenantID, mediaType, body string, headers map[string]string) *http.Response {
	t.Helper()
	resp, err := doExtractConfigRequestE(ts, method, tenantID, mediaType, body, headers)
	if err != nil {
		t.Fatal(err)
	}
	return resp
}

func decodeExtractConfigBody(t *testing.T, resp *http.Response) map[string]any {
	t.Helper()
	defer func() { _ = resp.Body.Close() }()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}
	var out map[string]any
	if err := json.Unmarshal(raw, &out); err != nil {
		t.Fatalf("decode body %q: %v", raw, err)
	}
	return out
}

// seedExtractConfigRow persists a config row directly; enabled rows get the
// given provider fields (api key encrypted through the pool), off-markers get
// all provider fields NULL.
func seedExtractConfigRow(t *testing.T, rt *extractConfigRuntime, enabled bool, apiBase, apiKey, model, prompt string) {
	t.Helper()
	cfg := &meta.TenantExtractConfig{
		TenantID: rt.tenantID, MediaType: meta.ExtractMediaTypeImage, Enabled: enabled,
	}
	if enabled {
		cipher, err := rt.pool.Encrypt(context.Background(), []byte(apiKey))
		if err != nil {
			t.Fatal(err)
		}
		cfg.APIBase = apiBase
		cfg.APIKeyCipher = cipher
		cfg.Model = model
		cfg.Prompt = prompt
	}
	if err := rt.meta.UpsertTenantExtractConfig(context.Background(), cfg); err != nil {
		t.Fatal(err)
	}
}

func getPersistedExtractConfig(t *testing.T, rt *extractConfigRuntime) *meta.TenantExtractConfig {
	t.Helper()
	row, err := rt.meta.GetTenantExtractConfig(context.Background(), rt.tenantID, meta.ExtractMediaTypeImage)
	if err != nil {
		t.Fatalf("GetTenantExtractConfig: %v", err)
	}
	return row
}

func decryptExtractKey(t *testing.T, rt *extractConfigRuntime, cipher []byte) string {
	t.Helper()
	plain, err := rt.pool.Decrypt(context.Background(), cipher)
	if err != nil {
		t.Fatal(err)
	}
	return string(plain)
}

func TestMaskAPIKey(t *testing.T) {
	tests := []struct {
		key  string
		want string
	}{
		{"", "********"},
		{"a", "********"},
		{"abcd", "********"},
		{"abcde", "abcd********"},
		{"sk-1234567890", "sk-1********"},
	}
	for _, tt := range tests {
		if got := maskAPIKey(tt.key); got != tt.want {
			t.Errorf("maskAPIKey(%q) = %q, want %q", tt.key, got, tt.want)
		}
	}
}

func TestInnerControlPlaneRouteExtractConfigMappings(t *testing.T) {
	tests := []struct {
		method   string
		path     string
		resource string
	}{
		{http.MethodGet, "/v1/admin/tenants/tenant-1/extract-config/image", "/drive9/orgs/{org_id}/extract-config/view"},
		{http.MethodGet, "/v1/admin/tenants/tenant-1/extract-config/text", "/drive9/orgs/{org_id}/extract-config/view"},
		{http.MethodPut, "/v1/admin/tenants/tenant-1/extract-config/image", "/drive9/orgs/{org_id}/extract-config/modify"},
		{http.MethodPut, "/v1/admin/tenants/tenant-1/extract-config/image/", "/drive9/orgs/{org_id}/extract-config/modify"},
	}
	for _, tt := range tests {
		got, ok := matchInnerControlPlaneRoute(tt.method, tt.path)
		if !ok || got != tt.resource {
			t.Fatalf("matchInnerControlPlaneRoute(%s, %s) = (%q, %t), want %q", tt.method, tt.path, got, ok, tt.resource)
		}
	}
	for _, near := range []struct{ method, path string }{
		{http.MethodPost, "/v1/admin/tenants/tenant-1/extract-config/image"},
		{http.MethodDelete, "/v1/admin/tenants/tenant-1/extract-config/image"},
		{http.MethodGet, "/v1/admin/tenants/tenant-1/extract-config"},
		{http.MethodGet, "/v1/admin/tenants/tenant-1/extract-config/image/extra"},
	} {
		if resource, ok := matchInnerControlPlaneRoute(near.method, near.path); ok {
			t.Fatalf("matchInnerControlPlaneRoute(%s, %s) unexpectedly matched %q", near.method, near.path, resource)
		}
	}
}

func TestAdminExtractConfigGetStates(t *testing.T) {
	t.Run("absent row without default reports none", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, "image", "", legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["enabled"] != false || out["source"] != "none" {
			t.Fatalf("response = %v", out)
		}
		if _, ok := out["api_base"]; ok {
			t.Fatalf("response unexpectedly carries api_base: %v", out)
		}
	})

	t.Run("absent row with provider default reports masked default", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		rt.server.ConfigureExtractResolver(rt.meta, rt.pool, defaultTestRuntime(), &ExtractDefaultProvider{
			APIBase: "https://default.example.com",
			APIKey:  "default-key-123",
			Model:   "default-model",
			Prompt:  "default prompt",
		}, 0)
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, "image", "", legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["enabled"] != true || out["source"] != "default" {
			t.Fatalf("response = %v", out)
		}
		if out["api_base"] != "https://default.example.com" || out["model"] != "default-model" || out["prompt"] != "default prompt" {
			t.Fatalf("response = %v", out)
		}
		if out["api_key"] != maskAPIKey("default-key-123") {
			t.Fatalf("api_key = %v, want masked", out["api_key"])
		}
	})

	t.Run("image provider default does not leak into audio or video", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		rt.server.ConfigureExtractResolver(rt.meta, rt.pool, defaultTestRuntime(), &ExtractDefaultProvider{
			APIBase: "https://default.example.com",
			APIKey:  "default-key-123",
			Model:   "default-model",
			Prompt:  "default prompt",
		})
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		for _, mediaType := range []string{meta.ExtractMediaTypeAudio, meta.ExtractMediaTypeVideo} {
			resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, mediaType, "", legacyExtractHeaders())
			if resp.StatusCode != http.StatusOK {
				t.Fatalf("%s status = %d, want 200", mediaType, resp.StatusCode)
			}
			out := decodeExtractConfigBody(t, resp)
			if out["enabled"] != false || out["source"] != "none" {
				t.Fatalf("%s response = %v", mediaType, out)
			}
			for _, field := range []string{"api_base", "api_key", "model", "prompt"} {
				if _, ok := out[field]; ok {
					t.Fatalf("%s response unexpectedly carries image %s: %v", mediaType, field, out)
				}
			}
		}
	})

	t.Run("audio and video runtime defaults omit image provider fields", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		rt.server.ConfigureExtractResolverDefaults(rt.meta, rt.pool, ExtractResolverDefaults{
			Image: defaultTestRuntime(),
			Audio: &backend.AudioExtractRuntime{Extractor: stubAudioTextExtractor{}},
			Video: &backend.VideoExtractRuntime{Extractor: stubVideoTextExtractor{}},
		}, &ExtractDefaultProvider{
			APIBase: "https://default.example.com",
			APIKey:  "default-key-123",
			Model:   "default-model",
			Prompt:  "default prompt",
		})
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		for _, mediaType := range []string{meta.ExtractMediaTypeAudio, meta.ExtractMediaTypeVideo} {
			resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, mediaType, "", legacyExtractHeaders())
			if resp.StatusCode != http.StatusOK {
				t.Fatalf("%s status = %d, want 200", mediaType, resp.StatusCode)
			}
			out := decodeExtractConfigBody(t, resp)
			if out["enabled"] != true || out["source"] != "default" {
				t.Fatalf("%s response = %v", mediaType, out)
			}
			for _, field := range []string{"api_base", "api_key", "model", "prompt"} {
				if _, ok := out[field]; ok {
					t.Fatalf("%s default response unexpectedly carries %s: %v", mediaType, field, out)
				}
			}
		}
	})

	t.Run("audio runtime default reports effective protocol", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		rt.server.ConfigureExtractResolverDefaults(rt.meta, rt.pool, ExtractResolverDefaults{
			Audio:         &backend.AudioExtractRuntime{Extractor: stubAudioTextExtractor{}},
			AudioProtocol: backend.MediaProviderProtocolQwenASR,
		}, nil)
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, meta.ExtractMediaTypeAudio, "", legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["enabled"] != true || out["source"] != "default" || out["protocol"] != backend.MediaProviderProtocolQwenASR {
			t.Fatalf("response = %v", out)
		}
	})

	t.Run("absent row with basic-only default reports default without provider fields", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		rt.server.SetExtractConfigResolver(newExtractConfigResolver(rt.meta, rt.pool, defaultTestRuntime(), time.Minute))
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, "image", "", legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["enabled"] != true || out["source"] != "default" {
			t.Fatalf("response = %v", out)
		}
		for _, field := range []string{"api_base", "api_key", "model", "prompt"} {
			if _, ok := out[field]; ok {
				t.Fatalf("basic default response unexpectedly carries %s: %v", field, out)
			}
		}
	})

	t.Run("custom enabled row reports masked custom config", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		seedExtractConfigRow(t, rt, true, "https://tenant.example.com", "tenant-secret-key", "tenant-model", "tenant prompt")
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, "image", "", legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["enabled"] != true || out["source"] != "custom" {
			t.Fatalf("response = %v", out)
		}
		if out["api_base"] != "https://tenant.example.com" || out["model"] != "tenant-model" || out["prompt"] != "tenant prompt" {
			t.Fatalf("response = %v", out)
		}
		if out["api_key"] != maskAPIKey("tenant-secret-key") {
			t.Fatalf("api_key = %v, want masked", out["api_key"])
		}
		if out["updated_at"] == "" || out["updated_at"] == nil {
			t.Fatalf("updated_at missing: %v", out)
		}
	})

	t.Run("off-marker row reports custom disabled", func(t *testing.T) {
		rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
		seedExtractConfigRow(t, rt, false, "", "", "", "")
		ts := httptest.NewServer(rt.server)
		defer ts.Close()
		resp := doExtractConfigRequest(t, ts, http.MethodGet, rt.tenantID, "image", "", legacyExtractHeaders())
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["enabled"] != false || out["source"] != "custom" {
			t.Fatalf("response = %v", out)
		}
		for _, field := range []string{"api_base", "api_key", "model", "prompt"} {
			if _, ok := out[field]; ok {
				t.Fatalf("off-marker response unexpectedly carries %s: %v", field, out)
			}
		}
	})
}
