package handlers

import (
	"bytes"
	"encoding/json"
	"errors"
	"go.mau.fi/whatsmeow/types"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"wa-assistant/backend/database"
	"wa-assistant/backend/models"
	"wa-assistant/backend/services"
)

func workflowTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	db, err := gorm.Open(sqlite.New(sqlite.Config{DriverName: "sqlite", DSN: "file:" + t.Name() + "?mode=memory&cache=shared&_time_format=sqlite"}), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	if err != nil {
		t.Fatal(err)
	}
	sqlDB, _ := db.DB()
	sqlDB.SetMaxOpenConns(1)
	t.Cleanup(func() { sqlDB.Close() })
	if err := db.AutoMigrate(&models.Agent{}, &models.Knowledge{}, &models.Handoff{}, &models.ChatHistory{}, &models.Contact{}, &models.ConversationMemory{}, &models.UserAgentAssignment{}, &models.ConversationWork{}, &models.User{}); err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&models.Agent{ID: 1, TenantID: 1, Name: "test"}).Error; err != nil {
		t.Fatal(err)
	}
	old := database.DB
	database.DB = db
	t.Cleanup(func() { database.DB = old; services.InvalidateKB(1) })
	return db
}

func workflowRequest(t *testing.T, fn gin.HandlerFunc, body any, params gin.Params) *httptest.ResponseRecorder {
	t.Helper()
	data, _ := json.Marshal(body)
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("POST", "/", bytes.NewReader(data))
	c.Request.Header.Set("Content-Type", "application/json")
	c.Params = params
	c.Set("tenant_id", uint(1))
	c.Set("user_id", uint(7))
	c.Set("role", "admin")
	fn(c)
	return w
}

func TestKnowledgeDraftDoesNotOverwritePublishedFacts(t *testing.T) {
	db := workflowTestDB(t)
	old := models.Knowledge{AgentID: 1, Question: "Harga paket Pro?", Answer: "Rp75.000", Active: true, ReviewStatus: "published", Source: "manual", Scope: "general"}
	if err := db.Create(&old).Error; err != nil {
		t.Fatal(err)
	}
	writer := newKnowledgeUpserter(1)
	draft, _, err := writer.save(old.Question, "Rp99.000", "harga", "web", "https://example.com")
	if err != nil {
		t.Fatal(err)
	}
	db.First(&old, old.ID)
	if old.Answer != "Rp75.000" || !old.Active || draft.ID == old.ID || draft.Active || draft.ReviewStatus != "draft" || draft.SupersedesID != old.ID {
		t.Fatalf("versi aktif tertimpa: old=%+v draft=%+v", old, draft)
	}
	if got := services.KnowledgeFor(1); len(got) != 1 || got[0].K.ID != old.ID {
		t.Fatalf("draft masuk retrieval: %+v", got)
	}
}

func TestKnowledgePublicationRequiresExplicitReplacement(t *testing.T) {
	db := workflowTestDB(t)
	old := models.Knowledge{AgentID: 1, Question: "Harga kaos?", Answer: "Rp75.000", Active: true, ReviewStatus: "published", Scope: "general"}
	draft := models.Knowledge{AgentID: 1, Question: "Harga kaos?", Answer: "Rp99.000", ReviewStatus: "draft", Scope: "general"}
	db.Create(&old)
	db.Create(&draft)
	params := gin.Params{{Key: "id", Value: "1"}, {Key: "kid", Value: "2"}}
	w := workflowRequest(t, ReviewKnowledge, map[string]any{"action": "publish", "scope": "general"}, params)
	if w.Code != 409 {
		t.Fatalf("konflik diterbitkan: %d %s", w.Code, w.Body.String())
	}
	w = workflowRequest(t, ReviewKnowledge, map[string]any{"action": "publish", "scope": "general", "replace_ids": []uint{old.ID}}, params)
	if w.Code != 200 {
		t.Fatalf("revisi gagal: %d %s", w.Code, w.Body.String())
	}
	db.First(&old, old.ID)
	db.First(&draft, draft.ID)
	if old.Active || old.ReviewStatus != "archived" || !draft.Active || draft.VerifiedAt == nil || draft.ReviewedBy != 7 {
		t.Fatal("transisi versi salah")
	}
	foreign := workflowRequest(t, ReviewKnowledge, map[string]any{"action": "archive"}, gin.Params{{Key: "id", Value: "999"}, {Key: "kid", Value: "2"}})
	if foreign.Code != 404 {
		t.Fatalf("scope agent tidak divalidasi: %d", foreign.Code)
	}
}

func TestKnowledgeScopeAndEffectiveDatesSeparateFacts(t *testing.T) {
	now := time.Now()
	later := now.Add(time.Hour)
	a := models.Knowledge{Question: "Harga kaos?", Answer: "75000", Scope: "bandung", EffectiveUntil: &now}
	b := models.Knowledge{Question: "Harga kaos?", Answer: "99000", Scope: "jakarta"}
	if knowledgeReviewConflict(a, b) {
		t.Fatal("cakupan berbeda dianggap konflik")
	}
	b.Scope = "bandung"
	b.EffectiveFrom = &later
	if knowledgeReviewConflict(a, b) {
		t.Fatal("periode berbeda dianggap konflik")
	}
	b.EffectiveFrom = nil
	if !knowledgeReviewConflict(a, b) {
		t.Fatal("fakta bertentangan lolos")
	}
}

func TestHandoffIsIdempotentStaysPausedAndResumesExplicitly(t *testing.T) {
	db := workflowTestDB(t)
	tid := uint(1)
	db.Create(&models.User{ID: 7, TenantID: &tid, Username: "fixture-owner", Role: "admin", Active: true})
	db.Create(&models.ChatHistory{AgentID: 1, Sender: "6281", Message: "Tolong cek pesanan kemarin", CreatedAt: time.Now()})
	created, err := ensureHandoffDetails(1, "6281", "Belum ada jawaban", "insufficient_evidence")
	if err != nil || !created {
		t.Fatalf("handoff tidak dibuat: %v", err)
	}
	created, err = ensureHandoffDetails(1, "6281", "Ada tambahan informasi", "insufficient_evidence")
	if err != nil || created {
		t.Fatal("handoff ganda")
	}
	db.Model(&models.Handoff{}).Where("agent_id = ?", 1).Update("created_at", time.Now().Add(-24*time.Hour))
	if state := resolveHandoffState(1, "6281"); !state.active {
		t.Fatalf("AI melanjutkan sendiri: %+v", state)
	}
	params := gin.Params{{Key: "id", Value: "1"}, {Key: "sender", Value: "6281"}}
	w := workflowRequest(t, ClaimHandoff, nil, params)
	if w.Code != 200 {
		t.Fatalf("claim: %s", w.Body.String())
	}
	var handoff models.Handoff
	db.First(&handoff)
	if handoff.Status != "handling" || handoff.AssignedUserID != 7 || handoff.Summary == "" {
		t.Fatalf("handoff tidak lengkap: %+v", handoff)
	}
	if automaticReplyStillAllowed(1, "6281") {
		t.Fatal("balasan terlambat menimpa CS")
	}
	w = workflowRequest(t, ResumeHandoff, nil, params)
	if w.Code != 200 {
		t.Fatalf("resume: %s", w.Body.String())
	}
	if resolveHandoffState(1, "6281").active {
		t.Fatal("resume tidak mengembalikan kendali")
	}
}

func TestKnowledgeEditCreatesReviewedRevisionAndPreservesArchive(t *testing.T) {
	db := workflowTestDB(t)
	old := models.Knowledge{AgentID: 1, Question: "Harga paket?", Answer: "Rp75.000", Active: true, ReviewStatus: "published", Embedding: "[1,2]"}
	if err := db.Create(&old).Error; err != nil {
		t.Fatal(err)
	}
	params := gin.Params{{Key: "id", Value: "1"}, {Key: "kid", Value: "1"}}
	w := workflowRequest(t, UpdateKnowledge, map[string]any{"question": old.Question, "answer": "Rp99.000", "active": true}, params)
	if w.Code != 200 {
		t.Fatalf("edit: %d %s", w.Code, w.Body.String())
	}
	var draft models.Knowledge
	db.Order("id DESC").First(&draft)
	db.First(&old, old.ID)
	if draft.ID == old.ID || draft.Active || draft.ReviewStatus != "draft" || draft.Embedding != "" || draft.SupersedesID != old.ID || old.Answer != "Rp75.000" {
		t.Fatalf("revisi tidak aman: old=%+v draft=%+v", old, draft)
	}
	w = workflowRequest(t, UpdateKnowledge, map[string]any{"question": old.Question, "answer": "Rp105.000"}, gin.Params{{Key: "id", Value: "1"}, {Key: "kid", Value: "2"}})
	if w.Code != 200 {
		t.Fatalf("edit draft: %d %s", w.Code, w.Body.String())
	}
	db.Model(&models.Knowledge{}).Where("agent_id = ?", 1).Updates(map[string]any{"active": false, "review_status": "archived"})
	fresh, _, err := newKnowledgeUpserter(1).save(old.Question, old.Answer, "", "manual", "")
	if err != nil || fresh.ID == old.ID || fresh.ReviewStatus != "draft" {
		t.Fatalf("arsip menghalangi input ulang: %+v %v", fresh, err)
	}
}

func TestHandoffCancelsStaleAutomaticRetryButPreservesHumanMessages(t *testing.T) {
	db := workflowTestDB(t)
	row := models.ChatHistory{AgentID: 1, Sender: "6281", Reply: "Balasan lama yang gagal dikirim"}
	if !retryReplyStillAllowed(row) {
		t.Fatal("balasan biasa tidak boleh diblok tanpa handoff")
	}
	if _, err := ensureHandoffDetails(1, "6281", "Butuh manusia", "customer_request"); err != nil {
		t.Fatal(err)
	}
	if retryReplyStillAllowed(row) {
		t.Fatal("retry jawaban lama lolos setelah handoff")
	}
	row.Reply = humanFacingHoldReply
	if !retryReplyStillAllowed(row) {
		t.Fatal("pemberitahuan pengalihan yang gagal harus masih dapat dikirim saat menunggu")
	}
	row.Reply = legacyHumanFacingHoldReply
	if !retryReplyStillAllowed(row) {
		t.Fatal("pembaruan teks membatalkan notifikasi lama yang tertunda")
	}
	db.Model(&models.Handoff{}).Where("agent_id = ?", 1).Update("status", "handling")
	if retryReplyStillAllowed(row) {
		t.Fatal("pemberitahuan terlambat menyela CS")
	}
	row.FromHuman = true
	if !retryReplyStillAllowed(row) {
		t.Fatal("pesan CS sendiri tidak boleh diblok")
	}
}

func TestSimulatorHumanRequestDoesNotNeedModelOrCreateRealHandoff(t *testing.T) {
	db := workflowTestDB(t)
	w := workflowRequest(t, TestChat, map[string]any{"message": "tolong hubungkan ke CS manusia"}, gin.Params{{Key: "id", Value: "1"}})
	if w.Code != 200 {
		t.Fatalf("simulator: %d %s", w.Code, w.Body.String())
	}
	var body struct {
		Escalate bool `json:"escalate"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil || !body.Escalate {
		t.Fatal("simulasi tidak beralih ke CS")
	}
	var count int64
	db.Model(&models.Handoff{}).Count(&count)
	if count != 0 {
		t.Fatal("simulator membuat antrean pelanggan nyata")
	}
}

func TestHandoffCanceledChunkIsNotReportedAsSent(t *testing.T) {
	err := sendChunked(0, types.NewJID("6281", types.DefaultUserServer), "Balasan yang tidak boleh terkirim", 0, 0, func() bool { return false })
	if !errors.Is(err, errAutomaticReplyPaused) {
		t.Fatalf("pembatalan disamarkan sebagai sukses: %v", err)
	}
}
