package notify

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

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
)

// reverseDecryptor 是测试用 Decryptor：把密文逐字节取反作为 "解密"，与
// reverseEncrypt 配对，确保 IMNotifier 走解密路径而非裸读明文。
type reverseDecryptor struct{ err error }

func reverseBytes(b []byte) []byte {
	out := make([]byte, len(b))
	for i, c := range b {
		out[i] = ^c
	}
	return out
}

func (d reverseDecryptor) Decrypt(ct []byte) ([]byte, error) {
	if d.err != nil {
		return nil, d.err
	}
	return reverseBytes(ct), nil
}

// captureDoer 记录最后一次 HTTP 请求并返回固定响应码。
type captureDoer struct {
	mu     sync.Mutex
	calls  int
	url    string
	body   []byte
	ctype  string
	status int
	err    error
}

func (c *captureDoer) Do(req *http.Request) (*http.Response, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err != nil {
		return nil, c.err
	}
	c.calls++
	c.url = req.URL.String()
	c.ctype = req.Header.Get("Content-Type")
	if req.Body != nil {
		c.body, _ = io.ReadAll(req.Body)
	}
	st := c.status
	if st == 0 {
		st = http.StatusOK
	}
	return &http.Response{StatusCode: st, Body: io.NopCloser(strings.NewReader("ok"))}, nil
}

func imPrefs(uid uuid.UUID, enabled bool, sev Severity, webhookEnc []byte) Prefs {
	return Prefs{UserID: uid, IMEnabled: enabled, MinSeverity: sev, ImWebhookURLEnc: webhookEnc}
}

func TestIMNotifier_PostsDecryptedWebhookWithSlackPayload(t *testing.T) {
	uid := uuid.New()
	enc := reverseBytes([]byte("https://hooks.slack.test/abc"))
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityWarning, enc))
	doer := &captureDoer{}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())

	err := n.Send(context.Background(), Message{
		UserID: uid, Topic: "acp.permission_pending", Severity: SeverityWarning,
		Title: "Approval needed", Body: "run rm", Link: "/approvals",
	})
	require.NoError(t, err)
	require.Equal(t, 1, doer.calls)
	require.Equal(t, "https://hooks.slack.test/abc", doer.url, "应 POST 到解密后的 URL")
	require.Equal(t, "application/json", doer.ctype)

	var payload map[string]any
	require.NoError(t, json.Unmarshal(doer.body, &payload))
	require.Equal(t, "acp.permission_pending", payload["topic"])
	require.Equal(t, "warning", payload["severity"])
	require.Equal(t, "Approval needed", payload["title"])
	text, _ := payload["text"].(string)
	require.Contains(t, text, "[WARNING] Approval needed")
	require.Contains(t, text, "run rm")
	require.Contains(t, text, "/approvals")
}

func TestIMNotifier_DisabledChannelSkipped(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, false, SeverityInfo, reverseBytes([]byte("https://x"))))
	doer := &captureDoer{}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())

	require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
	require.Equal(t, 0, doer.calls)
}

func TestIMNotifier_NoWebhookSkipped(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityInfo, nil))
	doer := &captureDoer{}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())

	require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
	require.Equal(t, 0, doer.calls, "未配置 webhook 应跳过")
}

func TestIMNotifier_BelowMinSeveritySkipped(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityError, reverseBytes([]byte("https://x"))))
	doer := &captureDoer{}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())

	require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityWarning}))
	require.Equal(t, 0, doer.calls)
}

func TestIMNotifier_GloballyDisabledSkipped(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityInfo, reverseBytes([]byte("https://x"))))
	doer := &captureDoer{}
	n := NewIMNotifier(IMConfig{Enabled: false}, prefs, reverseDecryptor{}, doer, newTestLogger())

	require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
	require.Equal(t, 0, doer.calls)
}

func TestIMNotifier_DecryptErrorReturnsErr(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityInfo, []byte("garbage")))
	doer := &captureDoer{}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{err: errors.New("bad key")}, doer, newTestLogger())

	require.Error(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
	require.Equal(t, 0, doer.calls)
}

func TestIMNotifier_Non2xxReturnsErr(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityInfo, reverseBytes([]byte("https://x"))))
	doer := &captureDoer{status: http.StatusInternalServerError}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())

	require.Error(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
	require.Equal(t, 1, doer.calls)
}

func TestIMNotifier_HTTPErrorReturnsErr(t *testing.T) {
	uid := uuid.New()
	prefs := newFakePrefs()
	prefs.set(imPrefs(uid, true, SeverityInfo, reverseBytes([]byte("https://x"))))
	doer := &captureDoer{err: errors.New("dial fail")}
	n := NewIMNotifier(IMConfig{Enabled: true}, prefs, reverseDecryptor{}, doer, newTestLogger())

	require.Error(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
}

func TestIMNotifier_NameIsIMChannel(t *testing.T) {
	n := NewIMNotifier(IMConfig{Enabled: true}, newFakePrefs(), reverseDecryptor{}, &captureDoer{}, newTestLogger())
	require.Equal(t, ChannelIM, n.Name())
}
