package services

import (
	"context"
	"errors"
	"fmt"
	"github.com/cloudwego/eino/components/model"
	"github.com/cloudwego/eino/schema"
	"strings"
	"testing"
	"time"
	"wa-assistant/backend/models"
)

func TestResponseModeDoesNotConfuseFactsWithStyleRequests(t *testing.T) {
	base := selectAIResponsePolicy("syarat retur?", "", "", 1)
	for _, tc := range []struct{ setting, message, want string }{
		{"short", "Alamat lengkap toko di mana?", "short"},
		{"balanced", "Ukuran panjang meja berapa?", "balanced"},
		{"detailed", "Jangan panjang, jawab ringkas saja", "short"},
		{"short", "Jangan terlalu singkat, jelaskan secara rinci", "detailed"},
		{"balanced", "Jawab singkat tapi alamatnya lengkap", "short"},
		{"short", "Jelaskan detail dan terstruktur", "detailed"},
		{"detailed", "Jangan terlalu detail", "short"},
	} {
		t.Run(tc.message, func(t *testing.T) {
			if got := responsePolicyForLength(base, tc.setting, tc.message); got.Mode != tc.want {
				t.Fatalf("mode=%s, want=%s", got.Mode, tc.want)
			}
		})
	}
}

type answerLoopModel struct{ sourceID uint }

func (m *answerLoopModel) WithTools([]*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return m, nil
}
func (m *answerLoopModel) Stream(context.Context, []*schema.Message, ...model.Option) (*schema.StreamReader[*schema.Message], error) {
	return nil, errors.New("unused")
}
func (m *answerLoopModel) Generate(_ context.Context, in []*schema.Message, _ ...model.Option) (*schema.Message, error) {
	last := in[len(in)-1]
	if last.Role == schema.Tool && strings.Contains(last.Content, `"approved":true`) {
		return schema.AssistantMessage("Retur maksimal 7 hari dan barang belum dipakai.", nil), nil
	}
	name, args := "search_knowledge", `{"query":"syarat retur"}`
	if last.Role == schema.Tool {
		days := 9
		if strings.Contains(last.Content, "unsupported_numbers") {
			days = 7
		}
		name, args = "check_answer", fmt.Sprintf(`{"answer":"Retur maksimal %d hari dan barang belum dipakai.","source_ids":[%d],"kind":"answer"}`, days, m.sourceID)
	}
	return schema.AssistantMessage("", []schema.ToolCall{{ID: fmt.Sprintf("call_%d", len(in)), Type: "function", Function: schema.FunctionCall{Name: name, Arguments: args}}}), nil
}

func TestEinoAgentRepairsRejectedAnswerUsingToolFeedback(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Apa syarat retur?", Answer: "Retur maksimal 7 hari dan barang belum dipakai.", Active: true, ReviewStatus: "published", Scope: "general"}
	if err := db.Create(&k).Error; err != nil {
		t.Fatal(err)
	}
	InvalidateKB(1)
	s := &agentTurnState{reviewAnswer: approveTestAnswer, userMessage: "Apa syarat retur?", policy: responsePolicyForLength(selectAIResponsePolicy("retur?", "", "", 1), "short", "retur?")}
	tools, err := makeAgentTools(1, ChatOptions{DryRun: true}, s)
	if err != nil {
		t.Fatal(err)
	}
	out, err := executeEino(context.Background(), &answerLoopModel{sourceID: k.ID}, tools, []*schema.Message{schema.UserMessage(s.userMessage)})
	if err != nil || s.approved == nil || s.answerChecks != 2 || out.Content != s.approved.input.Answer {
		t.Fatalf("native repair failed: checks=%d approved=%t err=%v", s.answerChecks, s.approved != nil, err)
	}
}

func TestAnswerReviewerRejectsChangedMeaningAndClearsPreviousApproval(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Apa syarat retur?", Answer: "Retur maksimal 7 hari dan barang belum dipakai.", Active: true, ReviewStatus: "published", Scope: "general"}
	db.Create(&k)
	s := &agentTurnState{reviewAnswer: approveTestAnswer, userMessage: "Syarat retur?", knowledge: []models.Knowledge{k}, policy: responsePolicyForLength(selectAIResponsePolicy("retur?", "", "", 1), "short", "retur?")}
	approved := s.checkAnswer(context.Background(), 1, answerCheckInput{Answer: k.Answer, SourceIDs: []uint{k.ID}, Kind: "answer"})
	if !approved.Approved {
		t.Fatal(approved)
	}
	called := false
	s.reviewAnswer = func(_ context.Context, q, a, e string) (answerEvidenceReview, error) {
		called = true
		if !strings.Contains(e, "belum dipakai") {
			t.Fatal("review has no source")
		}
		return answerEvidenceReview{UnsupportedClaims: []string{"barang boleh sudah dipakai"}}, nil
	}
	rejected := s.checkAnswer(context.Background(), 1, answerCheckInput{Answer: "Retur maksimal 7 hari dan barang boleh sudah dipakai.", SourceIDs: []uint{k.ID}, Kind: "answer"})
	if !called || rejected.Issue != "unsupported_claims" || s.approved != nil {
		t.Fatal("review rejection or prior approval bypassed")
	}
	if !strings.Contains(rejected.Guidance, "barang boleh sudah dipakai") {
		t.Fatal("agent did not receive the unsupported claim for repair")
	}
}

func TestAgentRejectsPriceChangedDuringTurn(t *testing.T) {
	db := agenticTestDB(t)
	if err := db.AutoMigrate(&models.Product{}); err != nil {
		t.Fatal(err)
	}
	p := models.Product{ID: 3, AgentID: 1, TenantID: 1, Name: "Kaos Nusa", Price: "Rp75.000"}
	db.Create(&p)
	s := &agentTurnState{reviewAnswer: approveTestAnswer, productFacts: map[uint]models.Product{p.ID: p}, businessEvidence: "Harga Kaos Nusa Rp75.000", userMessage: "Harga kaos?", policy: responsePolicyForLength(selectAIResponsePolicy("harga?", "", "", 1), "short", "harga?")}
	in := answerCheckInput{Answer: "Harga Kaos Nusa Rp75.000.", Kind: "answer"}
	if out := s.checkAnswer(context.Background(), 1, in); !out.Approved {
		t.Fatal(out)
	}
	db.Model(&p).UpdateColumn("price", "Rp99.000") // Even a write without updated_at must invalidate the quote.
	if out := s.checkAnswer(context.Background(), 1, in); out.Issue != "operational_changed" {
		t.Fatal(out)
	}
}

func TestAnswerSourceURLIdentifiesOriginWithoutGroundingPrice(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Harga kaos polos?", Answer: "Harga kaos polos Rp75.000.", Active: true, ReviewStatus: "published", Scope: "general", SourceURL: "https://store.example/products/99000"}
	if err := db.Create(&k).Error; err != nil {
		t.Fatal(err)
	}
	s := &agentTurnState{reviewAnswer: approveTestAnswer, userMessage: k.Question, knowledge: []models.Knowledge{k}, policy: responsePolicyForLength(selectAIResponsePolicy(k.Question, "", "", 1), "short", k.Question)}
	reviewed := false
	s.reviewAnswer = func(_ context.Context, _, _, evidence string) (answerEvidenceReview, error) {
		reviewed = true
		if !strings.Contains(evidence, k.SourceURL) {
			t.Error("source identity missing from semantic review")
		}
		return answerEvidenceReview{Supported: true}, nil
	}
	if out := s.checkAnswer(context.Background(), 1, answerCheckInput{Answer: "Harga kaos polos Rp99.000.", SourceIDs: []uint{k.ID}, Kind: "answer"}); out.Issue != "unsupported_numbers" || reviewed {
		t.Fatal("URL numeric path became a business price")
	}
	if out := s.checkAnswer(context.Background(), 1, answerCheckInput{Answer: k.Answer, SourceIDs: []uint{k.ID}, Kind: "answer"}); !out.Approved || !reviewed {
		t.Fatal("supported price rejected")
	}
}

func TestAnswerCheckRequiresReadingTruncatedKnowledge(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Apa syarat retur?", Answer: "Retur maksimal 7 hari. " + strings.Repeat("Barang harus belum dipakai. ", 120) + "Retur tidak berlaku untuk barang pesanan khusus.", Active: true, ReviewStatus: "published", Scope: "general"}
	db.Create(&k)
	k.Answer = string([]rune(k.Answer)[:2600])
	s := &agentTurnState{reviewAnswer: approveTestAnswer, userMessage: "Syarat retur?", knowledge: []models.Knowledge{k}, policy: responsePolicyForLength(selectAIResponsePolicy("retur?", "", "", 1), "short", "retur?")}
	out := s.checkAnswer(context.Background(), 1, answerCheckInput{Answer: "Retur maksimal 7 hari.", Kind: "answer", SourceIDs: []uint{k.ID}})
	if out.Issue != "source_incomplete" {
		t.Fatalf("unread tail can hide important exceptions: %+v", out)
	}
}

func TestDetailedResponseRequiresStructureWithoutPaddingShortAnswers(t *testing.T) {
	p := responsePolicyForLength(selectAIResponsePolicy("syarat?", "", "", 1), "detailed", "syarat?")
	if responseFormatIssue("Retur maksimal 7 hari.", p) != "" {
		t.Fatal("fakta sederhana dipaksa panjang")
	}
	if responseFormatIssue(strings.Repeat("Barang harus belum dipakai dan bukti pembelian perlu disertakan. ", 5), p) != "structure_required" {
		t.Fatal("paragraf panjang tidak diminta dipecah")
	}
	if responseFormatIssue("Syarat retur:\n1. Maksimal 7 hari.\n2. Barang belum dipakai.\n3. Sertakan bukti pembelian.", p) != "" {
		t.Fatal("nomor daftar dihitung sebagai kalimat tambahan")
	}
}

func TestAnswerCheckRejectsUnpublishedChangedForeignAndInventedFacts(t *testing.T) {
	db := agenticTestDB(t)
	source := models.Knowledge{AgentID: 1, Question: "Apa syarat retur?", Answer: "Retur maksimal 7 hari. Barang belum dipakai dan bukti pembelian wajib disertakan.", Active: true, ReviewStatus: "published", Scope: "general"}
	if err := db.Create(&source).Error; err != nil {
		t.Fatal(err)
	}
	state := &agentTurnState{reviewAnswer: approveTestAnswer, knowledge: []models.Knowledge{source}, userMessage: "Apa syarat retur?", policy: responsePolicyForLength(selectAIResponsePolicy("syarat?", "", "", 1), "short", "syarat?")}
	check := func(answer string, ids []uint) string {
		return state.checkAnswer(context.Background(), 1, answerCheckInput{Answer: answer, SourceIDs: ids, Kind: "answer"}).Issue
	}
	if issue := check("Retur maksimal 7 hari dengan barang belum dipakai dan bukti pembelian.", []uint{source.ID}); issue != "" {
		t.Fatal(issue)
	}
	if issue := check("Retur maksimal 9 hari, ada pertanyaan?", []uint{source.ID}); issue != "unsupported_numbers" {
		t.Fatalf("angka kecil lolos: %s", issue)
	}
	if issue := check("Retur maksimal 7 hari.", []uint{source.ID + 10}); issue != "unknown_source" {
		t.Fatal(issue)
	}
	if issue := check("Retur gratis selamanya, mau lanjut?", nil); issue != "missing_evidence" {
		t.Fatal("tanda tanya meloloskan fakta tanpa sumber", issue)
	}
	db.Model(&source).Update("answer", "Retur maksimal 3 hari.")
	if issue := check("Retur maksimal 7 hari.", []uint{source.ID}); issue != "source_changed" {
		t.Fatal(issue)
	}
	db.Model(&source).Update("review_status", "draft")
	if issue := check("Retur maksimal 7 hari.", []uint{source.ID}); issue != "source_unavailable" {
		t.Fatal(issue)
	}
	db.Model(&source).Updates(map[string]any{"review_status": "published", "effective_until": time.Now().Add(-time.Minute)})
	if issue := check("Retur maksimal 7 hari.", []uint{source.ID}); issue != "source_unavailable" {
		t.Fatal(issue)
	}
}

func TestAnswerCheckKeepsClarificationHonest(t *testing.T) {
	agenticTestDB(t)
	s := &agentTurnState{reviewAnswer: approveTestAnswer, userMessage: "Berapa harga paket?", policy: responsePolicyForLength(selectAIResponsePolicy("harga?", "", "", 1), "short", "harga?")}
	for _, tc := range []struct {
		text    string
		allowed bool
	}{
		{"Paket yang mana yang ingin ditanyakan?", true},
		{"Harganya Rp75.000, mau pesan?", false},
		{"Retur gratis untuk semua pelanggan. Mau paket yang mana?", false},
	} {
		out := s.checkAnswer(context.Background(), 1, answerCheckInput{Answer: tc.text, Kind: "clarification"})
		if (out.Issue == "") != tc.allowed {
			t.Fatalf("klarifikasi %q: %s", tc.text, out.Issue)
		}
	}
}
