package service

import (
	"context"
	"errors"
	"strings"
	"testing"

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

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

// newSettingEnv 建一个只含 Setting 表的内存库环境。
func newSettingEnv(t *testing.T) (*SettingService, *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 in-memory sqlite: %v", err)
	}
	// :memory: 库每个连接彼此独立，SendAsync 等后台 goroutine 读库需复用同一连接
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(&model.Setting{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	cipher, err := crypto.NewCipher("test-data-key")
	if err != nil {
		t.Fatalf("new cipher: %v", err)
	}
	return NewSettingService(db, cipher), db
}

func strPtr(s string) *string { return &s }

func TestUpdateTelegramTokenHandling(t *testing.T) {
	tests := []struct {
		name      string
		initial   *string // 先保存一次的 token；nil 表示未配置过
		update    *string // 第二次更新携带的 botToken
		wantToken string  // 最终 TelegramConfig 应解出的 token
	}{
		{name: "nil 沿用旧 token", initial: strPtr("tok-1234567890"), update: nil, wantToken: "tok-1234567890"},
		{name: "非 nil 覆盖旧 token", initial: strPtr("tok-1234567890"), update: strPtr("tok-abcdefghij"), wantToken: "tok-abcdefghij"},
		{name: "空串清除已存 token", initial: strPtr("tok-1234567890"), update: strPtr(""), wantToken: ""},
		{name: "首次设置", initial: nil, update: strPtr("tok-first99999"), wantToken: "tok-first99999"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, _ := newSettingEnv(t)
			ctx := context.Background()
			if tt.initial != nil {
				in := UpdateTelegramInput{Enabled: true, BotToken: tt.initial, ChatID: "42"}
				if err := svc.UpdateTelegram(ctx, in); err != nil {
					t.Fatalf("save initial: %v", err)
				}
			}
			in := UpdateTelegramInput{Enabled: true, BotToken: tt.update, ChatID: "42"}
			if err := svc.UpdateTelegram(ctx, in); err != nil {
				t.Fatalf("update: %v", err)
			}
			cfg, err := svc.TelegramConfig(ctx)
			if err != nil {
				t.Fatalf("TelegramConfig: %v", err)
			}
			if cfg.BotToken != tt.wantToken {
				t.Errorf("token = %q, want %q", cfg.BotToken, tt.wantToken)
			}
		})
	}
}

func TestTelegramViewNeverReturnsPlaintext(t *testing.T) {
	svc, db := newSettingEnv(t)
	ctx := context.Background()
	token := "123456789:AAtestTokenValue"
	in := UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "-100200"}
	if err := svc.UpdateTelegram(ctx, in); err != nil {
		t.Fatalf("UpdateTelegram: %v", err)
	}

	view, err := svc.TelegramView(ctx)
	if err != nil {
		t.Fatalf("TelegramView: %v", err)
	}
	want := TelegramView{Enabled: true, ChatID: "-100200", TokenSet: true, TokenTail: "alue"}
	if view != want {
		t.Errorf("view = %+v, want %+v", view, want)
	}

	// 落库必须是密文：存储值不含 token 原文，但能解密还原
	var st model.Setting
	if err := db.First(&st, "key = ?", settingTelegramBotToken).Error; err != nil {
		t.Fatalf("load setting row: %v", err)
	}
	if strings.Contains(st.Value, token) {
		t.Errorf("token stored in plaintext: %q", st.Value)
	}
	cfg, err := svc.TelegramConfig(ctx)
	if err != nil || cfg.BotToken != token {
		t.Errorf("decrypted token = %q (%v), want %q", cfg.BotToken, err, token)
	}
}

func TestTokenTail(t *testing.T) {
	tests := []struct {
		name  string
		token string
		want  string
	}{
		{name: "常规 token 取尾 4 位", token: "123456789:AAtestTokenValue", want: "alue"},
		{name: "过短不展示", token: "abc", want: ""},
		{name: "空串", token: "", want: ""},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tokenTail(tt.token); got != tt.want {
				t.Errorf("tokenTail(%q) = %q, want %q", tt.token, got, tt.want)
			}
		})
	}
}

func TestNotifyEventsReadWrite(t *testing.T) {
	allOn := NotifyEventsView{
		TaskFail: true, TaskRecover: true, SnatchSuccess: true, TenantDead: true,
		TaskStop: true, LoginLock: true, ModelDeprecated: true,
		LogEventInstance: true, LogEventIdentity: true, LogEventPolicy: true,
		LogEventRegion: true, LogEventLogin: true,
	}
	offTaskFailStop := allOn
	offTaskFailStop.TaskFail, offTaskFailStop.TaskStop = false, false
	offTenantDead := allOn
	offTenantDead.TenantDead = false
	tests := []struct {
		name  string
		setup func(t *testing.T, svc *SettingService)
		want  NotifyEventsView
	}{
		{
			name:  "键全部缺省视为全开（兼容存量部署）",
			setup: func(t *testing.T, svc *SettingService) {},
			want:  allOn,
		},
		{
			name: "写 0 后读回 false",
			setup: func(t *testing.T, svc *SettingService) {
				in := allOn
				in.TaskFail = false
				in.TaskStop = false
				if err := svc.UpdateNotifyEvents(context.Background(), in); err != nil {
					t.Fatalf("UpdateNotifyEvents: %v", err)
				}
			},
			want: offTaskFailStop,
		},
		{
			name: "部分键缺失时只关显式为 0 的项",
			setup: func(t *testing.T, svc *SettingService) {
				if err := svc.set(context.Background(), settingNotifyEventTenantDead, "0"); err != nil {
					t.Fatalf("set: %v", err)
				}
			},
			want: offTenantDead,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, _ := newSettingEnv(t)
			tt.setup(t, svc)
			got, err := svc.NotifyEvents(context.Background())
			if err != nil {
				t.Fatalf("NotifyEvents: %v", err)
			}
			if got != tt.want {
				t.Errorf("view = %+v, want %+v", got, tt.want)
			}
		})
	}
}

func TestNotifyEventEnabled(t *testing.T) {
	tests := []struct {
		name  string
		value *string // 键值；nil 表示键不存在
		want  bool
	}{
		{name: "键缺失视为开启", value: nil, want: true},
		{name: "显式 0 关闭", value: strPtr("0"), want: false},
		{name: "显式 1 开启", value: strPtr("1"), want: true},
		{name: "脏数据非 0 视为开启", value: strPtr("maybe"), want: true},
		{name: "空值视为开启", value: strPtr(""), want: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, _ := newSettingEnv(t)
			ctx := context.Background()
			if tt.value != nil {
				if err := svc.set(ctx, settingNotifyEventTaskFail, *tt.value); err != nil {
					t.Fatalf("set: %v", err)
				}
			}
			if got := svc.NotifyEventEnabled(ctx, "task_fail"); got != tt.want {
				t.Errorf("NotifyEventEnabled = %v, want %v", got, tt.want)
			}
		})
	}
}

// saveTaskLimit 返回把抢机熔断阈值写入设置的 setup 步骤，供 TestTaskSettings 复用。
func saveTaskLimit(limit int) func(t *testing.T, svc *SettingService) {
	return func(t *testing.T, svc *SettingService) {
		t.Helper()
		in := TaskSettingsView{SnatchAuthFailLimit: limit}
		if err := svc.UpdateTaskSettings(context.Background(), in); err != nil {
			t.Fatalf("UpdateTaskSettings(%d): %v", limit, err)
		}
	}
}

func TestTaskSettings(t *testing.T) {
	tests := []struct {
		name  string
		setup func(t *testing.T, svc *SettingService)
		want  int
	}{
		{name: "键缺省回退默认 3", setup: func(t *testing.T, svc *SettingService) {}, want: 3},
		{name: "写 5 读回 5", setup: saveTaskLimit(5), want: 5},
		{name: "合法下界写 1 读回 1", setup: saveTaskLimit(1), want: 1},
		{name: "合法上界写 10 读回 10", setup: saveTaskLimit(10), want: 10},
		{
			name: "脏数据非整数回退 3",
			setup: func(t *testing.T, svc *SettingService) {
				if err := svc.set(context.Background(), settingSnatchAuthFailLimit, "abc"); err != nil {
					t.Fatalf("set: %v", err)
				}
			},
			want: 3,
		},
		{
			name: "脏数据越界回退 3",
			setup: func(t *testing.T, svc *SettingService) {
				if err := svc.set(context.Background(), settingSnatchAuthFailLimit, "99"); err != nil {
					t.Fatalf("set: %v", err)
				}
			},
			want: 3,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, _ := newSettingEnv(t)
			tt.setup(t, svc)
			got, err := svc.TaskSettings(context.Background())
			if err != nil {
				t.Fatalf("TaskSettings: %v", err)
			}
			if got.SnatchAuthFailLimit != tt.want {
				t.Errorf("limit = %d, want %d", got.SnatchAuthFailLimit, tt.want)
			}
		})
	}
}

func TestUpdateTaskSettingsRejectsOutOfRange(t *testing.T) {
	tests := []struct {
		name  string
		limit int
	}{
		{name: "低于下界 0", limit: 0},
		{name: "高于上界 11", limit: 11},
		{name: "负值", limit: -3},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, _ := newSettingEnv(t)
			err := svc.UpdateTaskSettings(context.Background(), TaskSettingsView{SnatchAuthFailLimit: tt.limit})
			if !errors.Is(err, ErrInvalidAuthFailLimit) {
				t.Fatalf("err = %v, want ErrInvalidAuthFailLimit", err)
			}
			got, err := svc.TaskSettings(context.Background())
			if err != nil {
				t.Fatalf("TaskSettings: %v", err)
			}
			if got.SnatchAuthFailLimit != 3 {
				t.Errorf("拒绝后读回 = %d, want 保持默认 3", got.SnatchAuthFailLimit)
			}
		})
	}
}
