package telegram

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/yaoapp/gou/model"
	"github.com/yaoapp/xun/capsule"
	robottypes "github.com/yaoapp/yao/agent/robot/types"
	"github.com/yaoapp/yao/agent/testutils"
	"github.com/yaoapp/yao/event"
	tgapi "github.com/yaoapp/yao/integrations/telegram"
)

var (
	tgBotToken string
	tgHost     string
)

func TestMain(m *testing.M) {
	tgBotToken = os.Getenv("TELEGRAM_TEST_BOT_TOKEN")
	tgHost = os.Getenv("TELEGRAM_TEST_HOST")
	os.Exit(m.Run())
}

func skipIfNoToken(t *testing.T) {
	t.Helper()
	if tgBotToken == "" {
		t.Skip("TELEGRAM_TEST_BOT_TOKEN not set")
	}
}

func newTestBot() *tgapi.Bot {
	var opts []tgapi.BotOption
	if tgHost != "" {
		opts = append(opts, tgapi.WithAPIBase(tgHost))
	}
	return tgapi.NewBot(tgBotToken, "", opts...)
}

// confirmPendingUpdates checks if there are pending updates from previous seeds.
func confirmPendingUpdates(t *testing.T) []*tgapi.ConvertedMessage {
	t.Helper()
	b := newTestBot()
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	msgs, err := b.GetUpdates(ctx, 0, 5, nil)
	require.NoError(t, err)
	return msgs
}

// TestE2E_Adapter_Apply verifies that Apply correctly registers a bot.
func TestE2E_Adapter_Apply(t *testing.T) {
	skipIfNoToken(t)

	a := &Adapter{
		bots:   make(map[string]*botEntry),
		appIdx: make(map[string]string),
		dedup:  newDedupStore(),
		stopCh: make(chan struct{}),
	}
	defer close(a.stopCh)

	robot := &robottypes.Robot{
		MemberID: "robot_e2e_tg_adapter",
		TeamID:   "team_e2e_tg",
		Config: &robottypes.Config{
			Integrations: &robottypes.Integrations{
				Telegram: &robottypes.TelegramConfig{
					Enabled:  true,
					BotToken: tgBotToken,
					Host:     tgHost,
					AppID:    "e2e-test-app",
				},
			},
		},
	}

	a.Apply(context.Background(), robot)

	a.mu.RLock()
	entry, ok := a.bots["robot_e2e_tg_adapter"]
	a.mu.RUnlock()

	require.True(t, ok, "bot should be registered")
	assert.Equal(t, tgBotToken, entry.bot.Token())
	assert.Equal(t, "e2e-test-app", entry.appID)

	// Verify GetMe works through the registered bot
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	me, err := entry.bot.GetMe(ctx)
	require.NoError(t, err)
	assert.True(t, me.IsBot)
	t.Logf("OK  Apply: bot registered id=%d username=%s", me.ID, me.Username)

	// Verify ResolveBot
	resolved := a.ResolveBot("e2e-test-app")
	require.NotNil(t, resolved)
	assert.Equal(t, tgBotToken, resolved.Token())
}

// TestE2E_Adapter_Apply_Update verifies that Apply with a different token replaces the bot.
func TestE2E_Adapter_Apply_Update(t *testing.T) {
	skipIfNoToken(t)

	a := &Adapter{
		bots:   make(map[string]*botEntry),
		appIdx: make(map[string]string),
		dedup:  newDedupStore(),
		stopCh: make(chan struct{}),
	}
	defer close(a.stopCh)

	robot := &robottypes.Robot{
		MemberID: "robot_e2e_tg_update",
		TeamID:   "team_e2e_tg",
		Config: &robottypes.Config{
			Integrations: &robottypes.Integrations{
				Telegram: &robottypes.TelegramConfig{
					Enabled:  true,
					BotToken: tgBotToken,
					Host:     tgHost,
				},
			},
		},
	}

	a.Apply(context.Background(), robot)
	a.mu.RLock()
	_, ok := a.bots["robot_e2e_tg_update"]
	a.mu.RUnlock()
	require.True(t, ok)

	// Apply again with same token — should be a no-op
	a.Apply(context.Background(), robot)
	a.mu.RLock()
	assert.Len(t, a.bots, 1)
	a.mu.RUnlock()

	// Remove
	a.Remove(context.Background(), "robot_e2e_tg_update")
	a.mu.RLock()
	_, ok = a.bots["robot_e2e_tg_update"]
	a.mu.RUnlock()
	assert.False(t, ok, "bot should be removed")
	t.Log("OK  Apply/Remove lifecycle verified")
}

// TestE2E_Adapter_PollAll verifies that pollAll fetches updates from Telegram
// and processes them through handleMessages.
func TestE2E_Adapter_PollAll(t *testing.T) {
	skipIfNoToken(t)
	testutils.Prepare(t)
	defer testutils.Clean(t)

	pending := confirmPendingUpdates(t)
	if len(pending) == 0 {
		t.Skip("no pending updates; run integrations/telegram seed first")
	}
	t.Logf("found %d pending updates", len(pending))

	// Create adapter WITHOUT auto-starting pollLoop
	a := &Adapter{
		bots:   make(map[string]*botEntry),
		appIdx: make(map[string]string),
		dedup:  newDedupStore(),
		stopCh: make(chan struct{}),
	}
	defer close(a.stopCh)

	memberID := "robot_e2e_tg_poll"
	setupTestRobot(t, memberID)
	defer cleanupTestRobots(t)

	var opts []tgapi.BotOption
	if tgHost != "" {
		opts = append(opts, tgapi.WithAPIBase(tgHost))
	}
	a.bots[memberID] = &botEntry{
		robotID: memberID,
		appID:   "e2e-poll-app",
		bot:     tgapi.NewBot(tgBotToken, "", opts...),
	}

	// Start event bus so event.Push works
	if err := event.Start(); err != nil && err != event.ErrAlreadyStart {
		t.Fatalf("event.Start: %v", err)
	}
	defer func() { _ = event.Stop(context.Background()) }()

	// Manually trigger one poll cycle
	a.pollAll()

	// Verify offset advanced (meaning updates were processed)
	a.mu.RLock()
	entry := a.bots[memberID]
	a.mu.RUnlock()
	assert.Greater(t, entry.offset, int64(0), "offset should have advanced after processing updates")
	t.Logf("OK  pollAll: offset advanced to %d", entry.offset)
}

// TestE2E_Adapter_Dedup verifies that duplicate messages are not processed twice.
func TestE2E_Adapter_Dedup(t *testing.T) {
	skipIfNoToken(t)

	a := &Adapter{
		bots:   make(map[string]*botEntry),
		appIdx: make(map[string]string),
		dedup:  newDedupStore(),
		stopCh: make(chan struct{}),
	}
	defer close(a.stopCh)

	key := "tg:test-robot:12345"
	assert.True(t, a.dedup.markSeen(key), "first time should return true")
	assert.False(t, a.dedup.markSeen(key), "second time should return false (dedup)")
	t.Log("OK  dedup working correctly")
}

// TestE2E_Adapter_HandleMessages_Integration verifies the full flow:
// GetUpdates → ConvertedMessage → handleMessages → event.Push
func TestE2E_Adapter_HandleMessages_Integration(t *testing.T) {
	skipIfNoToken(t)
	testutils.Prepare(t)
	defer testutils.Clean(t)

	b := newTestBot()
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	msgs, err := b.GetUpdates(ctx, 0, 5, nil)
	require.NoError(t, err)
	if len(msgs) == 0 {
		t.Skip("no pending updates; run integrations/telegram seed first")
	}

	memberID := "robot_e2e_tg_handle"
	setupTestRobot(t, memberID)
	defer cleanupTestRobots(t)

	if err := event.Start(); err != nil && err != event.ErrAlreadyStart {
		t.Fatalf("event.Start: %v", err)
	}
	defer func() { _ = event.Stop(context.Background()) }()

	a := &Adapter{
		bots:   make(map[string]*botEntry),
		appIdx: make(map[string]string),
		dedup:  newDedupStore(),
		stopCh: make(chan struct{}),
	}
	defer close(a.stopCh)

	entry := &botEntry{
		robotID: memberID,
		appID:   "e2e-handle-app",
		bot:     b,
	}

	// Group all messages by chatID (like pollAll does) and process each group
	grouped := groupByChatID(msgs)
	for chatID, chatMsgs := range grouped {
		t.Logf("processing chat=%d messages=%d", chatID, len(chatMsgs))
		for _, cm := range chatMsgs {
			t.Logf("  update_id=%d msg_id=%d text=%q media=%d",
				cm.UpdateID, cm.MessageID, truncate(cm.Text, 40), len(cm.MediaItems))
		}
		a.handleMessages(ctx, entry, chatMsgs)
	}

	// Verify dedup: all updates should be marked as seen
	cm := msgs[0]
	assert.False(t, a.dedup.markSeen(fmt.Sprintf("tg:%s:%d", memberID, cm.UpdateID)),
		"update should be marked as seen after handleMessages")

	// Second call with same messages should be fully deduped (no-op)
	a.handleMessages(ctx, entry, msgs)
	t.Logf("OK  handleMessages processed %d updates across %d chats", len(msgs), len(grouped))
}

// ==================== Helpers ====================

func setupTestRobot(t *testing.T, memberID string) {
	t.Helper()
	m := model.Select("__yao.member")
	if m == nil {
		t.Skip("__yao.member model not loaded")
	}
	qb := capsule.Query()

	robotConfig := map[string]interface{}{
		"identity": map[string]interface{}{
			"role":   "Telegram E2E Test Robot",
			"duties": []string{"Process Telegram messages"},
		},
		"integrations": map[string]interface{}{
			"telegram": map[string]interface{}{
				"enabled":   true,
				"bot_token": tgBotToken,
				"host":      tgHost,
				"app_id":    "e2e-tg-app-" + memberID,
			},
		},
		"resources": map[string]interface{}{
			"phases": map[string]interface{}{
				"host": "robot.host",
			},
		},
	}
	configJSON, _ := json.Marshal(robotConfig)

	err := qb.Table(m.MetaData.Table.Name).Insert([]map[string]interface{}{
		{
			"member_id":       memberID,
			"team_id":         "team_e2e_tg",
			"member_type":     "robot",
			"display_name":    "E2E TG Adapter Test " + memberID,
			"system_prompt":   "You are a test robot for Telegram adapter E2E testing.",
			"status":          "active",
			"role_id":         "member",
			"autonomous_mode": false,
			"robot_status":    "idle",
			"robot_config":    string(configJSON),
		},
	})
	if err != nil {
		t.Fatalf("setup robot %s: %v", memberID, err)
	}
}

func cleanupTestRobots(t *testing.T) {
	t.Helper()
	m := model.Select("__yao.member")
	if m == nil {
		return
	}
	qb := capsule.Query()
	_, _ = qb.Table(m.MetaData.Table.Name).Where("member_id", "like", "robot_e2e_tg%").Delete()
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}
