package service

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"

	"oci-portal/internal/model"
)

// telegramCapture 记录假 Telegram 服务收到的 sendMessage 请求。
type telegramCapture struct {
	mu    sync.Mutex
	paths []string
	texts []string
}

// snapshot 返回目前收到的全部文本副本。
func (r *telegramCapture) snapshot() []string {
	r.mu.Lock()
	defer r.mu.Unlock()
	return append([]string(nil), r.texts...)
}

// newFakeTelegram 起一个假 Telegram API，固定返回 reply 响应体。
func newFakeTelegram(t *testing.T, reply string) (*httptest.Server, *telegramCapture) {
	t.Helper()
	rec := &telegramCapture{}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var payload struct {
			Text string `json:"text"`
		}
		_ = json.NewDecoder(r.Body).Decode(&payload)
		rec.mu.Lock()
		rec.paths = append(rec.paths, r.URL.Path)
		rec.texts = append(rec.texts, payload.Text)
		rec.mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(reply))
	}))
	t.Cleanup(srv.Close)
	return srv, rec
}

// newTestNotifier 组装指向假 Telegram 服务的 Notifier；in 为 nil 表示不预置配置。
func newTestNotifier(t *testing.T, base string, in *UpdateTelegramInput) *Notifier {
	t.Helper()
	settings, _ := newSettingEnv(t)
	if in != nil {
		if err := settings.UpdateTelegram(context.Background(), *in); err != nil {
			t.Fatalf("update telegram: %v", err)
		}
	}
	n := NewNotifier(settings)
	n.base = base
	return n
}

func TestNotifierSend(t *testing.T) {
	token := "123456:AAfake"
	tests := []struct {
		name     string
		setup    *UpdateTelegramInput
		reply    string
		wantSent bool
		wantErr  string // 空串表示期望无错误
	}{
		{
			name:     "启用且配置完整时发送",
			setup:    &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
			reply:    `{"ok":true}`,
			wantSent: true,
		},
		{
			name:  "未启用时跳过",
			setup: &UpdateTelegramInput{Enabled: false, BotToken: &token, ChatID: "42"},
			reply: `{"ok":true}`,
		},
		{
			name:  "未配置 token 时跳过",
			setup: &UpdateTelegramInput{Enabled: true, ChatID: "42"},
			reply: `{"ok":true}`,
		},
		{
			name:  "完全未配置时跳过",
			reply: `{"ok":true}`,
		},
		{
			name:     "telegram 报错时透出 description",
			setup:    &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
			reply:    `{"ok":false,"error_code":401,"description":"Unauthorized"}`,
			wantSent: true,
			wantErr:  "Unauthorized",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			srv, rec := newFakeTelegram(t, tt.reply)
			n := newTestNotifier(t, srv.URL, tt.setup)
			err := n.Send(context.Background(), "hi <b>&")
			if tt.wantErr == "" && err != nil {
				t.Fatalf("Send: %v, want nil", err)
			}
			if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
				t.Fatalf("Send err = %v, want contains %q", err, tt.wantErr)
			}
			texts := rec.snapshot()
			if sent := len(texts) > 0; sent != tt.wantSent {
				t.Fatalf("sent = %v (texts %v), want %v", sent, texts, tt.wantSent)
			}
			if tt.wantSent {
				if got, want := rec.paths[0], "/bot"+token+"/sendMessage"; got != want {
					t.Errorf("path = %q, want %q", got, want)
				}
				if got, want := texts[0], "hi &lt;b&gt;&amp;"; got != want {
					t.Errorf("text = %q, want HTML 转义后 %q", got, want)
				}
			}
		})
	}
}

func TestNotifierTest(t *testing.T) {
	token := "123456:AAfake"
	tests := []struct {
		name    string
		setup   *UpdateTelegramInput
		reply   string
		wantErr string
	}{
		{
			name:  "配置有效时发送成功",
			setup: &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
			reply: `{"ok":true}`,
		},
		{
			name:    "未配置时报可读错误",
			wantErr: "bot token and chat id are required",
		},
		{
			name:    "chat 不存在时透出描述",
			setup:   &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"},
			reply:   `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`,
			wantErr: "chat not found",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			srv, _ := newFakeTelegram(t, tt.reply)
			n := newTestNotifier(t, srv.URL, tt.setup)
			err := n.Test(context.Background())
			if tt.wantErr == "" && err != nil {
				t.Fatalf("Test: %v, want nil", err)
			}
			if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) {
				t.Fatalf("Test err = %v, want contains %q", err, tt.wantErr)
			}
		})
	}
}

// TestNotifierSendErrorHidesToken 验证网络层失败的错误信息不含 bot token：
// *url.Error 会拼出完整请求 URL（路径含 token），必须剥壳后再向上返回。
func TestNotifierSendErrorHidesToken(t *testing.T) {
	token := "123456:AAfake"
	srv, _ := newFakeTelegram(t, `{"ok":true}`)
	base := srv.URL
	srv.Close() // 立刻关闭：请求必然连接失败
	n := newTestNotifier(t, base, &UpdateTelegramInput{Enabled: true, BotToken: &token, ChatID: "42"})
	err := n.Send(context.Background(), "hi")
	if err == nil {
		t.Fatal("Send = nil, want connection error")
	}
	if strings.Contains(err.Error(), token) {
		t.Errorf("err = %v, 错误信息泄漏了 bot token", err)
	}
}

func TestNotifyEvents(t *testing.T) {
	tests := []struct {
		name string
		prev taskSnapshot
		cur  taskSnapshot
		want []notifyEvent // Kind 精确匹配,Vars 为需包含的变量键值;空表示不发
	}{
		{
			name: "成功转失败发失败通知",
			prev: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive},
			cur:  taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "Out of host capacity"},
			want: []notifyEvent{{Kind: notifyTaskFail, Vars: map[string]string{"task_name": "抢机", "error": "Out of host capacity"}}},
		},
		{
			name: "连续失败不重复发",
			prev: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "Out of host capacity"},
			cur:  taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "Out of host capacity"},
			want: nil,
		},
		{
			name: "失败换了错误内容也不重复发",
			prev: taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "capacity"},
			cur:  taskSnapshot{Name: "抢机", Status: model.TaskStatusActive, LastError: "timeout"},
			want: nil,
		},
		{
			name: "失败恢复成功发恢复通知",
			prev: taskSnapshot{Name: "测活", Status: model.TaskStatusActive, LastError: "boom"},
			cur:  taskSnapshot{Name: "测活", Status: model.TaskStatusActive},
			want: []notifyEvent{{Kind: notifyTaskRecover, Vars: map[string]string{"task_name": "测活"}}},
		},
		{
			name: "持续正常不发",
			prev: taskSnapshot{Name: "测活", Status: model.TaskStatusActive},
			cur:  taskSnapshot{Name: "测活", Status: model.TaskStatusActive},
			want: nil,
		},
		{
			name: "非成功转成功发抢机成功",
			prev: taskSnapshot{Name: "抢A1", Status: model.TaskStatusActive},
			cur:  taskSnapshot{Name: "抢A1", Status: model.TaskStatusSucceeded, Message: "created 1: ocid1"},
			want: []notifyEvent{{Kind: notifySnatchSuccess, Vars: map[string]string{"task_name": "抢A1", "message": "created 1: ocid1"}}},
		},
		{
			name: "失败后直接抢满只发抢机成功不叠加恢复",
			prev: taskSnapshot{Name: "抢A1", Status: model.TaskStatusActive, LastError: "capacity"},
			cur:  taskSnapshot{Name: "抢A1", Status: model.TaskStatusSucceeded, Message: "created 1: ocid1"},
			want: []notifyEvent{{Kind: notifySnatchSuccess, Vars: map[string]string{"task_name": "抢A1", "message": "created 1: ocid1"}}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := notifyEvents(tt.prev, tt.cur)
			if len(got) != len(tt.want) {
				t.Fatalf("events = %v (%d 条), want %d 条", got, len(got), len(tt.want))
			}
			for i, w := range tt.want {
				if got[i].Kind != w.Kind {
					t.Errorf("event[%d].Kind = %q, want %q", i, got[i].Kind, w.Kind)
				}
				for k, v := range w.Vars {
					if got[i].Vars[k] != v {
						t.Errorf("event[%d].Vars[%s] = %q, want %q", i, k, got[i].Vars[k], v)
					}
				}
			}
		})
	}
}

func TestSameStringSet(t *testing.T) {
	tests := []struct {
		name string
		a, b []string
		want bool
	}{
		{name: "都为空", a: nil, b: nil, want: true},
		{name: "顺序不同视为相同", a: []string{"x", "y"}, b: []string{"y", "x"}, want: true},
		{name: "长度不同", a: []string{"x"}, b: []string{"x", "y"}, want: false},
		{name: "元素不同", a: []string{"x"}, b: []string{"y"}, want: false},
		{name: "重复元素次数不同", a: []string{"x", "y"}, b: []string{"x", "x"}, want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := sameStringSet(tt.a, tt.b); got != tt.want {
				t.Errorf("sameStringSet(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
			}
		})
	}
}

func TestTruncateText(t *testing.T) {
	tests := []struct {
		name  string
		text  string
		limit int
		want  string
	}{
		{name: "未超限原样返回", text: "hello", limit: 10, want: "hello"},
		{name: "超限截断加省略号", text: "hello", limit: 3, want: "hel…"},
		{name: "多字节按 rune 截断", text: "抢机成功", limit: 2, want: "抢机…"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := truncateText(tt.text, tt.limit); got != tt.want {
				t.Errorf("truncateText(%q, %d) = %q, want %q", tt.text, tt.limit, got, tt.want)
			}
		})
	}
}
