package service

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"oci-portal/internal/crypto"
	"oci-portal/internal/model"
	"oci-portal/internal/oci"
)

type invalidatingClient struct {
	*fakeClient
	invalidated []string
}

type blockingClient struct {
	*fakeClient
	started chan struct{}
	release chan struct{}
}

func (c *blockingClient) ValidateKey(context.Context, oci.Credentials) (oci.TenancyInfo, error) {
	c.started <- struct{}{}
	<-c.release
	return oci.TenancyInfo{Name: "target"}, nil
}

func (c *invalidatingClient) InvalidateTenancy(id string) {
	c.invalidated = append(c.invalidated, id)
}

func newTenantDeleteEnv(t *testing.T, client oci.Client) (*OciConfigService, *TaskService, *gorm.DB) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	if err != nil {
		t.Fatalf("open database: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("database handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	migrateTenantDeleteModels(t, db)
	cipher, err := crypto.NewCipher("test-data-key")
	if err != nil {
		t.Fatalf("new cipher: %v", err)
	}
	configs := NewOciConfigService(db, cipher, client)
	tasks := NewTaskService(db, configs, nil, nil)
	configs.SetTenantCleanupDeps(tasks)
	return configs, tasks, db
}

func migrateTenantDeleteModels(t *testing.T, db *gorm.DB) {
	t.Helper()
	err := db.AutoMigrate(
		&model.OciConfig{}, &model.Task{}, &model.TaskLog{}, &model.Setting{},
		&model.CheckSnapshot{}, &model.CostSnapshot{}, &model.RegionCache{}, &model.CompartmentCache{},
		&model.LogEvent{},
		&model.AiChannel{}, &model.AiModelCache{}, &model.AiCallLog{}, &model.AiContentLog{},
		&model.Proxy{}, &model.AiKey{}, &model.SystemLog{},
	)
	if err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
}

func TestPlanTenantTask(t *testing.T) {
	for _, tt := range tenantTaskCases() {
		t.Run(tt.name, func(t *testing.T) {
			action, ok, err := planTenantTask(tt.task, 1)
			if (err != nil) != tt.wantErr {
				t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
			}
			if ok != tt.wantOK || action.deleteTask != tt.wantDelete {
				t.Errorf("result = (ok=%v, delete=%v), want (%v, %v)", ok, action.deleteTask, tt.wantOK, tt.wantDelete)
			}
			if action.payload != tt.wantPayload {
				t.Errorf("payload = %q, want %q", action.payload, tt.wantPayload)
			}
		})
	}
}

type tenantTaskCase struct {
	name        string
	task        model.Task
	wantOK      bool
	wantDelete  bool
	wantPayload string
	wantErr     bool
}

func tenantTaskCases() []tenantTaskCase {
	return []tenantTaskCase{
		{name: "抢机命中", task: taskOf(model.TaskTypeSnatch, `{"ociConfigId":1}`), wantOK: true, wantDelete: true},
		{name: "抢机未命中", task: taskOf(model.TaskTypeSnatch, `{"ociConfigId":2}`)},
		{name: "测活全局", task: taskOf(model.TaskTypeHealthCheck, `{"ociConfigIds":[]}`), wantOK: true, wantPayload: `{"ociConfigIds":[]}`},
		{name: "测活单租户", task: taskOf(model.TaskTypeHealthCheck, `{"ociConfigIds":[1]}`), wantOK: true, wantDelete: true},
		{name: "测活多租户", task: taskOf(model.TaskTypeHealthCheck, `{"ociConfigIds":[1,2]}`), wantOK: true, wantPayload: `{"ociConfigIds":[2]}`},
		{name: "成本去重命中", task: taskOf(model.TaskTypeCost, `{"ociConfigIds":[1,1,2]}`), wantOK: true, wantPayload: `{"ociConfigIds":[2]}`},
		{name: "成本未命中", task: taskOf(model.TaskTypeCost, `{"ociConfigIds":[2]}`)},
		// 坏 payload 记警告跳过(保留原任务),不再 fail-closed 阻断整个租户删除
		{name: "非法 JSON 跳过", task: taskOf(model.TaskTypeCost, `{`)},
		{name: "抢机坏 payload 跳过", task: taskOf(model.TaskTypeSnatch, `not-json`)},
		{name: "抢机空 payload 跳过", task: taskOf(model.TaskTypeSnatch, ``)},
	}
}

func taskOf(taskType, payload string) model.Task {
	return model.Task{ID: 10, Type: taskType, Payload: payload}
}

func TestDeleteTenantCleansRelatedRows(t *testing.T) {
	client := &invalidatingClient{fakeClient: &fakeClient{}}
	configs, _, db := newTenantDeleteEnv(t, client)
	target, other := seedDeleteTenants(t, db)
	seedTenantSnapshots(t, db, target.ID, other.ID)
	seedTenantEvents(t, db, target.ID, other.ID)
	seedTenantAI(t, db, target.ID, other.ID)
	seedRetainedGlobals(t, db)
	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("delete tenant: %v", err)
	}
	assertTenantRowsGone(t, db, target.ID)
	assertOtherTenantRowsRemain(t, db, other.ID)
	assertRetainedGlobals(t, db)
	if len(client.invalidated) != 1 || client.invalidated[0] != target.TenancyOCID {
		t.Errorf("invalidated = %v, want [%s]", client.invalidated, target.TenancyOCID)
	}
}

func seedDeleteTenants(t *testing.T, db *gorm.DB) (model.OciConfig, model.OciConfig) {
	t.Helper()
	target := model.OciConfig{Alias: "target", TenancyOCID: "ocid1.tenancy.target"}
	other := model.OciConfig{Alias: "other", TenancyOCID: "ocid1.tenancy.other"}
	mustCreate(t, db, &target)
	mustCreate(t, db, &other)
	return target, other
}

func seedTenantSnapshots(t *testing.T, db *gorm.DB, target, other uint) {
	t.Helper()
	for _, id := range []uint{target, other} {
		mustCreate(t, db, &model.CheckSnapshot{OciConfigID: id})
		mustCreate(t, db, &model.CostSnapshot{OciConfigID: id, Day: "2026-07-10"})
		mustCreate(t, db, &model.RegionCache{OciConfigID: id, Key: "PHX"})
		mustCreate(t, db, &model.CompartmentCache{OciConfigID: id, OCID: fmt.Sprintf("comp-%d", id)})
		mustCreate(t, db, &model.Setting{Key: secretKey(id), Value: fmt.Sprintf("secret-%d", id)})
	}
}

func seedTenantEvents(t *testing.T, db *gorm.DB, target, other uint) {
	t.Helper()
	targetEvent := model.LogEvent{OciConfigID: target, MessageID: "target-event"}
	otherEvent := model.LogEvent{OciConfigID: other, MessageID: "other-event"}
	for _, value := range []any{&targetEvent, &otherEvent} {
		mustCreate(t, db, value)
	}
}

func seedTenantAI(t *testing.T, db *gorm.DB, target, other uint) {
	t.Helper()
	for _, id := range []uint{target, other} {
		channel := model.AiChannel{Name: fmt.Sprintf("channel-%d", id), OciConfigID: id, Region: "us-phoenix-1"}
		mustCreate(t, db, &channel)
		mustCreate(t, db, &model.AiModelCache{ChannelID: channel.ID, ModelOcid: fmt.Sprintf("model-%d", id)})
		call := model.AiCallLog{ChannelID: channel.ID, ChannelName: channel.Name}
		mustCreate(t, db, &call)
		mustCreate(t, db, &model.AiContentLog{CallLogID: call.ID, RequestBody: "sensitive"})
	}
}

func seedRetainedGlobals(t *testing.T, db *gorm.DB) {
	t.Helper()
	mustCreate(t, db, &model.Proxy{Name: "shared", Type: "http"})
	mustCreate(t, db, &model.AiKey{Name: "global-key", KeyHash: "hash", Tail: "hash"})
	mustCreate(t, db, &model.SystemLog{Method: "DELETE", Path: "/api/v1/oci-configs/1"})
	mustCreate(t, db, &model.Setting{Key: "notify_channels", Value: "[]"})
}

func assertTenantRowsGone(t *testing.T, db *gorm.DB, id uint) {
	t.Helper()
	rows := []any{
		&model.OciConfig{}, &model.CheckSnapshot{}, &model.CostSnapshot{},
		&model.RegionCache{}, &model.CompartmentCache{}, &model.LogEvent{},
		&model.AiChannel{},
	}
	for _, value := range rows {
		column := "oci_config_id"
		if _, ok := value.(*model.OciConfig); ok {
			column = "id"
		}
		assertCount(t, db, value, column+" = ?", []any{id}, 0)
	}
	assertCount(t, db, &model.Setting{}, "key = ?", []any{secretKey(id)}, 0)
	assertCount(t, db, &model.AiModelCache{}, "", nil, 1)
	assertCount(t, db, &model.AiCallLog{}, "", nil, 1)
	assertCount(t, db, &model.AiContentLog{}, "", nil, 1)
}

func assertOtherTenantRowsRemain(t *testing.T, db *gorm.DB, id uint) {
	t.Helper()
	for _, value := range []any{
		&model.OciConfig{}, &model.CheckSnapshot{}, &model.CostSnapshot{},
		&model.RegionCache{}, &model.CompartmentCache{}, &model.LogEvent{},
		&model.AiChannel{},
	} {
		column := "oci_config_id"
		if _, ok := value.(*model.OciConfig); ok {
			column = "id"
		}
		assertCount(t, db, value, column+" = ?", []any{id}, 1)
	}
	assertCount(t, db, &model.Setting{}, "key = ?", []any{secretKey(id)}, 1)
	assertRemainingIndirectRows(t, db, id)
}

func assertRemainingIndirectRows(t *testing.T, db *gorm.DB, otherID uint) {
	t.Helper()
	var channel model.AiChannel
	if err := db.Where("oci_config_id = ?", otherID).First(&channel).Error; err != nil {
		t.Fatalf("load other AI channel: %v", err)
	}
	assertCount(t, db, &model.AiModelCache{}, "channel_id = ?", []any{channel.ID}, 1)
	assertCount(t, db, &model.AiCallLog{}, "channel_id = ?", []any{channel.ID}, 1)
	var call model.AiCallLog
	if err := db.Where("channel_id = ?", channel.ID).First(&call).Error; err != nil {
		t.Fatalf("load other AI call: %v", err)
	}
	assertCount(t, db, &model.AiContentLog{}, "call_log_id = ?", []any{call.ID}, 1)
}

func assertRetainedGlobals(t *testing.T, db *gorm.DB) {
	t.Helper()
	assertCount(t, db, &model.Proxy{}, "", nil, 1)
	assertCount(t, db, &model.AiKey{}, "", nil, 1)
	assertCount(t, db, &model.SystemLog{}, "", nil, 1)
	assertCount(t, db, &model.Setting{}, "key = ?", []any{"notify_channels"}, 1)
}

func TestDeleteTenantRewritesTasksAndCron(t *testing.T) {
	configs, tasks, db := newTenantDeleteEnv(t, &fakeClient{})
	target, other := seedDeleteTenants(t, db)
	created := seedTenantTasks(t, tasks, target.ID, other.ID)
	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("delete tenant: %v", err)
	}
	assertDeletedTasks(t, db, tasks, created[:2])
	assertRewrittenTasks(t, db, created[2:])
}

func TestDeleteTenantReconcilesAiProbe(t *testing.T) {
	for _, keepOther := range []bool{false, true} {
		name := fmt.Sprintf("keepOther=%v", keepOther)
		t.Run(name, func(t *testing.T) {
			configs, tasks, db := newTenantDeleteEnv(t, &fakeClient{})
			target, other := seedDeleteTenants(t, db)
			seedProbeChannels(t, db, target.ID, other.ID, keepOther)
			tasks.SyncAiProbeTask(context.Background())
			probe := loadAiProbe(t, db)
			seedTaskHistory(t, db, probe.ID)
			if err := configs.Delete(context.Background(), target.ID); err != nil {
				t.Fatalf("delete tenant: %v", err)
			}
			assertAiProbeResult(t, db, tasks, probe.ID, keepOther)
		})
	}
}

func seedProbeChannels(t *testing.T, db *gorm.DB, target, other uint, keepOther bool) {
	t.Helper()
	mustCreate(t, db, &model.AiChannel{Name: "target", OciConfigID: target, Region: "r1"})
	if keepOther {
		mustCreate(t, db, &model.AiChannel{Name: "other", OciConfigID: other, Region: "r1"})
	}
}

func loadAiProbe(t *testing.T, db *gorm.DB) model.Task {
	t.Helper()
	var task model.Task
	if err := db.Where("type = ?", model.TaskTypeAiProbe).First(&task).Error; err != nil {
		t.Fatalf("load AI probe: %v", err)
	}
	return task
}

func seedTaskHistory(t *testing.T, db *gorm.DB, taskID uint) {
	t.Helper()
	updates := map[string]any{"last_error": "old", "run_count": 2, "last_run_at": time.Now()}
	if err := db.Model(&model.Task{}).Where("id = ?", taskID).Updates(updates).Error; err != nil {
		t.Fatalf("seed task history: %v", err)
	}
	mustCreate(t, db, &model.TaskLog{TaskID: taskID, Message: "old"})
	mustCreate(t, db, &model.Setting{Key: deadAliasKey(taskID), Value: "old"})
}

func assertAiProbeResult(t *testing.T, db *gorm.DB, tasks *TaskService, id uint, keep bool) {
	t.Helper()
	want := int64(0)
	if keep {
		want = 1
	}
	assertCount(t, db, &model.Task{}, "id = ?", []any{id}, want)
	assertCount(t, db, &model.TaskLog{}, "task_id = ?", []any{id}, 0)
	assertCount(t, db, &model.Setting{}, "key = ?", []any{deadAliasKey(id)}, 0)
	if !keep {
		if _, ok := tasks.entries[id]; ok {
			t.Errorf("AI probe %d still scheduled", id)
		}
		return
	}
	probe := loadAiProbe(t, db)
	if probe.RunCount != 0 || probe.LastRunAt != nil || probe.LastError != "" {
		t.Errorf("AI probe history not reset: %+v", probe)
	}
}

func seedTenantTasks(t *testing.T, tasks *TaskService, target, other uint) []model.Task {
	t.Helper()
	inputs := []CreateTaskInput{
		{Name: "snatch", Type: model.TaskTypeSnatch, CronExpr: "0 0 * * *", Payload: []byte(fmt.Sprintf(`{"ociConfigId":%d,"instance":{"displayName":"vm","region":"r","availabilityDomain":"a","subnetId":"s","shape":"x","imageId":"i"}}`, target))},
		{Name: "single", Type: model.TaskTypeCost, CronExpr: "0 0 * * *", Payload: []byte(fmt.Sprintf(`{"ociConfigIds":[%d]}`, target))},
		{Name: "mixed", Type: model.TaskTypeHealthCheck, CronExpr: "0 0 * * *", Payload: []byte(fmt.Sprintf(`{"ociConfigIds":[%d,%d]}`, target, other))},
		{Name: "global", Type: model.TaskTypeCost, CronExpr: "0 0 * * *", Payload: []byte(`{"ociConfigIds":[]}`)},
		{Name: "other", Type: model.TaskTypeHealthCheck, CronExpr: "0 0 * * *", Payload: []byte(fmt.Sprintf(`{"ociConfigIds":[%d]}`, other))},
	}
	return createTasksWithHistory(t, tasks, inputs)
}

func createTasksWithHistory(t *testing.T, tasks *TaskService, inputs []CreateTaskInput) []model.Task {
	t.Helper()
	out := make([]model.Task, 0, len(inputs))
	for _, input := range inputs {
		task, err := tasks.CreateTask(context.Background(), input)
		if err != nil {
			t.Fatalf("create task %s: %v", input.Name, err)
		}
		tasks.db.Model(task).Updates(map[string]any{"last_error": "old", "run_count": 3, "last_run_at": time.Now()})
		mustCreate(t, tasks.db, &model.TaskLog{TaskID: task.ID, Message: "old"})
		mustCreate(t, tasks.db, &model.Setting{Key: deadAliasKey(task.ID), Value: `["target"]`})
		out = append(out, *task)
	}
	return out
}

func assertDeletedTasks(t *testing.T, db *gorm.DB, tasks *TaskService, deleted []model.Task) {
	t.Helper()
	for _, task := range deleted {
		assertCount(t, db, &model.Task{}, "id = ?", []any{task.ID}, 0)
		assertCount(t, db, &model.TaskLog{}, "task_id = ?", []any{task.ID}, 0)
		assertCount(t, db, &model.Setting{}, "key = ?", []any{deadAliasKey(task.ID)}, 0)
		if _, ok := tasks.entries[task.ID]; ok {
			t.Errorf("task %d still scheduled", task.ID)
		}
	}
}

func assertRewrittenTasks(t *testing.T, db *gorm.DB, tasks []model.Task) {
	t.Helper()
	wantPayload := []string{`{"ociConfigIds":[2]}`, `{"ociConfigIds":[]}`, `{"ociConfigIds":[2]}`}
	for i, original := range tasks {
		var got model.Task
		if err := db.First(&got, original.ID).Error; err != nil {
			t.Fatalf("load task %d: %v", original.ID, err)
		}
		if got.Payload != wantPayload[i] {
			t.Errorf("task %d payload = %s, want %s", got.ID, got.Payload, wantPayload[i])
		}
		wantHistory := int64(0)
		if original.Name == "other" {
			wantHistory = 1
		} else if got.RunCount != 0 || got.LastRunAt != nil || got.LastError != "" {
			t.Errorf("task %d history fields not reset: %+v", got.ID, got)
		}
		assertCount(t, db, &model.TaskLog{}, "task_id = ?", []any{got.ID}, wantHistory)
		assertCount(t, db, &model.Setting{}, "key = ?", []any{deadAliasKey(got.ID)}, wantHistory)
	}
}

func TestDeleteTenantRollback(t *testing.T) {
	configs, tasks, db := newTenantDeleteEnv(t, &fakeClient{})
	target, _ := seedDeleteTenants(t, db)
	mustCreate(t, db, &model.CheckSnapshot{OciConfigID: target.ID})
	task := createHealthTask(t, tasks, target.ID)
	seedTaskHistory(t, db, task.ID)
	registerDeleteFailure(t, db, "cost_snapshots")
	err := configs.Delete(context.Background(), target.ID)
	if err == nil || !errors.Is(err, errInjectedTenantDelete) {
		t.Fatalf("delete error = %v, want injected failure", err)
	}
	assertCount(t, db, &model.OciConfig{}, "id = ?", []any{target.ID}, 1)
	assertCount(t, db, &model.CheckSnapshot{}, "oci_config_id = ?", []any{target.ID}, 1)
	assertCount(t, db, &model.Task{}, "id = ?", []any{task.ID}, 1)
	assertCount(t, db, &model.TaskLog{}, "task_id = ?", []any{task.ID}, 1)
	assertCount(t, db, &model.Setting{}, "key = ?", []any{deadAliasKey(task.ID)}, 1)
}

func TestDeleteTenantWaitsForRunningTask(t *testing.T) {
	client := &blockingClient{fakeClient: &fakeClient{}, started: make(chan struct{}, 1), release: make(chan struct{})}
	configs, tasks, db := newTenantDeleteEnv(t, client)
	target, _ := seedDeleteTenants(t, db)
	setTenantPrivateKey(t, configs, target.ID)
	task := createHealthTask(t, tasks, target.ID)
	executed := make(chan struct{})
	go func() {
		tasks.execute(task.ID)
		close(executed)
	}()
	<-client.started
	deleted := make(chan error, 1)
	go func() { deleted <- configs.Delete(context.Background(), target.ID) }()
	assertDeleteBlocked(t, deleted)
	close(client.release)
	<-executed
	if err := <-deleted; err != nil {
		t.Fatalf("delete tenant: %v", err)
	}
	assertCount(t, db, &model.Task{}, "id = ?", []any{task.ID}, 0)
	assertCount(t, db, &model.TaskLog{}, "task_id = ?", []any{task.ID}, 0)
}

func TestVerifyDoesNotResurrectDeletedTenant(t *testing.T) {
	client := &blockingClient{fakeClient: &fakeClient{}, started: make(chan struct{}, 1), release: make(chan struct{})}
	configs, _, db := newTenantDeleteEnv(t, client)
	target, _ := seedDeleteTenants(t, db)
	setTenantPrivateKey(t, configs, target.ID)
	verified := make(chan error, 1)
	go func() {
		_, _, err := configs.Verify(context.Background(), target.ID)
		verified <- err
	}()
	<-client.started
	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("delete tenant: %v", err)
	}
	close(client.release)
	if err := <-verified; !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("verify error = %v, want record not found", err)
	}
	assertCount(t, db, &model.OciConfig{}, "id = ?", []any{target.ID}, 0)
}

func TestScopeCacheRejectsDeletedTenant(t *testing.T) {
	configs, _, db := newTenantDeleteEnv(t, &fakeClient{})
	target, _ := seedDeleteTenants(t, db)
	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("delete tenant: %v", err)
	}
	err := configs.saveRegionCache(context.Background(), target.ID,
		[]oci.RegionSubscription{{Key: "PHX", Name: "us-phoenix-1"}})
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("save region cache error = %v, want record not found", err)
	}
	err = configs.saveCompartmentCache(context.Background(), target.ID,
		[]oci.Compartment{{ID: "compartment", Name: "deleted"}})
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("save compartment cache error = %v, want record not found", err)
	}
	assertCount(t, db, &model.RegionCache{}, "oci_config_id = ?", []any{target.ID}, 0)
	assertCount(t, db, &model.CompartmentCache{}, "oci_config_id = ?", []any{target.ID}, 0)
}

func setTenantPrivateKey(t *testing.T, configs *OciConfigService, id uint) {
	t.Helper()
	encrypted, err := configs.cipher.EncryptString("private-key")
	if err != nil {
		t.Fatalf("encrypt private key: %v", err)
	}
	if err := configs.db.Model(&model.OciConfig{}).Where("id = ?", id).
		Update("private_key_enc", encrypted).Error; err != nil {
		t.Fatalf("set private key: %v", err)
	}
}

func createHealthTask(t *testing.T, tasks *TaskService, cfgID uint) *model.Task {
	t.Helper()
	payload := []byte(fmt.Sprintf(`{"ociConfigIds":[%d]}`, cfgID))
	task, err := tasks.CreateTask(context.Background(), CreateTaskInput{
		Name: "running", Type: model.TaskTypeHealthCheck,
		CronExpr: "0 0 * * *", Payload: payload,
	})
	if err != nil {
		t.Fatalf("create task: %v", err)
	}
	return task
}

func assertDeleteBlocked(t *testing.T, deleted <-chan error) {
	t.Helper()
	select {
	case err := <-deleted:
		t.Fatalf("delete returned before running task finished: %v", err)
	case <-time.After(50 * time.Millisecond):
	}
}

func TestPersistTaskRunDoesNotResurrectDeletedTask(t *testing.T) {
	_, tasks, db := newTenantDeleteEnv(t, &fakeClient{})
	task := &model.Task{Name: "stale", Type: model.TaskTypeCost, Status: model.TaskStatusActive}
	mustCreate(t, db, task)
	stale := *task
	if err := db.Delete(task).Error; err != nil {
		t.Fatalf("delete task: %v", err)
	}
	stored, err := tasks.persistTaskRun(context.Background(), &stale)
	if err != nil || stored {
		t.Fatalf("persist stale task = (%v, %v), want (false, nil)", stored, err)
	}
	assertCount(t, db, &model.Task{}, "id = ?", []any{task.ID}, 0)
}

func TestAiLogsRejectDeletedTenantParents(t *testing.T) {
	configs, _, db := newTenantDeleteEnv(t, &fakeClient{})
	target, _ := seedDeleteTenants(t, db)
	channel := model.AiChannel{Name: "target", OciConfigID: target.ID, Region: "r1"}
	mustCreate(t, db, &channel)
	gw := NewAiGatewayService(db, configs, &fakeClient{})
	callID := gw.LogCall(model.AiCallLog{ChannelID: channel.ID, ChannelName: channel.Name})
	if callID == 0 {
		t.Fatal("initial call log was not created")
	}
	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("delete tenant: %v", err)
	}
	lateID := gw.LogCall(model.AiCallLog{ChannelID: channel.ID, ChannelName: channel.Name})
	if lateID != 0 {
		t.Errorf("late call ID = %d, want 0", lateID)
	}
	gw.LogContent(model.AiContentLog{CallLogID: callID, RequestBody: "late"})
	gw.LogContent(model.AiContentLog{CallLogID: 0, RequestBody: "orphan"})
	assertCount(t, db, &model.AiCallLog{}, "channel_id = ?", []any{channel.ID}, 0)
	assertCount(t, db, &model.AiContentLog{}, "", nil, 0)
}

var errInjectedTenantDelete = errors.New("injected tenant delete failure")

func registerDeleteFailure(t *testing.T, db *gorm.DB, table string) {
	t.Helper()
	name := "test:tenant_delete_failure"
	err := db.Callback().Delete().Before("gorm:delete").Register(name, func(tx *gorm.DB) {
		if tx.Statement.Table == table {
			tx.AddError(errInjectedTenantDelete)
		}
	})
	if err != nil {
		t.Fatalf("register callback: %v", err)
	}
	t.Cleanup(func() { _ = db.Callback().Delete().Remove(name) })
}

func mustCreate(t *testing.T, db *gorm.DB, value any) {
	t.Helper()
	if err := db.Create(value).Error; err != nil {
		t.Fatalf("create %T: %v", value, err)
	}
}

func assertCount(t *testing.T, db *gorm.DB, value any, query string, args []any, want int64) {
	t.Helper()
	q := db.Model(value)
	if query != "" {
		q = q.Where(query, args...)
	}
	var got int64
	if err := q.Count(&got).Error; err != nil {
		t.Fatalf("count %T: %v", value, err)
	}
	if got != want {
		t.Errorf("count %T = %d, want %d", value, got, want)
	}
}

func TestDeleteTenantSkipsCorruptTaskPayload(t *testing.T) {
	configs, _, db := newTenantDeleteEnv(t, &fakeClient{})
	target, _ := seedDeleteTenants(t, db)
	corrupt := model.Task{Name: "corrupt-snatch", Type: model.TaskTypeSnatch, Payload: `{broken`}
	mustCreate(t, db, &corrupt)
	mustCreate(t, db, &model.TaskLog{TaskID: corrupt.ID, Message: "keep"})

	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("坏 payload 不应阻断租户删除: %v", err)
	}
	var kept model.Task
	if err := db.First(&kept, corrupt.ID).Error; err != nil || kept.Payload != `{broken` {
		t.Errorf("坏任务应原样保留: %+v, %v", kept, err)
	}
	assertCount(t, db, &model.TaskLog{}, "task_id = ?", []any{corrupt.ID}, 1)
	assertCount(t, db, &model.OciConfig{}, "id = ?", []any{target.ID}, 0)
}

func TestDeleteTenantManyEventsNoVarLimit(t *testing.T) {
	// 回归:事件数超 SQLite 绑定变量上限(32766)时删除仍成功(旧实现 IN 展开必失败)
	configs, _, db := newTenantDeleteEnv(t, &fakeClient{})
	target, _ := seedDeleteTenants(t, db)
	events := make([]model.LogEvent, 0, 33000)
	for i := 0; i < 33000; i++ {
		events = append(events, model.LogEvent{OciConfigID: target.ID, MessageID: fmt.Sprintf("bulk-%d", i)})
	}
	if err := db.CreateInBatches(&events, 500).Error; err != nil {
		t.Fatalf("seed events: %v", err)
	}
	if err := configs.Delete(context.Background(), target.ID); err != nil {
		t.Fatalf("数万事件时删除不应受绑定变量上限影响: %v", err)
	}
	assertCount(t, db, &model.LogEvent{}, "oci_config_id = ?", []any{target.ID}, 0)
}
