//go:build failpoint

package server

import (
	"context"
	"database/sql"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/pingcap/failpoint"

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

const beforeTenantExtractConfigLockFailpoint = "github.com/mem9-ai/drive9/pkg/meta/beforeTenantExtractConfigLock"

// A terminal tenant delete committing between the PUT's authorization
// precheck and its write transaction must win: the PUT fails with 404 and no
// config row can exist after the committed delete.
func TestAdminExtractConfigPutVsTerminalDelete(t *testing.T) {
	rt := newExtractConfigRuntime(t, tenant.ProviderTiDBCloudNative, "org-1", nil)
	ts := httptest.NewServer(rt.server)
	defer ts.Close()
	ctx := context.Background()

	if err := failpoint.EnableCall(beforeTenantExtractConfigLockFailpoint, func(tenantID string) {
		if tenantID != rt.tenantID {
			return
		}
		if err := rt.meta.MarkTenantDeleted(context.Background(), tenantID); err != nil {
			t.Errorf("mark tenant deleted: %v", err)
		}
	}); err != nil {
		t.Fatalf("enable failpoint: %v", err)
	}
	t.Cleanup(func() { _ = failpoint.Disable(beforeTenantExtractConfigLockFailpoint) })

	resp := doExtractConfigRequest(t, ts, http.MethodPut, rt.tenantID, "image",
		`{"enabled":true,"api_base":"https://api.example.com","api_key":"sk-live-key","model":"model-1"}`, legacyExtractHeaders())
	if resp.StatusCode != http.StatusNotFound {
		t.Fatalf("PUT status = %d, want 404", resp.StatusCode)
	}
	_ = decodeExtractConfigBody(t, resp)
	if _, err := rt.meta.GetTenantExtractConfig(ctx, rt.tenantID, meta.ExtractMediaTypeImage); !errors.Is(err, meta.ErrNotFound) {
		t.Fatalf("config row after committed terminal delete = %v, want %v", err, meta.ErrNotFound)
	}
}

func TestAudioExtractConfigRejectsProtocolChangeAfterPreflight(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)
	}
	row := &meta.TenantExtractConfig{
		TenantID: rt.tenantID, MediaType: meta.ExtractMediaTypeAudio, Enabled: true,
		APIBase: "https://old.example.com", APIKeyCipher: cipher, Model: "old-model",
		Protocol: backend.MediaProviderProtocolQwenASR,
	}
	if err := rt.meta.UpsertTenantExtractConfig(ctx, row); 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)
	}
	validated := backend.MediaProviderProtocolQwenASR
	req.validatedProtocol = &validated
	if err := failpoint.EnableCall(beforeTenantExtractConfigLockFailpoint, func(tenantID string) {
		if tenantID != rt.tenantID {
			return
		}
		changed := *row
		changed.Protocol = backend.MediaProviderProtocolOpenAI
		if err := rt.meta.UpsertTenantExtractConfig(context.Background(), &changed); err != nil {
			t.Errorf("change protocol between preflight and lock: %v", err)
		}
	}); err != nil {
		t.Fatalf("enable failpoint: %v", err)
	}
	t.Cleanup(func() { _ = failpoint.Disable(beforeTenantExtractConfigLockFailpoint) })

	err = rt.meta.InTx(ctx, func(tx *sql.Tx) error {
		_, applyErr := rt.server.applyExtractConfigPut(ctx, tx, rt.tenantID, meta.ExtractMediaTypeAudio, req)
		return applyErr
	})
	if !errors.Is(err, errExtractConfigProtocolChanged) {
		t.Fatalf("apply error=%v, want protocol changed conflict", err)
	}
	got, err := rt.meta.GetTenantExtractConfig(ctx, rt.tenantID, meta.ExtractMediaTypeAudio)
	if err != nil {
		t.Fatal(err)
	}
	if got.Protocol != backend.MediaProviderProtocolOpenAI || got.APIBase != row.APIBase {
		t.Fatalf("persisted row=%+v, want concurrent protocol change without unvalidated provider update", got)
	}
}
