package handlers

import (
	"context"
	"encoding/json"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"wa-assistant/backend/models"
	"wa-assistant/backend/services"
)

func TestGroupPinTimelineKeepsActorAndDoesNotChangeCustomerState(t *testing.T) {
	db, _ := csWorkspaceDB(t)
	if err := db.AutoMigrate(&models.PinnedMessage{}, &models.InboxReadState{}); err != nil {
		t.Fatal(err)
	}
	const group = "120363000000001@g.us"
	now := time.Now().Truncate(time.Second)
	read := models.InboxReadState{AgentID: 1, Sender: group, WhatsAppUnreadCount: 3, WhatsAppSynced: true, LastReadAt: &now}
	if err := db.Create(&read).Error; err != nil {
		t.Fatal(err)
	}
	expires := now.Add(24 * time.Hour)
	own := services.MessagePinUpdate{Sender: group, MessageID: "target", EventMessageID: "own-action", Pinned: true, ChangedAt: now, ExpiresAt: &expires, ActorFromMe: true}
	other := own
	other.EventMessageID = "other-action"
	other.ActorFromMe = false
	other.ActorName = "Budi"
	other.ActorJID = "628111111111@s.whatsapp.net"
	other.ChangedAt = now.Add(time.Minute)
	unpin := other
	unpin.EventMessageID = "unpin-action"
	unpin.Pinned = false
	unpin.ChangedAt = now.Add(2 * time.Minute)
	stub := services.MessagePinUpdate{Sender: group, EventMessageID: "stub-action", Pinned: true, ChangedAt: now.Add(3 * time.Minute), ActorFromMe: true, ActionOnly: true}
	for _, update := range []services.MessagePinUpdate{own, other, unpin, own, other, stub, stub} {
		if err := OnWAMessagePin(1, update); err != nil {
			t.Fatal(err)
		}
	}
	var rows []models.ChatHistory
	db.Where("agent_id=1 AND sender=?", group).Order("created_at").Find(&rows)
	want := []string{"Anda menyematkan pesan", "Budi menyematkan pesan", "Budi melepas sematan pesan", "Anda menyematkan pesan"}
	if len(rows) != len(want) {
		t.Fatalf("duplicate/missing activities: %d", len(rows))
	}
	for i, row := range rows {
		if row.Message != want[i] || row.MediaType != "system" || row.LiveIncoming || row.Reply != "" {
			t.Fatalf("wrong activity: %+v", row)
		}
		if inboxMessageSent(row) || services.IsOutgoingHistoryMessage(row) {
			t.Fatal("activity can be quoted or revoked as a customer message")
		}
	}
	var pin models.PinnedMessage
	db.Where("agent_id=1").First(&pin)
	if pin.Pinned || pin.ExpiresAt != nil || pin.MessageID != "target" {
		t.Fatal("older action or bare stub changed current pin state")
	}
	db.First(&read, read.ID)
	if read.WhatsAppUnreadCount != 3 || read.LastReadAt == nil || !read.LastReadAt.Equal(now) {
		t.Fatal("activity changed read markers")
	}
	history, err := services.LoadRecentChatContext(context.Background(), db, 1, group, nil)
	if err != nil || len(history) != 0 {
		t.Fatal("system activity entered AI context", err, len(history))
	}
	var work int64
	db.Model(&models.ConversationWork{}).Count(&work)
	if work != 0 {
		t.Fatal("system activity opened customer work")
	}
}

func TestPinnedMessagesScopeOldContentExpiryRevocationAndMissingSource(t *testing.T) {
	db, user := csWorkspaceDB(t)
	if err := db.AutoMigrate(&models.PinnedMessage{}); err != nil {
		t.Fatal(err)
	}
	now := time.Now().Truncate(time.Second)
	sender := "628111111111"
	for _, row := range []models.ChatHistory{{AgentID: 1, Sender: sender, WAMsgID: "old", Message: "Info lama tetap tersemat", CreatedAt: now.Add(-90 * 24 * time.Hour)}, {AgentID: 2, Sender: sender, WAMsgID: "old", Message: "SECRET_FOREIGN_AGENT"}, {AgentID: 1, Sender: "628999999999", WAMsgID: "wrong-chat", Message: "SECRET_OTHER_CHAT"}, {AgentID: 1, Sender: sender, WAMsgID: "revoked", Message: "SECRET_DELETED", Revoked: true}, {AgentID: 1, Sender: sender, WAMsgID: "media", Message: "Caption gambar", MediaType: "image", MediaMetadata: []byte("PRIVATE_BLOB")}} {
		db.Create(&row)
	}
	for _, id := range []string{"old", "wrong-chat", "missing", "revoked", "expired", "media"} {
		update := services.MessagePinUpdate{Sender: sender, MessageID: id, Pinned: true, ChangedAt: now}
		if id == "expired" {
			expiry := now.Add(-time.Second)
			update.ExpiresAt = &expiry
		}
		if err := OnWAMessagePin(1, update); err != nil {
			t.Fatal(err)
		}
	}
	r := gin.New()
	r.Use(AuthMiddleware(), CSRouteGuard())
	r.GET("/api/agents/:id/pinned-messages", ListPinnedMessages)
	if w := csRequest(r, user, "GET", "/api/agents/2/pinned-messages?sender="+sender, nil); w.Code != 404 {
		t.Fatal("foreign pins accessible")
	}
	w := csRequest(r, user, "GET", "/api/agents/1/pinned-messages?sender="+sender, nil)
	if w.Code != 200 || strings.Contains(w.Body.String(), "SECRET") || strings.Contains(w.Body.String(), "PRIVATE_BLOB") {
		t.Fatal("pin data leaked", w.Code)
	}
	var out struct {
		Data []pinnedMessageView `json:"data"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
		t.Fatal(err)
	}
	if len(out.Data) != 4 {
		t.Fatal("expired/revoked pins not filtered", len(out.Data))
	}
	seen := map[string]pinnedMessageView{}
	for _, pin := range out.Data {
		seen[pin.MessageID] = pin
	}
	if seen["old"].Message == nil || seen["old"].Message.Message != "Info lama tetap tersemat" || seen["missing"].Message != nil || seen["wrong-chat"].Message != nil || seen["media"].Message == nil || !seen["media"].Message.MediaDownloadable {
		t.Fatal("old/missing/media pin incorrect")
	}
	db.Where("user_id = ?", user.ID).Delete(&models.UserAgentAssignment{})
	if w := csRequest(r, user, "GET", "/api/agents/1/pinned-messages?sender="+sender, nil); w.Code != 404 {
		t.Fatal("revoked access still reads pins")
	}
}

func TestUnpinTombstoneWinsOutOfOrderSyncAndDuplicateDelivery(t *testing.T) {
	db, _ := csWorkspaceDB(t)
	db.AutoMigrate(&models.PinnedMessage{})
	now := time.Now().Truncate(time.Second)
	removed := services.MessagePinUpdate{Sender: "628111111111", MessageID: "target", ChangedAt: now}
	if err := OnWAMessagePin(1, removed); err != nil {
		t.Fatal(err)
	}
	older := removed
	older.ChangedAt = now.Add(-time.Hour)
	older.Pinned = true
	var wg sync.WaitGroup
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := OnWAMessagePin(1, older); err != nil {
				t.Error(err)
			}
		}()
	}
	wg.Wait()
	var row models.PinnedMessage
	db.First(&row)
	var count int64
	db.Model(&models.PinnedMessage{}).Count(&count)
	if row.Pinned || count != 1 {
		t.Fatal("older history resurrected removed pin")
	}
	newer := older
	newer.ChangedAt = now.Add(time.Minute)
	expiry := now.Add(24 * time.Hour)
	newer.ExpiresAt = &expiry
	if err := OnWAMessagePin(1, newer); err != nil {
		t.Fatal(err)
	}
	newer.ExpiresAt = nil
	if err := OnWAMessagePin(1, newer); err != nil {
		t.Fatal(err)
	}
	var current models.PinnedMessage
	db.First(&current)
	if !current.Pinned || current.ExpiresAt == nil {
		t.Fatal("re-pin or known expiry lost")
	}
	var histories, work int64
	db.Model(&models.ChatHistory{}).Count(&histories)
	db.Model(&models.ConversationWork{}).Count(&work)
	if histories != 0 || work != 0 {
		t.Fatal("pin created customer history or response timer")
	}
}

func TestGroupPinRemovesOnlyVerifiedProtocolPlaceholder(t *testing.T) {
	db, _ := csWorkspaceDB(t)
	if err := db.AutoMigrate(&models.PinnedMessage{}); err != nil {
		t.Fatal(err)
	}
	sender := "120363000000001@g.us"
	for _, row := range []models.ChatHistory{
		{AgentID: 1, Sender: sender, WAMsgID: "pin-action", Message: "Nama: Pesan grup"},
		{AgentID: 1, Sender: sender, WAMsgID: "real-text", Message: "Nama: Pesan grup"},
		{AgentID: 2, Sender: sender, WAMsgID: "pin-action", Message: "Nama: Pesan grup"},
		{AgentID: 1, Sender: sender, WAMsgID: "target", Message: "Info yang disematkan"},
	} {
		if err := db.Create(&row).Error; err != nil {
			t.Fatal(err)
		}
	}
	update := services.MessagePinUpdate{Sender: sender, MessageID: "target", Pinned: true, ChangedAt: time.Now()}
	if err := OnWAMessagePin(1, update); err != nil {
		t.Fatal(err)
	}
	var count int64
	db.Model(&models.ChatHistory{}).Count(&count)
	if count != 4 {
		t.Fatal("placeholder deleted without protocol proof")
	}
	update.EventMessageID = "pin-action"
	if err := OnWAMessagePin(1, update); err != nil {
		t.Fatal(err)
	}
	db.Model(&models.ChatHistory{}).Count(&count)
	if count != 4 {
		t.Fatal("verified placeholder should become one system activity; other messages must remain", count)
	}
	db.Model(&models.ChatHistory{}).Where("agent_id = ? AND sender = ? AND wa_msg_id = ? AND media_type = ?", 1, sender, "pin-action", "system").Count(&count)
	if count != 1 {
		t.Fatal("proven pin action must render as a system activity")
	}
}

func TestChatPinsOrderInboxPreserveUnreadAndRejectOlderHistory(t *testing.T) {
	db, user := csWorkspaceDB(t)
	if err := db.AutoMigrate(&models.InboxReadState{}, &models.GroupGuardConfig{}, &models.Label{}, &models.ChatLabel{}); err != nil {
		t.Fatal(err)
	}
	now := time.Now().Truncate(time.Second)
	sender, recent := "628111111111", "628222222222"
	old := now.Add(-24 * time.Hour)
	state := models.InboxReadState{AgentID: 1, Sender: sender, LastMsgAt: &old, LastReadAt: &old, WhatsAppUnreadCount: 3, WhatsAppSynced: true, WhatsAppStateAt: &now}
	db.Create(&state)
	db.Create(&models.InboxReadState{AgentID: 1, Sender: recent, LastMsgAt: &now})
	db.Create(&models.ChatHistory{AgentID: 1, Sender: sender, Message: "Older message", CreatedAt: old})
	db.Create(&models.ChatHistory{AgentID: 1, Sender: recent, Message: "Recent message", CreatedAt: now})
	if err := OnWAChatPin(1, services.ChatPinUpdate{Sender: sender, Pinned: true, ChangedAt: now}); err != nil {
		t.Fatal(err)
	}
	db.First(&state, state.ID)
	if !state.Pinned || state.WhatsAppUnreadCount != 3 || !state.LastMsgAt.Equal(old) || !state.LastReadAt.Equal(old) || !state.WhatsAppStateAt.Equal(now) {
		t.Fatal("pin overwrote read markers", state)
	}
	r := gin.New()
	r.Use(AuthMiddleware(), CSRouteGuard())
	r.GET("/api/agents/:id/contacts", InboxContacts)
	assertFirst := func(want string, pinned bool) {
		t.Helper()
		w := csRequest(r, user, "GET", "/api/agents/1/contacts", nil)
		var response struct {
			Data []struct {
				Sender string `json:"sender"`
				Pinned bool   `json:"pinned"`
				Unread int    `json:"unread_count"`
			} `json:"data"`
		}
		if json.Unmarshal(w.Body.Bytes(), &response) != nil || w.Code != 200 || len(response.Data) != 2 || response.Data[0].Sender != want || response.Data[0].Pinned != pinned {
			t.Fatal("wrong inbox pin order", w.Code, w.Body.String())
		}
		if pinned && response.Data[0].Unread != 3 {
			t.Fatal("pin changed unread count")
		}
	}
	assertFirst(sender, true)
	if err := OnWAChatPin(1, services.ChatPinUpdate{Sender: sender, Pinned: false, ChangedAt: now.Add(time.Minute)}); err != nil {
		t.Fatal(err)
	}
	for _, update := range []services.ChatPinUpdate{
		{Sender: sender, Pinned: true, ChangedAt: now},
		{Sender: sender, Pinned: true, ChangedAt: now.Add(time.Minute)},
		{Sender: sender, Pinned: true, ChangedAt: now.Add(time.Hour), FromHistory: true},
	} {
		if err := OnWAChatPin(1, update); err != nil {
			t.Fatal(err)
		}
	}
	assertFirst(recent, false)
	if w := csRequest(r, user, "GET", "/api/agents/2/contacts", nil); w.Code != 404 {
		t.Fatal("foreign chat pins accessible")
	}
}

func TestChatPinsKeepGroupsWithoutHistoryAboveNewerUnpinnedChats(t *testing.T) {
	db, user := csWorkspaceDB(t)
	if err := db.AutoMigrate(&models.InboxReadState{}, &models.GroupGuardConfig{}, &models.Label{}, &models.ChatLabel{}); err != nil {
		t.Fatal(err)
	}
	now := time.Now().Truncate(time.Second)
	private, group, oldGroup, other := "628111111111", "120363000000001@g.us", "120363000000002@g.us", "628222222222"
	// Pin timestamps deliberately differ from message order. A recent unpinned
	// message must still remain below both active and history-less pinned groups.
	for i, sender := range []string{private, group, oldGroup} {
		if err := OnWAChatPin(1, services.ChatPinUpdate{Sender: sender, Pinned: true, ChangedAt: now.Add(time.Duration(i) * time.Minute)}); err != nil {
			t.Fatal(err)
		}
	}
	for i, sender := range []string{other, private, group} {
		at := now.Add(-time.Duration(i) * time.Hour)
		state := models.InboxReadState{AgentID: 1, Sender: sender}
		if err := db.Where("agent_id = ? AND sender = ?", 1, sender).FirstOrCreate(&state).Error; err != nil {
			t.Fatal(err)
		}
		if err := db.Model(&state).Update("last_msg_at", at).Error; err != nil {
			t.Fatal(err)
		}
		if err := db.Create(&models.ChatHistory{AgentID: 1, Sender: sender, Message: "Message", CreatedAt: at}).Error; err != nil {
			t.Fatal(err)
		}
	}
	r := gin.New()
	r.Use(AuthMiddleware(), CSRouteGuard())
	r.GET("/api/agents/:id/contacts", InboxContacts)
	w := csRequest(r, user, "GET", "/api/agents/1/contacts", nil)
	var response struct {
		Data []struct {
			Sender string `json:"sender"`
			Pinned bool   `json:"pinned"`
			Group  bool   `json:"is_group"`
		} `json:"data"`
	}
	if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &response) != nil || len(response.Data) != 4 {
		t.Fatal("inbox response", w.Code, w.Body.String())
	}
	for i, sender := range []string{private, group, oldGroup, other} {
		row := response.Data[i]
		if row.Sender != sender || row.Pinned != (i < 3) || row.Group != (i == 1 || i == 2) {
			t.Fatal("pinned private/group ordering failed", response.Data)
		}
	}
}
