package server

import (
	"context"
	"database/sql"
	"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/tenant"
)

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

	resetRow := func(t *testing.T) {
		t.Helper()
		if err := rt.meta.DeleteTenantExtractConfigs(ctx, rt.tenantID); err != nil {
			t.Fatal(err)
		}
	}
	put := func(t *testing.T, body string) *http.Response {
		t.Helper()
		return doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image", body, legacyExtractHeaders())
	}
	fullTrio := `{"enabled":true,"api_base":"https://api.example.com","api_key":"sk-live-key","model":"model-1"}`

	t.Run("absent/enabled omitted", func(t *testing.T) {
		resetRow(t)
		resp := put(t, `{}`)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if !strings.Contains(fmt.Sprint(out["error"]), "enabled is required") {
			t.Fatalf("error = %v", out)
		}
	})

	t.Run("absent/enabled false creates off-marker", func(t *testing.T) {
		resetRow(t)
		resp := put(t, `{"enabled":false}`)
		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)
		}
		row := getPersistedExtractConfig(t, rt)
		if row.Enabled || row.APIBase != "" || row.APIKeyCipher != nil || row.Model != "" || row.Prompt != "" {
			t.Fatalf("off-marker row = %+v", row)
		}
	})

	t.Run("absent/enabled true without trio", func(t *testing.T) {
		resetRow(t)
		for _, body := range []string{
			`{"enabled":true}`,
			`{"enabled":true,"api_base":"https://api.example.com","api_key":"sk-live-key"}`,
		} {
			resp := put(t, body)
			if resp.StatusCode != http.StatusBadRequest {
				t.Fatalf("body %s: status = %d, want 400", body, resp.StatusCode)
			}
			_ = decodeExtractConfigBody(t, resp)
		}
		if _, err := rt.meta.GetTenantExtractConfig(ctx, rt.tenantID, meta.ExtractMediaTypeImage); !errors.Is(err, meta.ErrNotFound) {
			t.Fatalf("row should stay absent, err = %v", err)
		}
	})

	t.Run("absent/enabled true with full trio", func(t *testing.T) {
		resetRow(t)
		resp := put(t, fullTrio)
		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_key"] != maskAPIKey("sk-live-key") {
			t.Fatalf("api_key = %v, want masked", out["api_key"])
		}
		row := getPersistedExtractConfig(t, rt)
		if !row.Enabled || row.APIBase != "https://api.example.com" || row.Model != "model-1" || row.Prompt != "" {
			t.Fatalf("row = %+v", row)
		}
		if got := decryptExtractKey(t, rt, row.APIKeyCipher); got != "sk-live-key" {
			t.Fatalf("decrypted key = %q", got)
		}
	})

	t.Run("provided protocol is normalized before persistence", func(t *testing.T) {
		resetRow(t)
		resp := put(t, `{"enabled":true,"api_base":"https://api.example.com","api_key":"sk-live-key","model":"model-1","protocol":" OpenAI "}`)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200; body=%v", resp.StatusCode, decodeExtractConfigBody(t, resp))
		}
		out := decodeExtractConfigBody(t, resp)
		if out["protocol"] != backend.MediaProviderProtocolOpenAI {
			t.Fatalf("response protocol = %v, want %q", out["protocol"], backend.MediaProviderProtocolOpenAI)
		}
		row := getPersistedExtractConfig(t, rt)
		if row.Protocol != backend.MediaProviderProtocolOpenAI {
			t.Fatalf("persisted protocol = %q, want %q", row.Protocol, backend.MediaProviderProtocolOpenAI)
		}
	})

	t.Run("off-marker/enabled omitted", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, false, "", "", "", "")
		resp := put(t, `{}`)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		_ = decodeExtractConfigBody(t, resp)
	})

	t.Run("off-marker/enabled false is a no-op", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, false, "", "", "", "")
		before := getPersistedExtractConfig(t, rt)
		resp := put(t, `{"enabled":false}`)
		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)
		}
		after := getPersistedExtractConfig(t, rt)
		if !after.UpdatedAt.Equal(before.UpdatedAt) {
			t.Fatalf("no-op updated the row: before %v after %v", before.UpdatedAt, after.UpdatedAt)
		}
	})

	t.Run("off-marker/enabled true without trio", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, false, "", "", "", "")
		resp := put(t, `{"enabled":true,"api_base":"https://api.example.com"}`)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		_ = decodeExtractConfigBody(t, resp)
	})

	t.Run("off-marker/enabled true with trio enables", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, false, "", "", "", "")
		resp := put(t, fullTrio)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		_ = decodeExtractConfigBody(t, resp)
		row := getPersistedExtractConfig(t, rt)
		if !row.Enabled || row.APIBase != "https://api.example.com" || row.Model != "model-1" {
			t.Fatalf("row = %+v", row)
		}
	})

	t.Run("enabled/enabled omitted keeps provider fields", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "prompt-a")
		resp := put(t, `{"prompt":"prompt-b"}`)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["model"] != "model-a" || out["api_base"] != "https://a.example.com" || out["prompt"] != "prompt-b" {
			t.Fatalf("response = %v", out)
		}
		if out["api_key"] != maskAPIKey("key-one-1") {
			t.Fatalf("api_key = %v, want masked kept key", out["api_key"])
		}
		row := getPersistedExtractConfig(t, rt)
		if got := decryptExtractKey(t, rt, row.APIKeyCipher); got != "key-one-1" {
			t.Fatalf("decrypted key = %q, want kept key", got)
		}
	})

	t.Run("enabled/enabled false becomes off-marker with cleared fields", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "prompt-a")
		resp := put(t, `{"enabled":false}`)
		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)
		}
		row := getPersistedExtractConfig(t, rt)
		if row.Enabled || row.APIBase != "" || row.APIKeyCipher != nil || row.Model != "" || row.Prompt != "" {
			t.Fatalf("off-marker row = %+v", row)
		}
	})

	t.Run("enabled/enabled true replaces provided fields", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "prompt-a")
		resp := put(t, `{"enabled":true,"api_base":"https://b.example.com","api_key":"key-two-2","model":"model-b","prompt":"prompt-b"}`)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if out["prompt"] != "prompt-b" || out["api_key"] != maskAPIKey("key-two-2") ||
			out["api_base"] != "https://b.example.com" || out["model"] != "model-b" {
			t.Fatalf("response = %v", out)
		}
		row := getPersistedExtractConfig(t, rt)
		if got := decryptExtractKey(t, rt, row.APIKeyCipher); got != "key-two-2" {
			t.Fatalf("decrypted key = %q, want replaced key", got)
		}
	})

	t.Run("enabled/empty prompt clears to NULL", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "prompt-a")
		resp := put(t, `{"prompt":""}`)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("status = %d, want 200", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if _, ok := out["prompt"]; ok {
			t.Fatalf("prompt should be omitted after clearing: %v", out)
		}
		row := getPersistedExtractConfig(t, rt)
		if row.Prompt != "" {
			t.Fatalf("prompt = %q, want cleared", row.Prompt)
		}
		var promptNull bool
		if err := rt.meta.DB().QueryRowContext(ctx,
			`SELECT prompt IS NULL FROM tenant_extract_configs WHERE tenant_id = ? AND media_type = 'image'`,
			rt.tenantID).Scan(&promptNull); err != nil {
			t.Fatal(err)
		}
		if !promptNull {
			t.Fatal("prompt should be persisted as NULL")
		}
	})

	t.Run("enabled/empty trio strings rejected", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "")
		for _, body := range []string{`{"api_base":""}`, `{"model":""}`, `{"api_key":""}`} {
			resp := put(t, body)
			if resp.StatusCode != http.StatusBadRequest {
				t.Fatalf("body %s: status = %d, want 400", body, resp.StatusCode)
			}
			_ = decodeExtractConfigBody(t, resp)
		}
		row := getPersistedExtractConfig(t, rt)
		if row.APIBase != "https://a.example.com" || row.Model != "model-a" {
			t.Fatalf("rejected writes changed the row: %+v", row)
		}
	})

	t.Run("masked api_key echo rejected", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "")
		resp := put(t, `{"api_key":"key-****"}`)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		out := decodeExtractConfigBody(t, resp)
		if !strings.Contains(fmt.Sprint(out["error"]), "api_key") {
			t.Fatalf("error = %v", out)
		}
	})

	t.Run("explicit null fields rejected", func(t *testing.T) {
		resetRow(t)
		seedExtractConfigRow(t, rt, true, "https://a.example.com", "key-one-1", "model-a", "")
		for _, body := range []string{
			`{"enabled":null}`,
			`{"api_base":null}`,
			`{"api_key":null}`,
			`{"model":null}`,
			`{"prompt":null}`,
		} {
			resp := put(t, body)
			if resp.StatusCode != http.StatusBadRequest {
				t.Fatalf("body %s: status = %d, want 400", body, resp.StatusCode)
			}
			out := decodeExtractConfigBody(t, resp)
			if !strings.Contains(fmt.Sprint(out["error"]), "must not be null") {
				t.Fatalf("body %s: error = %v", body, out)
			}
		}
	})

	t.Run("non-https api_base rejected", func(t *testing.T) {
		resetRow(t)
		resp := put(t, `{"enabled":true,"api_base":"http://api.example.com","api_key":"sk-live-key","model":"model-1"}`)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		_ = decodeExtractConfigBody(t, resp)
	})

	t.Run("oversize prompt rejected", func(t *testing.T) {
		resetRow(t)
		body := `{"enabled":false,"prompt":"` + strings.Repeat("p", maxExtractConfigPromptBytes+1) + `"}`
		resp := put(t, body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		_ = decodeExtractConfigBody(t, resp)
	})

	t.Run("unknown field rejected", func(t *testing.T) {
		resetRow(t)
		resp := put(t, `{"enabled":false,"bogus":1}`)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("status = %d, want 400", resp.StatusCode)
		}
		_ = decodeExtractConfigBody(t, resp)
	})
}

func TestAudioExtractConfigProtocolChangeRequiresProviderTrio(t *testing.T) {
	req, err := decodeExtractConfigPutRequest(httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/", strings.NewReader(`{"protocol":"qwen-asr"}`)))
	if err != nil {
		t.Fatal(err)
	}
	if err := validateExtractConfigProtocol(meta.ExtractMediaTypeAudio, req); err == nil || !strings.Contains(err.Error(), "api_base, api_key and model") {
		t.Fatalf("validation error=%v, want provider trio requirement", err)
	}
}

func TestAudioExtractConfigProviderTrioLeavesOmittedProtocolForStateMerge(t *testing.T) {
	req, err := decodeExtractConfigPutRequest(httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/", strings.NewReader(
		`{"api_base":"https://api.example.com","api_key":"key","model":"model"}`)))
	if err != nil {
		t.Fatal(err)
	}
	if err := validateExtractConfigProtocol(meta.ExtractMediaTypeAudio, req); err != nil {
		t.Fatal(err)
	}
	if req.protocol != nil {
		t.Fatalf("protocol=%v, want omitted for existing-row state merge", req.protocol)
	}
}

func TestApplyAudioExtractConfigPreservesExistingProtocolWhenOmitted(t *testing.T) {
	rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
	ctx := context.Background()
	cipher, err := rt.pool.Encrypt(ctx, []byte("old-key"))
	if err != nil {
		t.Fatal(err)
	}
	if err := rt.meta.UpsertTenantExtractConfig(ctx, &meta.TenantExtractConfig{
		TenantID: rt.tenantID, MediaType: meta.ExtractMediaTypeAudio, Enabled: true,
		APIBase: "https://old.example.com", APIKeyCipher: cipher, Model: "old-model",
		Protocol: backend.MediaProviderProtocolQwenASR,
	}); err != nil {
		t.Fatal(err)
	}
	req, err := decodeExtractConfigPutRequest(httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/", strings.NewReader(
		`{"api_base":"https://new.example.com","api_key":"new-key","model":"new-model"}`)))
	if err != nil {
		t.Fatal(err)
	}
	if err := validateExtractConfigProtocol(meta.ExtractMediaTypeAudio, req); err != nil {
		t.Fatal(err)
	}
	var final *meta.TenantExtractConfig
	if err := rt.meta.InTx(ctx, func(tx *sql.Tx) error {
		var err error
		final, err = rt.server.applyExtractConfigPut(ctx, tx, rt.tenantID, meta.ExtractMediaTypeAudio, req)
		return err
	}); err != nil {
		t.Fatal(err)
	}
	if final.Protocol != backend.MediaProviderProtocolQwenASR {
		t.Fatalf("protocol=%q, want preserved %q", final.Protocol, backend.MediaProviderProtocolQwenASR)
	}
}
