package handlers

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"
	"time"

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

// Runs the real React Inbox against real authenticated handlers, SQLite and
// SSE in an isolated process. WhatsApp transport and AI are never connected.
// Opt in with CHATLOOP_BROWSER_INTEGRATION=1 and a local frontend URL.
func TestConversationTransferAcrossTwoBrowserAccounts(t *testing.T) {
	if os.Getenv("CHATLOOP_BROWSER_INTEGRATION") != "1" {
		t.Skip("set CHATLOOP_BROWSER_INTEGRATION=1 to run the two-account browser test")
	}
	frontendURL, err := url.Parse(os.Getenv("CHATLOOP_TEST_FRONTEND_URL"))
	if err != nil || frontendURL.Scheme != "http" || (frontendURL.Hostname() != "127.0.0.1" && frontendURL.Hostname() != "localhost") {
		t.Fatal("CHATLOOP_TEST_FRONTEND_URL must point to the local frontend")
	}
	db := workflowTestDB(t)
	if err := db.AutoMigrate(&models.CSActivityLog{}, &models.InboxReadState{}, &models.GroupGuardConfig{}, &models.Label{}, &models.ChatLabel{}, &models.PinnedMessage{}); err != nil {
		t.Fatal(err)
	}
	oldSecret := jwtSecret
	jwtSecret = make([]byte, 32)
	if _, err := rand.Read(jwtSecret); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { jwtSecret = oldSecret })
	tenant := uint(1)
	admin := models.User{ID: 1, Username: "internal.superadmin", Name: "Super Admin", Role: "admin", IsSuperAdmin: true, Active: true}
	cs := models.User{ID: 2, TenantID: &tenant, Username: "internal.budi", Name: "budi", Role: "cs", Active: true}
	sender := "6289000000001"
	at := time.Now().Truncate(time.Second)
	waiting, due := at.Add(-5*time.Minute), at.Add(10*time.Minute)
	rows := []any{
		&admin, &cs,
		&models.Agent{ID: 3, TenantID: 1, Name: "CS budi", Number: "6289000000003", AIEnabled: true},
		&models.UserAgentAssignment{TenantID: 1, UserID: cs.ID, AgentID: 3},
		&models.Contact{AgentID: 3, Number: sender, Name: "Pelanggan Uji Transfer"},
		&models.InboxReadState{AgentID: 3, Sender: sender, LastMsgAt: &at},
		&models.ChatHistory{AgentID: 3, Sender: sender, Message: "Percakapan khusus pengujian transfer.", WAMsgID: "internal-transfer-message", CreatedAt: at},
		&models.ConversationWork{AgentID: 3, Sender: sender, AssignedUserID: admin.ID, Status: "open", WaitingSince: &waiting, DueAt: &due, Version: 1},
		&models.Handoff{AgentID: 3, Sender: sender, AssignedUserID: admin.ID, Status: "handling", CreatedAt: waiting},
	}
	for _, row := range rows {
		if err := db.Create(row).Error; err != nil {
			t.Fatal(err)
		}
	}
	if err := db.Model(&models.Agent{}).Where("id = 1").Update("name", "CS Utama").Error; err != nil {
		t.Fatal(err)
	}

	r := gin.New()
	r.Use(AuthMiddleware(), CSRouteGuard())
	r.GET("/api/agents", ListAgents)
	r.GET("/api/agents/:id/contacts", InboxContacts)
	r.GET("/api/agents/:id/conversation", InboxConversation)
	r.GET("/api/agents/:id/conversation-work", ListConversationWork)
	r.POST("/api/agents/:id/conversation-work/:sender", ChangeConversationWork)
	r.GET("/api/agents/:id/inbox/events", InboxEvents)
	r.GET("/api/agents/:id/pinned-messages", ListPinnedMessages)
	// The default GET builds a local brief. No paid refresh route is registered.
	r.GET("/api/agents/:id/conversation/brief", GetConversationBrief)
	r.GET("/api/agents-status", func(c *gin.Context) { c.JSON(200, gin.H{"data": gin.H{"1": "connected", "3": "connected"}}) })
	r.GET("/api/agents/:id/wa/status", func(c *gin.Context) { c.JSON(200, gin.H{"status": "connected"}) })
	// Only a rejected send is exercised, using the real endpoint's ownership
	// guard. The isolated process has no connected WhatsApp client.
	r.POST("/api/agents/:id/send", InboxSend)
	r.POST("/api/agents/:id/conversation/read", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
	r.POST("/api/agents/:id/inbox/client-debug", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
	var unexpectedWrites atomic.Int32
	r.NoRoute(func(c *gin.Context) {
		if c.Request.Method != http.MethodGet {
			unexpectedWrites.Add(1)
			c.JSON(400, gin.H{"error": "Unexpected action in isolated test"})
			return
		}
		if strings.HasSuffix(c.Request.URL.Path, "/brief") {
			c.JSON(200, gin.H{"data": nil})
			return
		}
		c.JSON(200, gin.H{"data": []any{}})
	})
	proxy := httputil.NewSingleHostReverseProxy(frontendURL)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		if strings.HasPrefix(req.URL.Path, "/api/") {
			r.ServeHTTP(w, req)
			return
		}
		proxy.ServeHTTP(w, req)
	}))
	defer server.Close()
	defer server.CloseClientConnections()
	input, err := json.Marshal(map[string]any{"base": server.URL, "sender": sender, "accounts": []any{
		map[string]any{"token": issueToken(admin), "user": admin},
		map[string]any{"token": issueToken(cs), "user": cs},
	}})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 150*time.Second)
	defer cancel()
	script, err := filepath.Abs("../../frontend/tests/browser/conversation-transfer-integration.mjs")
	if err != nil {
		t.Fatal(err)
	}
	cmd := exec.CommandContext(ctx, "node", script)
	cmd.Stdin = bytes.NewReader(input)
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("browser integration failed: %v\n%s", err, output)
	}
	t.Log(string(output))
	if unexpectedWrites.Load() != 0 {
		t.Fatal("test attempted an unrelated write")
	}
	var work models.ConversationWork
	if err := db.Where("agent_id = ? AND sender = ?", 3, sender).First(&work).Error; err != nil {
		t.Fatal(err)
	}
	if work.AssignedUserID != 0 || work.Status != "resolved" || work.WaitingSince != nil || work.DueAt != nil || conversationHandledByCS(3, sender) {
		t.Fatal("final resolve did not release ownership and AI pause")
	}
	var messages, handoffs int64
	db.Model(&models.ChatHistory{}).Where("agent_id = ? AND sender = ?", 3, sender).Count(&messages)
	db.Model(&models.Handoff{}).Where("agent_id = ? AND sender = ?", 3, sender).Count(&handoffs)
	if messages != 1 || handoffs != 0 {
		t.Fatal("transfer changed history, sent a message or retained the handoff", messages, handoffs)
	}
}
