package channels

import (
	"context"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
)

// ==================== Adapter Interface Tests ====================

func TestTelegramAdapter_Type(t *testing.T) {
	a := &TelegramAdapter{}
	if a.Type() != "telegram" {
		t.Errorf("expected 'telegram', got %q", a.Type())
	}
}

func TestTelegramAdapter_DisplayName(t *testing.T) {
	a := &TelegramAdapter{}
	if a.DisplayName() != "Telegram" {
		t.Errorf("expected 'Telegram', got %q", a.DisplayName())
	}
}

func TestTelegramAdapter_ValidateConfig_Valid(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.ValidateConfig(map[string]interface{}{
		"bot_token": "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
		"chat_id":   "-100123",
	})
	if err != nil {
		t.Errorf("expected no error, got %v", err)
	}
}

func TestTelegramAdapter_ValidateConfig_MissingBotToken(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.ValidateConfig(map[string]interface{}{
		"chat_id": "-100123",
	})
	if err == nil {
		t.Error("expected error for missing bot_token")
	}
}

func TestTelegramAdapter_ValidateConfig_MissingChatID(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.ValidateConfig(map[string]interface{}{
		"bot_token": "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
	})
	if err == nil {
		t.Error("expected error for missing chat_id")
	}
}

func TestTelegramAdapter_ValidateConfig_BadTokenFormat(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.ValidateConfig(map[string]interface{}{
		"bot_token": "not-a-real-token",
		"chat_id":   "-100",
	})
	if err == nil {
		t.Error("expected error for malformed bot_token")
	}
}

func TestTelegramAdapter_ValidateConfig_Empty(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.ValidateConfig(map[string]interface{}{})
	if err == nil {
		t.Error("expected error for empty config")
	}
}

func TestTelegramAdapter_SendMessage_EmptyToken(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.SendMessage(context.Background(), map[string]interface{}{}, "-100", "hello")
	if err == nil {
		t.Error("expected error for empty bot_token")
	}
}

func TestTelegramAdapter_SendMessage_InvalidChatID(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.SendMessage(context.Background(), map[string]interface{}{
		"bot_token": "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
	}, "not-a-number", "hello")
	if err == nil {
		t.Error("expected error for invalid chat_id")
	}
}

func TestTelegramAdapter_StartPolling_EmptyToken(t *testing.T) {
	a := &TelegramAdapter{}
	err := a.StartPolling(context.Background(), map[string]interface{}{}, nil)
	if err == nil {
		t.Error("expected error for empty bot_token")
	}
}

// ==================== Registry Tests ====================

func TestGetAdapter_Telegram(t *testing.T) {
	a, ok := GetAdapter("telegram")
	if !ok || a == nil {
		t.Error("expected telegram adapter to be registered")
	}
	if a.Type() != "telegram" {
		t.Errorf("expected type 'telegram', got %q", a.Type())
	}
}

func TestGetAdapter_Unknown(t *testing.T) {
	_, ok := GetAdapter("whatsapp")
	if ok {
		t.Error("expected unknown adapter to not be found")
	}
}

func TestListAdapters(t *testing.T) {
	list := ListAdapters()
	if len(list) == 0 {
		t.Fatal("expected at least 1 adapter")
	}
	found := false
	for _, a := range list {
		if a["type"] == "telegram" {
			found = true
			if a["display_name"] != "Telegram" {
				t.Errorf("expected display_name 'Telegram', got %q", a["display_name"])
			}
		}
	}
	if !found {
		t.Error("telegram not found in ListAdapters")
	}
}

// ==================== Manager Tests ====================

type mockProxy struct {
	statusCode int
	respBody   []byte
	err        error
	calls      int
}

func (m *mockProxy) ProxyA2ARequest(ctx context.Context, workspaceID string, body []byte, callerID string, logActivity bool) (int, []byte, error) {
	m.calls++
	return m.statusCode, m.respBody, m.err
}

type mockBroadcaster struct {
	events []string
}

func (m *mockBroadcaster) RecordAndBroadcast(ctx context.Context, eventType, workspaceID string, data interface{}) error {
	m.events = append(m.events, eventType)
	return nil
}

func TestManager_NewManager(t *testing.T) {
	proxy := &mockProxy{}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)
	if mgr == nil {
		t.Fatal("expected non-nil manager")
	}
	if mgr.pollers == nil {
		t.Error("expected pollers map to be initialized")
	}
}

func TestManager_Stop(t *testing.T) {
	proxy := &mockProxy{}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)

	ctx, cancel := context.WithCancel(context.Background())
	mgr.pollers["test-id-123456"] = cancel

	mgr.Stop()
	if len(mgr.pollers) != 0 {
		t.Errorf("expected 0 pollers after stop, got %d", len(mgr.pollers))
	}
	// Verify context was cancelled
	select {
	case <-ctx.Done():
		// good
	default:
		t.Error("expected poller context to be cancelled")
	}
}

func TestManager_HandleInbound_AllowlistBlocked(t *testing.T) {
	proxy := &mockProxy{statusCode: 200, respBody: []byte(`{"result":{"parts":[{"kind":"text","text":"hi"}]}}`)}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)

	ch := ChannelRow{
		ID:           "ch-123456789012",
		WorkspaceID:  "ws-123456789012",
		ChannelType:  "telegram",
		Config:       map[string]interface{}{"bot_token": "fake", "chat_id": "-100"},
		AllowedUsers: []string{"user-999"}, // Only user-999 allowed
	}

	msg := &InboundMessage{
		ChatID:    "-100",
		UserID:    "user-123", // Not in allowlist
		Username:  "blocked",
		Text:      "hello",
		MessageID: "1",
	}

	err := mgr.HandleInbound(context.Background(), ch, msg)
	if err != nil {
		t.Errorf("expected nil error for blocked user, got %v", err)
	}
	if proxy.calls != 0 {
		t.Errorf("expected 0 proxy calls for blocked user, got %d", proxy.calls)
	}
}

func TestManager_HandleInbound_AllowlistAllowed(t *testing.T) {
	proxy := &mockProxy{statusCode: 200, respBody: []byte(`{"result":{"parts":[{"kind":"text","text":"hi"}]}}`)}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)

	ch := ChannelRow{
		ID:           "ch-123456789012",
		WorkspaceID:  "ws-123456789012",
		ChannelType:  "telegram",
		Config:       map[string]interface{}{"bot_token": "fake", "chat_id": "-100"},
		AllowedUsers: []string{"user-123"},
	}

	msg := &InboundMessage{
		ChatID:    "-100",
		UserID:    "user-123",
		Username:  "allowed",
		Text:      "hello",
		MessageID: "1",
	}

	// This will fail at SendMessage (no real Telegram API) but proves allowlist passed
	_ = mgr.HandleInbound(context.Background(), ch, msg)
	if proxy.calls != 1 {
		t.Errorf("expected 1 proxy call for allowed user, got %d", proxy.calls)
	}
}

func TestManager_HandleInbound_EmptyAllowlist(t *testing.T) {
	proxy := &mockProxy{statusCode: 200, respBody: []byte(`{"result":{"parts":[{"kind":"text","text":"hi"}]}}`)}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)

	ch := ChannelRow{
		ID:           "ch-123456789012",
		WorkspaceID:  "ws-123456789012",
		ChannelType:  "telegram",
		Config:       map[string]interface{}{"bot_token": "fake", "chat_id": "-100"},
		AllowedUsers: []string{}, // Empty = allow all
	}

	msg := &InboundMessage{
		ChatID:    "-100",
		UserID:    "anyone",
		Username:  "anyone",
		Text:      "hello",
		MessageID: "1",
	}

	_ = mgr.HandleInbound(context.Background(), ch, msg)
	if proxy.calls != 1 {
		t.Errorf("expected 1 proxy call for empty allowlist, got %d", proxy.calls)
	}
}

func TestManager_HandleInbound_AllowByChatID(t *testing.T) {
	proxy := &mockProxy{statusCode: 200, respBody: []byte(`{"result":{"parts":[{"kind":"text","text":"ok"}]}}`)}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)

	ch := ChannelRow{
		ID:           "ch-123456789012",
		WorkspaceID:  "ws-123456789012",
		ChannelType:  "telegram",
		Config:       map[string]interface{}{"bot_token": "fake", "chat_id": "-100"},
		AllowedUsers: []string{"-100"}, // Allow by chat_id (group)
	}

	msg := &InboundMessage{
		ChatID:    "-100",
		UserID:    "user-456",
		Username:  "groupuser",
		Text:      "hello",
		MessageID: "1",
	}

	_ = mgr.HandleInbound(context.Background(), ch, msg)
	if proxy.calls != 1 {
		t.Errorf("expected 1 proxy call when chat_id matches allowlist, got %d", proxy.calls)
	}
}

func TestManager_HandleInbound_BroadcastsEvent(t *testing.T) {
	// Use empty result so SendMessage is skipped (no reply text) — broadcast still fires
	proxy := &mockProxy{statusCode: 200, respBody: []byte(`{"result":{}}`)}
	bc := &mockBroadcaster{}
	mgr := NewManager(proxy, bc)

	ch := ChannelRow{
		ID:          "ch-123456789012",
		WorkspaceID: "ws-123456789012",
		ChannelType: "telegram",
		Config:      map[string]interface{}{"bot_token": "fake", "chat_id": "-100"},
	}
	msg := &InboundMessage{ChatID: "-100", UserID: "u1", Username: "test", Text: "hi", MessageID: "1"}

	_ = mgr.HandleInbound(context.Background(), ch, msg)
	found := false
	for _, e := range bc.events {
		if e == "CHANNEL_MESSAGE" {
			found = true
		}
	}
	if !found {
		t.Error("expected CHANNEL_MESSAGE broadcast event")
	}
}

// ==================== extractReplyText Tests ====================

func TestExtractReplyText_Parts(t *testing.T) {
	proxy := &mockProxy{}
	mgr := NewManager(proxy, nil)

	body := []byte(`{"result":{"parts":[{"kind":"text","text":"hello world"}]}}`)
	text := mgr.extractReplyText(body, 200)
	if text != "hello world" {
		t.Errorf("expected 'hello world', got %q", text)
	}
}

func TestExtractReplyText_Artifacts(t *testing.T) {
	proxy := &mockProxy{}
	mgr := NewManager(proxy, nil)

	body := []byte(`{"result":{"artifacts":[{"parts":[{"kind":"text","text":"artifact text"}]}]}}`)
	text := mgr.extractReplyText(body, 200)
	if text != "artifact text" {
		t.Errorf("expected 'artifact text', got %q", text)
	}
}

func TestExtractReplyText_ErrorStatus(t *testing.T) {
	proxy := &mockProxy{}
	mgr := NewManager(proxy, nil)

	text := mgr.extractReplyText([]byte(`{}`), 500)
	if text == "" {
		t.Error("expected error message for non-2xx status")
	}
}

func TestExtractReplyText_InvalidJSON(t *testing.T) {
	proxy := &mockProxy{}
	mgr := NewManager(proxy, nil)

	text := mgr.extractReplyText([]byte(`not json`), 200)
	if text != "" {
		t.Errorf("expected empty for invalid JSON, got %q", text)
	}
}

func TestExtractReplyText_EmptyResult(t *testing.T) {
	proxy := &mockProxy{}
	mgr := NewManager(proxy, nil)

	text := mgr.extractReplyText([]byte(`{"result":{}}`), 200)
	if text != "" {
		t.Errorf("expected empty for no text parts, got %q", text)
	}
}

// ==================== truncID Tests ====================

func TestTruncID_Long(t *testing.T) {
	if got := truncID("abcdefghijklmnop"); got != "abcdefghijkl" {
		t.Errorf("expected 'abcdefghijkl', got %q", got)
	}
}

func TestTruncID_Short(t *testing.T) {
	if got := truncID("abc"); got != "abc" {
		t.Errorf("expected 'abc', got %q", got)
	}
}

func TestTruncID_Exact12(t *testing.T) {
	if got := truncID("123456789012"); got != "123456789012" {
		t.Errorf("expected '123456789012', got %q", got)
	}
}

func TestTruncID_Empty(t *testing.T) {
	if got := truncID(""); got != "" {
		t.Errorf("expected '', got %q", got)
	}
}

// ==================== DiscoverChats Tests ====================

func TestDiscoverChats_InvalidToken(t *testing.T) {
	a := &TelegramAdapter{}
	// tgbotapi.NewBotAPI with empty token calls the API and fails
	_, err := a.DiscoverChats(context.Background(), "")
	if err == nil {
		t.Error("expected error for empty bot token")
	}
}

func TestDiscoverChats_MalformedToken(t *testing.T) {
	a := &TelegramAdapter{}
	// Clearly malformed tokens return an error from tgbotapi
	_, err := a.DiscoverChats(context.Background(), "not-a-real-token")
	if err == nil {
		t.Error("expected error for malformed token")
	}
}

// ==================== Poller Lifetime Tests ====================

// TestManager_PollerSurvivesRequestContext verifies pollers use the manager's
// background context, NOT the request ctx passed to Reload(). Otherwise pollers
// die immediately when an HTTP handler returns.
//
// This catches the bug where Reload(c.Request.Context()) would cancel the
// polling goroutine ~50ms after channel creation when the HTTP request finished.
func TestManager_PollerSurvivesRequestContext(t *testing.T) {
	mgr := NewManager(&mockProxy{}, &mockBroadcaster{})

	// Manager started with long-lived background context (simulates main.go wiring)
	bgCtx, bgCancel := context.WithCancel(context.Background())
	defer bgCancel()
	mgr.bgCtx = bgCtx

	// Spawn a poller using the manager's bgCtx (the correct path)
	pollCtx, pollCancel := context.WithCancel(mgr.bgCtx)
	mgr.mu.Lock()
	mgr.pollers["test-id"] = pollCancel
	mgr.mu.Unlock()

	pollerStopped := make(chan struct{})
	go func() {
		<-pollCtx.Done()
		close(pollerStopped)
	}()

	// Simulate an HTTP request context being cancelled (handler returned)
	requestCtx, requestCancel := context.WithCancel(context.Background())
	requestCancel()
	_ = requestCtx

	// Poller MUST still be alive — it's tied to bgCtx, not requestCtx
	select {
	case <-pollerStopped:
		t.Fatal("poller died when request context was cancelled — pollers must use manager.bgCtx, not request ctx")
	case <-time.After(100 * time.Millisecond):
		// good — poller still running
	}

	// Cleanup: cancel bg ctx to stop the poller
	bgCancel()
	select {
	case <-pollerStopped:
		// good
	case <-time.After(500 * time.Millisecond):
		t.Fatal("poller didn't stop after bg context cancelled")
	}
}

// TestManager_BgCtxFallback verifies that if Start() was never called (bgCtx is nil),
// pollers fall back to context.Background() instead of crashing.
func TestManager_BgCtxFallback(t *testing.T) {
	mgr := NewManager(&mockProxy{}, &mockBroadcaster{})
	// Don't set mgr.bgCtx — simulates a manager that wasn't Start()ed

	parent := mgr.bgCtx
	if parent == nil {
		parent = context.Background()
	}
	pollCtx, cancel := context.WithCancel(parent)
	defer cancel()

	// Poller should be alive
	select {
	case <-pollCtx.Done():
		t.Fatal("poller ctx already cancelled with nil bgCtx — should fall back to context.Background()")
	default:
		// good
	}
}

// ==================== Multi-Chat ID Tests ====================

func TestParseChatIDs_Single(t *testing.T) {
	ids := parseChatIDs(map[string]interface{}{"chat_id": "-100123"})
	if len(ids) != 1 || ids[0] != "-100123" {
		t.Errorf("expected ['-100123'], got %v", ids)
	}
}

func TestParseChatIDs_Multiple(t *testing.T) {
	ids := parseChatIDs(map[string]interface{}{"chat_id": "-100123, -100456, -100789"})
	if len(ids) != 3 {
		t.Fatalf("expected 3 IDs, got %d: %v", len(ids), ids)
	}
	if ids[0] != "-100123" || ids[1] != "-100456" || ids[2] != "-100789" {
		t.Errorf("unexpected IDs: %v", ids)
	}
}

func TestParseChatIDs_Empty(t *testing.T) {
	ids := parseChatIDs(map[string]interface{}{})
	if len(ids) != 0 {
		t.Errorf("expected empty, got %v", ids)
	}
}

func TestParseChatIDs_Whitespace(t *testing.T) {
	ids := parseChatIDs(map[string]interface{}{"chat_id": " -100 , , -200 "})
	if len(ids) != 2 || ids[0] != "-100" || ids[1] != "-200" {
		t.Errorf("expected ['-100','-200'], got %v", ids)
	}
}

func TestIsChatAllowed_InList(t *testing.T) {
	config := map[string]interface{}{"chat_id": "-100, -200, -300"}
	if !isChatAllowed(config, "-200") {
		t.Error("expected -200 to be allowed")
	}
}

func TestIsChatAllowed_NotInList(t *testing.T) {
	config := map[string]interface{}{"chat_id": "-100, -200"}
	if isChatAllowed(config, "-999") {
		t.Error("expected -999 to NOT be allowed")
	}
}

func TestIsChatAllowed_EmptyConfig(t *testing.T) {
	config := map[string]interface{}{}
	if !isChatAllowed(config, "-anything") {
		t.Error("expected all chats allowed when no chat_id configured")
	}
}

func TestSplitChatIDs_Multiple(t *testing.T) {
	ids := splitChatIDs("-100, -200, -300")
	if len(ids) != 3 {
		t.Fatalf("expected 3, got %d", len(ids))
	}
}

func TestSplitChatIDs_Single(t *testing.T) {
	ids := splitChatIDs("-100")
	if len(ids) != 1 || ids[0] != "-100" {
		t.Errorf("expected ['-100'], got %v", ids)
	}
}

func TestSplitChatIDs_Empty(t *testing.T) {
	ids := splitChatIDs("")
	if len(ids) != 0 {
		t.Errorf("expected empty, got %v", ids)
	}
}

// ==================== SendOutbound Tests ====================

func TestManager_SendOutbound_NoChatID(t *testing.T) {
	// Test that SendMessage fails when chatID is empty
	adapter, _ := GetAdapter("telegram")
	config := map[string]interface{}{"bot_token": "fake"} // no chat_id
	err := adapter.SendMessage(context.Background(), config, "", "test")
	if err == nil {
		t.Error("expected error for empty chatID")
	}
}

// ==================== #123 — disableChannelByChatID wiring ====================

// The callback is a package-level var set by NewManager; we verify both its
// default (safe no-op) and the wired-up path via a UPDATE assertion against
// a sqlmock-backed db.DB. Two tests guard the contract: the var is callable
// at zero-value, and a wired callback issues the right UPDATE.

func TestDisableChannelByChatID_DefaultIsNoOp(t *testing.T) {
	// Save + restore so we don't pollute sibling tests that build a Manager.
	prev := disableChannelByChatID
	t.Cleanup(func() { disableChannelByChatID = prev })
	disableChannelByChatID = func(ctx context.Context, chatID string) {}

	// Must not panic when called with empty / odd inputs.
	disableChannelByChatID(context.Background(), "")
	disableChannelByChatID(context.Background(), "not-a-number")
	disableChannelByChatID(context.Background(), "-100123")
}

func TestDisableChannelByChatID_WiredSetsEnabledFalse(t *testing.T) {
	mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	t.Cleanup(func() { mockDB.Close() })
	prevDB := db.DB
	db.DB = mockDB
	t.Cleanup(func() { db.DB = prevDB })

	// UPDATE must match the chat_id path and flip enabled=false.
	mock.ExpectExec(`UPDATE workspace_channels\s+SET enabled = false.*WHERE channel_type = 'telegram'\s+AND enabled = true\s+AND config->>'chat_id' = \$1`).
		WithArgs("-100123").
		WillReturnResult(sqlmock.NewResult(0, 1))

	// Build a Manager to install the wired callback. Pass nils — Reload
	// is invoked after the UPDATE when rows>0; sqlmock doesn't need to
	// cover that follow-up query here because we're only asserting the
	// UPDATE's shape and args.
	mock.MatchExpectationsInOrder(false)
	// Reload() reads enabled rows; cover the SELECT that follows.
	mock.ExpectQuery(`SELECT .+ FROM workspace_channels WHERE enabled = true`).
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "workspace_id", "channel_type", "config", "allowlist", "enabled",
			"last_message_at", "message_count",
		}))

	_ = NewManager(nil, nil)

	disableChannelByChatID(context.Background(), "-100123")

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("sqlmock expectations: %v", err)
	}
}

// ==================== SlackAdapter Tests (#384) ====================

func TestSlackAdapter_Type(t *testing.T) {
	a := &SlackAdapter{}
	if a.Type() != "slack" {
		t.Errorf("expected 'slack', got %q", a.Type())
	}
}

func TestSlackAdapter_DisplayName(t *testing.T) {
	a := &SlackAdapter{}
	if a.DisplayName() != "Slack" {
		t.Errorf("expected 'Slack', got %q", a.DisplayName())
	}
}

func TestSlackAdapter_ValidateConfig_Valid(t *testing.T) {
	a := &SlackAdapter{}
	err := a.ValidateConfig(map[string]interface{}{
		"webhook_url": "https://hooks.slack.com/services/T000/B000/xxx",
	})
	if err != nil {
		t.Errorf("expected no error for valid webhook URL, got %v", err)
	}
}

func TestSlackAdapter_ValidateConfig_MissingWebhookURL(t *testing.T) {
	a := &SlackAdapter{}
	err := a.ValidateConfig(map[string]interface{}{})
	if err == nil {
		t.Error("expected error for missing webhook_url")
	}
}

func TestSlackAdapter_ValidateConfig_InvalidPrefix(t *testing.T) {
	// Any URL that doesn't start with https://hooks.slack.com/ must be rejected.
	a := &SlackAdapter{}
	cases := []string{
		"http://hooks.slack.com/services/T000/B000/xxx",    // wrong scheme
		"https://evil.example.com/slack-hook",              // wrong host
		"https://hooks.slack.com.evil.com/services/T/B/x", // SSRF lookalike
		"not-a-url",
		"",
	}
	for _, u := range cases {
		config := map[string]interface{}{"webhook_url": u}
		err := a.ValidateConfig(config)
		if err == nil {
			t.Errorf("expected error for webhook_url %q, got nil", u)
		}
		if u != "" && err != nil && err.Error() != "invalid Slack webhook URL" {
			t.Errorf("webhook_url %q: expected 'invalid Slack webhook URL', got %q", u, err.Error())
		}
	}
}

func TestSlackAdapter_ValidateConfig_EmptyString(t *testing.T) {
	a := &SlackAdapter{}
	err := a.ValidateConfig(map[string]interface{}{"webhook_url": ""})
	if err == nil {
		t.Error("expected error for empty webhook_url")
	}
}

func TestSlackAdapter_SendMessage_EmptyWebhookURL(t *testing.T) {
	a := &SlackAdapter{}
	err := a.SendMessage(context.Background(), map[string]interface{}{}, "ignored-chat", "hello")
	if err == nil {
		t.Error("expected error for missing webhook_url")
	}
}

func TestSlackAdapter_SendMessage_InvalidPrefix(t *testing.T) {
	a := &SlackAdapter{}
	err := a.SendMessage(context.Background(), map[string]interface{}{
		"webhook_url": "https://evil.example.com/hook",
	}, "ignored", "hello")
	if err == nil {
		t.Error("expected error for invalid webhook URL prefix in SendMessage")
	}
}

func TestSlackAdapter_StartPolling_ReturnsNil(t *testing.T) {
	// Slack webhooks don't support polling — must return nil immediately.
	a := &SlackAdapter{}
	err := a.StartPolling(context.Background(), map[string]interface{}{}, nil)
	if err != nil {
		t.Errorf("expected nil from StartPolling, got %v", err)
	}
}

func TestGetAdapter_Slack(t *testing.T) {
	a, ok := GetAdapter("slack")
	if !ok || a == nil {
		t.Error("expected slack adapter to be registered")
	}
	if a.Type() != "slack" {
		t.Errorf("expected type 'slack', got %q", a.Type())
	}
}

func TestListAdapters_IncludesSlack(t *testing.T) {
	list := ListAdapters()
	found := false
	for _, a := range list {
		if a["type"] == "slack" {
			found = true
			if a["display_name"] != "Slack" {
				t.Errorf("expected display_name 'Slack', got %q", a["display_name"])
			}
		}
	}
	if !found {
		t.Error("slack not found in ListAdapters")
	}
}

func TestDisableChannelByChatID_NoRowsAffectedSkipsReload(t *testing.T) {
	// When the chat_id doesn't match any row (already disabled, or a different
	// bot), the UPDATE returns RowsAffected=0 and we skip the reload. Verifies
	// we don't emit a spurious log or SELECT storm on unrelated kicked events.
	mockDB, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	t.Cleanup(func() { mockDB.Close() })
	prevDB := db.DB
	db.DB = mockDB
	t.Cleanup(func() { db.DB = prevDB })

	mock.ExpectExec(`UPDATE workspace_channels\s+SET enabled = false`).
		WithArgs("999999999").
		WillReturnResult(sqlmock.NewResult(0, 0))

	_ = NewManager(nil, nil)
	disableChannelByChatID(context.Background(), "999999999")

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("sqlmock expectations: %v", err)
	}
}
